2026 03 23 Command Center Realtime Migration
Documentation for Plans
Command Center Real-Time Migration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix the Mission Control tab (/command/mission) to show real-time data by connecting it to VittoMonitorService (SSE) + existing WebSocket, replacing the dead WebSocket-only architecture.
Architecture: Dual transport — SSE via VittoMonitorService for Vitto engine events (tasks, phases, delegations, checkpoints), WebSocket via existing CommandCenterGateway for MCP events (decisions, learning stats, pipeline). The component is extracted from a 2144-line inline monolith into separate .ts, .html, .scss files.
Tech Stack: Angular 20+, standalone components, signals, OnPush, RxJS, Socket.io-client, VittoMonitorService (SSE), VittoApiService (REST)
Spec: docs/superpowers/specs/2026-03-23-command-center-realtime-migration-design.md
IMPORTANT: Do NOT use git actions (commit, push, branch) — another session is running concurrently on the same branch.
File Structure
| File | Action | Responsibility |
|---|---|---|
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.ts |
Rewrite | Component class: services, signals, lifecycle, methods. No template/styles. |
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.html |
Create | Extracted template with structural changes for new panels |
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.scss |
Create | Extracted styles (unchanged from inline) |
libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/command-center.component.ts |
Delete | Stale duplicate — was byte-for-byte identical to canonical |
libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/index.ts |
Modify | Re-export from canonical location |
Task 1: Extract template to separate HTML file
The component has an inline template (lines 189-646) and inline styles (lines 648-1432). Extract both to separate files first, before making any logic changes. This ensures we can diff structural changes cleanly.
Files:
Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.ts:185-1432(remove inline template + styles)Create:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.htmlCreate:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.scssStep 1: Create the HTML file
Copy lines 190-646 (the content inside template: \...`) into command-center.component.html`. Do NOT modify the HTML content yet — this is a pure extraction.
- Step 2: Create the SCSS file
Copy lines 649-1431 (the content inside styles: [\...`]) into command-center.component.scss`. Do NOT modify styles yet.
- Step 3: Update the component decorator
Replace the inline template and styles in the @Component decorator:
@Component({
selector: 'lib-command-center',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './command-center.component.html',
styleUrls: ['./command-center.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
Key changes:
template:→templateUrl:styles:→styleUrls:Add
ChangeDetectionStrategy.OnPush(was missing — project standard violation)Add
ChangeDetectionStrategyto the import from@angular/coreStep 4: Verify the app still serves
Check the terminal running yarn serve:gograb-ai (port 6400). Navigate to http://localhost:6400/command/mission. It should render exactly as before (still OFFLINE — that's expected). The goal is zero visual regression from the extraction.
If the dev server is not running, verify with a build: nx build gograb-ai-lib — must produce 0 errors.
Task 2: Delete stale duplicate in vitto-hub
Files:
Delete:
libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/command-center.component.tsModify:
libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/index.tsStep 1: Replace the duplicate with a re-export
Replace the full 2144-line file libs/libs-ai/vitto-hub/src/lib/components/dashboards/command-center/command-center.component.ts with a re-export:
/**
* Re-export from canonical location.
* S262: Removed stale duplicate. Canonical source is @libs-ai/gograb-ai-lib.
*/
export { CommandCenterComponent } from '@libs-ai/gograb-ai-lib';
The index.ts already does export * from './command-center.component' — so this chain will work: index.ts → command-center.component.ts → @libs-ai/gograb-ai-lib.
- Step 2: Verify no imports break
Search for any imports of CommandCenterComponent from the vitto-hub path:
grep -r "dashboards/command-center" libs/ apps/ --include="*.ts" -l
If any files import directly from this path (other than the index.ts barrel), they need updating. Based on exploration, the routing at app.routes.ts:150 imports from @libs-ai/gograb-ai-lib, so this should be safe.
Task 3: Add VittoMonitorService SSE connection
This is the core fix. Add SSE connection alongside the existing WebSocket.
Files:
- Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.ts
IMPORTANT: When rewriting this file, preserve:
The
export { AI_API_URL }re-export at line 27 (consumers depend on it)All interface definitions at lines 33-179 (
SessionInfo,TaskState,AgentState,DecisionEvent,ApprovalRequest,CodePreview,RecordedEvent,LiveDecision,LearningStats,EventLog, and 6 MCP workflow types). They are consumed by WebSocket handlers andautoFlashStategenerics within this file.The
activeTasksignal (line 1456) andactiveAgentssignal (line 1457) — they serve as WebSocket fallbacks in the template and their handlers may receive data in the future.Step 1: Add new imports
Add these imports at the top of the file:
import { Component, OnInit, OnDestroy, signal, computed, inject, ChangeDetectionStrategy, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';
import { VittoMonitorService } from '../../services/vitto-monitor.service';
import { VittoApiService } from '@libs-ai/vitto-core';
Keep existing imports: CommonModule, FormsModule, io/Socket, autoFlashState, AI_API_URL.
- Step 2: Inject new services
Add to the class body, after the existing aiApiUrl injection:
private readonly monitor = inject(VittoMonitorService);
private readonly vittoApi = inject(VittoApiService);
private readonly destroyRef = inject(DestroyRef);
- Step 3: Add SSE-sourced signal aliases
Add after the existing signal declarations:
// ─── SSE-sourced signals (VittoMonitorService) ───────────────────────
readonly sseConnected = this.monitor.isConnected;
readonly vittoTask = this.monitor.currentTask;
readonly vittoPhase = this.monitor.currentPhase;
readonly vittoPhaseResults = this.monitor.phaseResults;
readonly vittoDelegations = this.monitor.delegations;
readonly vittoCheckpoints = this.monitor.pendingCheckpoints;
readonly vittoCheckpointCount = this.monitor.checkpointCount;
readonly vittoTaskHistory = this.monitor.taskHistory;
readonly vittoEngineStatus = this.monitor.engineStatus;
readonly vittoAlerts = this.monitor.groupedAlerts;
readonly vittoEventLog = this.monitor.eventLog;
- Step 4: Add agent definitions signal
// ─── System agent definitions (REST on init) ─────────────────────────
readonly agentDefinitions = signal<Array<{
id: string;
name: string;
role: string;
systemPrompt?: string;
allowedTools?: string[];
triggerKeywords?: string[];
performance?: { totalExecutions: number; successRate: number; avgQualityScore: number };
}>>([]);
- Step 5: Update the
isConnectedsignal
Replace the existing isConnected signal (line ~1454):
// OLD: readonly isConnected = signal(false);
// NEW: WebSocket connection state (renamed from isConnected)
readonly wsConnected = signal(false);
// Combined — dashboard is useful if either transport is active
readonly isConnected = computed(() => this.sseConnected() || this.wsConnected());
Note: wsConnected must be declared BEFORE isConnected because the computed references it.
Then update exactly two locations in connectToCommandCenter():
Line ~1681:
this.isConnected.set(true)→this.wsConnected.set(true)(in thesocket.on('connect')handler)Line ~1686:
this.isConnected.set(false)→this.wsConnected.set(false)(in thesocket.on('disconnect')handler)Step 6: Add merged event log
/** Merged event log from SSE + WebSocket, sorted newest-first */
readonly mergedEventLog = computed(() => {
const sseEvents = this.vittoEventLog().map(e => ({
type: e.type,
message: this.formatSSEEventMessage(e),
timestamp: new Date(e.timestamp),
}));
const wsEvents = this.eventLog();
return [...sseEvents, ...wsEvents]
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, 100);
});
Note: formatSSEEventMessage is defined in Task 6 Step 2. For now, declare it as a stub that returns e.type — it will be implemented in Task 6.
- Step 7: Update
ngOnInitto connect SSE and load agent definitions
Modify the existing ngOnInit:
ngOnInit(): void {
// 1. Connect SSE — VittoMonitorService handles reconnection.
// If another tab already connected, this reuses the EventSource.
this.monitor.connect();
// 2. Connect WebSocket for MCP events (existing logic)
this.connectToCommandCenter();
// 3. Load system agent definitions (seeded agents)
this.vittoApi.listAgentDefinitions().pipe(
catchError(() => of({ data: [] })),
takeUntilDestroyed(this.destroyRef),
).subscribe((res: any) => this.agentDefinitions.set(res?.data || []));
}
- Step 8: Update
ngOnDestroyto disconnect SSE
ngOnDestroy(): void {
this.monitor.disconnect();
this.socket?.disconnect();
if (this.approvalTimer) clearInterval(this.approvalTimer);
}
- Step 9: Rename the WebSocket connect method
Rename connectToCommandCenter() to connectWebSocket() for clarity. Update the call in ngOnInit. Inside the method, replace this.isConnected.set(true) → this.wsConnected.set(true) and this.isConnected.set(false) → this.wsConnected.set(false).
- Step 10: Verify SSE connection works
Check the terminal. Navigate to /command/mission. The connection indicator should now show as connected (at least the SSE side) instead of "OFFLINE", assuming backend-vitto-api is running on port 3003.
Task 4: Update template for SSE-sourced panels
Files:
Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.htmlStep 1: Update connection indicator
Replace the status badge in the header:
<span class="cc-status" [class.online]="isConnected()" [class.paused]="isPaused()">
@if (isPaused()) {
⏸️ PAUSED
} @else if (isConnected()) {
● LIVE
} @else {
○ OFFLINE
}
</span>
Add a diagnostic indicator after the status badge:
<span class="cc-transport-status">
<span [class.live]="sseConnected()">SSE</span> |
<span [class.live]="wsConnected()">WS</span>
</span>
- Step 2: Update Active Task panel to show Vitto phases
Replace the Active Task section with one that includes phase progress:
<section class="cc-panel task-panel">
<h2>📡 Active Task</h2>
@if (vittoTask()) {
<div class="task-card" [class]="vittoTask()?.status">
<div class="task-header">
<span class="task-mode">
{{ vittoTask()?.type || 'task' }}
</span>
<span class="task-status" [class]="vittoTask()?.status">
{{ vittoTask()?.status | uppercase }}
</span>
</div>
<p class="task-description">{{ vittoTask()?.summary || vittoTask()?.description }}</p>
<!-- Phase Progress (ANALYZE → PLAN → EXECUTE → VERIFY) -->
@if (vittoPhaseResults().length > 0) {
<div class="phase-progress">
@for (phase of vittoPhaseResults(); track phase.phase) {
<div class="phase-step" [class.active]="vittoPhase() === phase.phase"
[class.completed]="phase.status === 'completed'"
[class.running]="phase.status === 'running'">
<span class="phase-icon">
@if (phase.status === 'completed') { ✅ }
@else if (phase.status === 'running') { 🔄 }
@else { ⏳ }
</span>
<span class="phase-name">{{ phase.phase }}</span>
</div>
}
</div>
}
</div>
} @else if (activeTask()) {
<!-- Fallback to WebSocket task if SSE has none -->
<div class="task-card" [class]="activeTask()?.status">
<div class="task-header">
<span class="task-mode" [class.orchestrated]="activeTask()?.mode === 'orchestrated'">
{{ activeTask()?.mode === 'orchestrated' ? '🎭 Multi-Agent' : '🚀 Quick' }}
</span>
<span class="task-status" [class]="activeTask()?.status">
{{ activeTask()?.status | uppercase }}
</span>
</div>
<p class="task-description">{{ activeTask()?.task }}</p>
<div class="task-progress">
<div class="progress-bar" [style.width.%]="activeTask()?.progress || 0"></div>
</div>
</div>
} @else {
<div class="task-card idle">
<p class="idle-message">No active task</p>
</div>
}
</section>
- Step 3: Replace "Execution Agents" panel with "Active Delegations"
Replace the entire execution-agents-panel section:
<section class="cc-panel execution-agents-panel">
<h2>🤖 Active Delegations</h2>
<p class="panel-subtitle">Sub-tasks delegated by Vitto to specialist agents</p>
<div class="agents-grid">
@for (delegation of vittoDelegations(); track delegation.handoffId) {
<div class="agent-card execution" [class]="delegation.status">
<div class="agent-icon">
@switch (delegation.agentType) {
@case ('code') { ⚡ }
@case ('browser') { 🌐 }
@case ('researcher') { 🔍 }
@default { 🤖 }
}
</div>
<div class="agent-info">
<span class="agent-type">{{ delegation.agentType || 'specialist' }}</span>
<span class="agent-status">{{ delegation.status }}</span>
@if (delegation.progress) {
<span class="agent-progress">{{ delegation.progress }}%</span>
}
</div>
</div>
} @empty {
<div class="no-agents">No active delegations</div>
}
</div>
</section>
- Step 4: Replace "Training Agents" panel with "System Agents"
Replace the entire training-agents-panel section:
<section class="cc-panel training-agents-panel">
<h2>🎓 System Agents</h2>
<p class="panel-subtitle">Persistent specialist agents seeded on startup</p>
<div class="agents-grid">
@for (agent of agentDefinitions(); track agent.id) {
<div class="agent-card training">
<div class="agent-icon training-icon">
@if (agent.role?.includes('module')) { 📦 }
@else if (agent.role?.includes('widget')) { 🧩 }
@else if (agent.role?.includes('architecture')) { 🏗️ }
@else if (agent.role?.includes('quality')) { ✅ }
@else { 🤖 }
</div>
<div class="agent-info">
<span class="agent-type training-label">{{ agent.name }}</span>
@if (agent.performance) {
<span class="learning-status">
{{ agent.performance.totalExecutions }} runs •
{{ (agent.performance.successRate * 100).toFixed(0) }}% success
</span>
}
</div>
</div>
} @empty {
<div class="no-agents training-hint">
<span class="hint-icon">💡</span>
<span>No system agents registered. Is backend-vitto-api running?</span>
</div>
}
</div>
</section>
- Step 5: Add Checkpoints panel
Add this new panel after the Active Task section in the grid:
<!-- Autonomy Checkpoints -->
@if (vittoCheckpoints().length > 0) {
<section class="cc-panel checkpoints-panel">
<h2>🚧 Autonomy Checkpoints ({{ vittoCheckpointCount() }})</h2>
<div class="checkpoints-list">
@for (cp of vittoCheckpoints(); track cp.id) {
<div class="checkpoint-card">
<p class="checkpoint-reason">{{ cp.reason }}</p>
<div class="checkpoint-actions">
<button class="control-btn resume" (click)="resolveCheckpoint(cp.id, 'approved')">
✅ Approve
</button>
<button class="control-btn cancel" (click)="resolveCheckpoint(cp.id, 'rejected')">
❌ Reject
</button>
</div>
</div>
}
</div>
</section>
}
- Step 6: Add Proactive Alerts panel
Add after the Checkpoints panel:
<!-- Proactive Alerts -->
@if (vittoAlerts().length > 0) {
<section class="cc-panel alerts-panel">
<h2>⚠️ Proactive Alerts ({{ vittoAlerts().length }})</h2>
<div class="alerts-list">
@for (alert of vittoAlerts(); track alert.message) {
<div class="alert-card" [class]="alert.severity">
<span class="alert-severity">
@switch (alert.severity) {
@case ('critical') { 🔴 }
@case ('warning') { 🟡 }
@default { 🔵 }
}
</span>
<span class="alert-message">{{ alert.message }}</span>
@if (alert.count > 1) {
<span class="alert-count">×{{ alert.count }}</span>
}
</div>
}
</div>
</section>
}
- Step 7: Update header stats Events count
In the stats bar, update the Events counter to use the merged log:
<div class="stat">
<span class="stat-value">{{ mergedEventLog().length }}</span>
<span class="stat-label">Events</span>
</div>
- Step 8: Update Event Log to use merged log
In the Event Log section, replace eventLog() with mergedEventLog():
<section class="cc-panel log-panel">
<h2>📜 Event Log</h2>
<div class="event-log">
@for (event of mergedEventLog().slice(0, 30); track $index) {
<div class="log-entry" [class]="event.type">
<span class="log-time">{{ formatTime(event.timestamp) }}</span>
<span class="log-type">{{ event.type }}</span>
<span class="log-message">{{ event.message }}</span>
</div>
} @empty {
<p class="no-events">Waiting for events...</p>
}
</div>
</section>
Task 5: Add styles for new panels and connection indicator
Files:
Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.scssStep 1: Add transport status indicator styles
Append to the SCSS file:
.cc-transport-status {
font-size: 0.7rem;
color: var(--cc-text-muted);
font-family: 'Courier New', monospace;
span {
opacity: 0.5;
&.live {
opacity: 1;
color: var(--cc-success);
}
}
}
- Step 2: Add phase progress styles
.phase-progress {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--cc-border);
.phase-step {
flex: 1;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.375rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
background: rgba(100, 116, 139, 0.1);
opacity: 0.5;
&.active {
opacity: 1;
background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.4);
}
&.completed {
opacity: 1;
background: rgba(16, 185, 129, 0.15);
}
&.running {
opacity: 1;
animation: thinking 1s infinite;
}
.phase-icon { font-size: 0.875rem; }
.phase-name { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
}
}
- Step 3: Add checkpoint and alert panel styles
.checkpoints-panel {
grid-column: span 2;
.checkpoints-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkpoint-card {
padding: 0.75rem;
border-radius: 8px;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
.checkpoint-reason { margin: 0 0 0.5rem; font-size: 0.875rem; }
.checkpoint-actions { display: flex; gap: 0.5rem; }
}
}
.alerts-panel {
grid-column: span 1;
.alerts-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
max-height: 200px;
overflow-y: auto;
}
.alert-card {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
&.critical { background: rgba(239, 68, 68, 0.15); }
&.warning { background: rgba(245, 158, 11, 0.1); }
&.info { background: rgba(100, 116, 139, 0.1); }
.alert-count {
margin-left: auto;
font-weight: 700;
color: var(--cc-text-muted);
}
}
}
Task 6: Add checkpoint resolution method and verify
Files:
Modify:
libs/libs-ai/gograb-ai-lib/src/lib/components/command-center/command-center.component.tsStep 1: Add the resolveCheckpoint method
Add this method to the component class:
resolveCheckpoint(id: string, decision: 'approved' | 'rejected'): void {
this.monitor.resolveCheckpoint(id, decision).pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe({
next: () => this.addLog('checkpoint', `Checkpoint ${decision}: ${id.slice(0, 8)}...`),
error: (err) => this.addLog('error', `Failed to resolve checkpoint: ${err.message}`),
});
}
- Step 2: Add formatSSEEventMessage method
Ensure the method exists — it was referenced by mergedEventLog computed in Task 3 Step 6:
private formatSSEEventMessage(event: { type: string; data?: Record<string, unknown> }): string {
switch (event.type) {
case 'task.started': return `Task started: ${(event.data?.['summary'] as string)?.slice(0, 50) || 'unknown'}`;
case 'task.completed': return 'Task completed';
case 'task.failed': return `Task failed: ${(event.data?.['error'] as string) || 'unknown'}`;
case 'phase.started': return `Phase started: ${event.data?.['phase']}`;
case 'phase.completed': return `Phase completed: ${event.data?.['phase']}`;
case 'tool.called': return `Tool: ${(event.data?.['toolName'] as string) || (event.data?.['name'] as string) || 'unknown'}`;
case 'vitto.delegation': return `Delegation: ${event.data?.['agentType']} (${event.data?.['status']})`;
case 'vitto.checkpoint.created': return `Checkpoint: ${(event.data?.['reason'] as string)?.slice(0, 40) || 'approval needed'}`;
case 'heartbeat': return 'Heartbeat';
default: return event.type;
}
}
- Step 3: Remove stale computed signals that are now unused
The old executionAgents and trainingAgents computed signals (lines ~1471-1476) are no longer referenced in the template. Remove them:
// DELETE these:
// readonly executionAgents = computed<AgentState[]>(...)
// readonly trainingAgents = computed<AgentState[]>(...)
Also remove these dead computed signals — they are never referenced in the template:
filteredTasks(line ~1479)filteredAgents(line ~1485)filteredDecisions(line ~1490)
Keep currentSessionFilter and activeSessions — the session filter dropdown is still in the template (line 210: [(ngModel)]="currentSessionFilter").
- Step 4: Final verification
Navigate to http://localhost:6400/command/mission. Verify:
- Connection indicator shows at least SSE as LIVE (if backend-vitto-api is running)
- If a Vitto task is active, the Active Task panel shows it with phase progress
- System Agents panel shows seeded agents (if backend-vitto-api is running and agents are seeded)
- Active Delegations panel shows delegations when a task with sub-agents runs
- WebSocket panels still work: decisions feed, MCP pipeline, learning stats
- Event Log shows merged events from both sources
- No console errors
Summary
| Task | Description | Risk |
|---|---|---|
| 1 | Extract inline template/styles to .html/.scss files + add OnPush | Low — mechanical |
| 2 | Delete stale duplicate in vitto-hub | Low — re-export preserves API |
| 3 | Add VittoMonitorService SSE + VittoApiService REST | Medium — core change |
| 4 | Update template for SSE-sourced panels | Medium — structural HTML changes |
| 5 | Add styles for new panels | Low — additive CSS |
| 6 | Add checkpoint resolution + verify | Low — single method + cleanup |