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:

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.

Copy lines 649-1431 (the content inside styles: [\...`]) into command-center.component.scss`. Do NOT modify styles yet.

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:

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:

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.

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:

IMPORTANT: When rewriting this file, preserve:

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.

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);

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;
// ─── 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 };
}>>([]);

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():

/** 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.

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 || []));
}
ngOnDestroy(): void {
  this.monitor.disconnect();
  this.socket?.disconnect();
  if (this.approvalTimer) clearInterval(this.approvalTimer);
}

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).

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:

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>

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>

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>

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>

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>
}

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>
}

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>

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:

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);
    }
  }
}
.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; }
  }
}
.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:

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}`),
  });
}

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;
  }
}

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:

Keep currentSessionFilter and activeSessions — the session filter dropdown is still in the template (line 210: [(ngModel)]="currentSessionFilter").

Navigate to http://localhost:6400/command/mission. Verify:

  1. Connection indicator shows at least SSE as LIVE (if backend-vitto-api is running)
  2. If a Vitto task is active, the Active Task panel shows it with phase progress
  3. System Agents panel shows seeded agents (if backend-vitto-api is running and agents are seeded)
  4. Active Delegations panel shows delegations when a task with sub-agents runs
  5. WebSocket panels still work: decisions feed, MCP pipeline, learning stats
  6. Event Log shows merged events from both sources
  7. 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