beadboard/docs/plans/2026-03-08-phase-4-implementation.md
zenchantlive d335e5bf71 fix: orchestrator button + Pi SDK session error
- Move leftSidebarMode from URL state to local useState in unified-shell,
    avoiding force-dynamic router round-trip that made the button appear broken                                           - Replace fileURLToPath(new URL(..., import.meta.url)) with process.cwd()
    in bb-pi-bootstrap.ts — import.meta.url is a webpack:// URL in Next.js,
    causing cross-realm TypeError when passed to Node.js fileURLToPath()
2026-03-24 19:02:04 -05:00

27 KiB

Phase 4: Launch-Anywhere UX Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add spawn affordances to all UI surfaces with a reusable two-icon system (assign + spawn) that works on social cards, graph nodes, and blocked triage modal.

Architecture: Create reusable components (AgentActionRow, AgentAssignButton, AgentSpawnButton) and hooks (useAgentStatus, useSpawnAgent) in src/components/agents/. Each surface imports AgentActionRow which handles the full assign → spawn flow. Icon colors reflect agent/worker status (blue=ready, green=working, red=blocked).

Tech Stack: React, TypeScript, Lucide icons, existing agent types from types-swarm.ts


Prerequisites

  • Phase 3 complete (agents, workers, beads work)
  • bb_spawn_worker tool exists and works
  • Worker session manager tracks status

Task 1: Create useAgentStatus Hook

Files:

  • Create: src/components/agents/hooks/use-agent-status.ts

Step 1: Create hook file with interface

// src/components/agents/hooks/use-agent-status.ts
import { useState, useEffect } from 'react';

export type WorkerStatus = 'idle' | 'spawning' | 'working' | 'blocked' | 'completed' | 'failed';

export interface AgentStatus {
  agentTypeId?: string;
  workerStatus: WorkerStatus;
  workerDisplayName?: string;
  isLoading: boolean;
}

export function useAgentStatus(beadId: string): AgentStatus {
  const [status, setStatus] = useState<AgentStatus>({
    workerStatus: 'idle',
    isLoading: true,
  });

  useEffect(() => {
    // TODO: Fetch from agent status API
    setStatus({ workerStatus: 'idle', isLoading: false });
  }, [beadId]);

  return status;
}

Step 2: Commit

git add src/components/agents/hooks/use-agent-status.ts
git commit -m "feat: add useAgentStatus hook interface"

Task 2: Create useSpawnAgent Hook

Files:

  • Create: src/components/agents/hooks/use-spawn-agent.ts

Step 1: Create spawn hook

// src/components/agents/hooks/use-spawn-agent.ts
import { useState } from 'react';

export interface SpawnResult {
  success: boolean;
  workerId?: string;
  displayName?: string;
  error?: string;
}

export function useSpawnAgent(projectRoot: string) {
  const [isSpawning, setIsSpawning] = useState(false);

  const spawn = async (beadId: string, agentTypeId: string): Promise<SpawnResult> => {
    setIsSpawning(true);
    try {
      const response = await fetch('/api/runtime/spawn', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ projectRoot, beadId, agentTypeId }),
      });
      const data = await response.json();
      
      if (!data.ok) {
        return { success: false, error: data.error };
      }
      
      return {
        success: true,
        workerId: data.workerId,
        displayName: data.displayName,
      };
    } catch (error) {
      return { success: false, error: String(error) };
    } finally {
      setIsSpawning(false);
    }
  };

  return { spawn, isSpawning };
}

Step 2: Commit

git add src/components/agents/hooks/use-spawn-agent.ts
git commit -m "feat: add useSpawnAgent hook"

Task 3: Create hooks index

Files:

  • Create: src/components/agents/hooks/index.ts

Step 1: Create index file

// src/components/agents/hooks/index.ts
export { useAgentStatus, type AgentStatus, type WorkerStatus } from './use-agent-status';
export { useSpawnAgent, type SpawnResult } from './use-spawn-agent';

Step 2: Commit

git add src/components/agents/hooks/index.ts
git commit -m "feat: add agents hooks index"

Task 4: Create AgentPickerPopup Component

Files:

  • Create: src/components/agents/agent-picker-popup.tsx

Step 1: Create picker popup

// src/components/agents/agent-picker-popup.tsx
'use client';

import { useEffect, useRef } from 'react';
import { Rocket, Brain, Wrench, Search, CheckCircle, FlaskConical, Upload } from 'lucide-react';
import type { AgentArchetype } from '../../lib/types-swarm';

export interface AgentPickerPopupProps {
  isOpen: boolean;
  onClose: () => void;
  agents: AgentArchetype[];
  selectedAgentId?: string;
  onSelect: (agentId: string) => void;
  onSpawn?: (agentId: string) => void;
  position?: { x: number; y: number };
}

const AGENT_ICONS: Record<string, React.ReactNode> = {
  architect: <Brain className="w-4 h-4" />,
  engineer: <Wrench className="w-4 h-4" />,
  investigator: <Search className="w-4 h-4" />,
  reviewer: <CheckCircle className="w-4 h-4" />,
  tester: <FlaskConical className="w-4 h-4" />,
  shipper: <Upload className="w-4 h-4" />,
};

export function AgentPickerPopup({
  isOpen,
  onClose,
  agents,
  selectedAgentId,
  onSelect,
  onSpawn,
  position,
}: AgentPickerPopupProps) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        onClose();
      }
    };
    if (isOpen) {
      document.addEventListener('mousedown', handleClickOutside);
    }
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  const style = position
    ? { position: 'absolute' as const, left: position.x, top: position.y + 8 }
    : {};

  return (
    <div
      ref={ref}
      style={style}
      className="z-50 min-w-[180px] rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-elevated)] p-1 shadow-lg"
    >
      {/* Orchestrator option */}
      <button
        onClick={() => {
          onSelect('orchestrator');
          onClose();
        }}
        className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors ${
          selectedAgentId === 'orchestrator'
            ? 'bg-[var(--accent-info)]/20 text-[var(--accent-info)]'
            : 'text-[var(--text-primary)] hover:bg-[var(--surface-hover)]'
        }`}
      >
        <Rocket className="w-4 h-4" />
        <span className="font-medium">Orchestrator</span>
        <span className="ml-auto text-xs text-[var(--text-tertiary)]">auto</span>
      </button>

      <div className="my-1 border-t border-[var(--border-subtle)]" />

      {/* Agent types */}
      {agents.map((agent) => (
        <button
          key={agent.id}
          onClick={() => {
            onSelect(agent.id);
            onClose();
          }}
          className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors ${
            selectedAgentId === agent.id
              ? 'bg-[var(--accent-info)]/20 text-[var(--accent-info)]'
              : 'text-[var(--text-primary)] hover:bg-[var(--surface-hover)]'
          }`}
        >
          <span style={{ color: agent.color }}>
            {AGENT_ICONS[agent.id] || <Wrench className="w-4 h-4" />}
          </span>
          <span>{agent.name}</span>
        </button>
      ))}

      {/* Spawn button */}
      {onSpawn && selectedAgentId && (
        <>
          <div className="my-1 border-t border-[var(--border-subtle)]" />
          <button
            onClick={() => {
              onSpawn(selectedAgentId);
              onClose();
            }}
            className="flex w-full items-center justify-center gap-2 rounded-md bg-emerald-500/20 px-3 py-2 text-sm font-medium text-emerald-400 transition-colors hover:bg-emerald-500/30"
          >
            <Rocket className="w-4 h-4" />
            Spawn {agents.find(a => a.id === selectedAgentId)?.name || 'Agent'}
          </button>
        </>
      )}
    </div>
  );
}

Step 2: Commit

git add src/components/agents/agent-picker-popup.tsx
git commit -m "feat: add AgentPickerPopup component"

Task 5: Create AgentAssignButton Component

Files:

  • Create: src/components/agents/agent-assign-button.tsx

Step 1: Create assign button

// src/components/agents/agent-assign-button.tsx
'use client';

import { useState } from 'react';
import { UserPlus } from 'lucide-react';
import { AgentPickerPopup } from './agent-picker-popup';
import type { AgentArchetype } from '../../lib/types-swarm';

export interface AgentAssignButtonProps {
  beadId: string;
  agents: AgentArchetype[];
  currentAgentTypeId?: string;
  onAssign: (agentTypeId: string) => void;
  size?: 'sm' | 'md';
  disabled?: boolean;
}

export function AgentAssignButton({
  beadId,
  agents,
  currentAgentTypeId,
  onAssign,
  size = 'sm',
  disabled = false,
}: AgentAssignButtonProps) {
  const [isOpen, setIsOpen] = useState(false);

  const sizeClasses = size === 'sm' 
    ? 'h-6 w-6' 
    : 'h-7 w-7';

  const iconSize = size === 'sm' 
    ? 'w-3 h-3' 
    : 'w-3.5 h-3.5';

  const isAssigned = !!currentAgentTypeId;
  const assignedAgent = agents.find(a => a.id === currentAgentTypeId);
  const bgColor = isAssigned && assignedAgent 
    ? `${assignedAgent.color}30` 
    : 'var(--surface-tertiary)';
  const iconColor = isAssigned && assignedAgent 
    ? assignedAgent.color 
    : 'var(--text-tertiary)';

  return (
    <div className="relative">
      <button
        type="button"
        onClick={() => !disabled && setIsOpen(true)}
        disabled={disabled}
        className={`inline-flex ${sizeClasses} items-center justify-center rounded-md border transition-colors ${
          disabled 
            ? 'opacity-50 cursor-not-allowed' 
            : 'hover:opacity-80'
        }`}
        style={{
          backgroundColor: bgColor,
          borderColor: isAssigned && assignedAgent 
            ? `${assignedAgent.color}50` 
            : 'var(--border-subtle)',
        }}
        title={isAssigned ? `Assigned: ${assignedAgent?.name}` : 'Assign agent'}
      >
        <UserPlus className={iconSize} style={{ color: iconColor }} />
      </button>

      <AgentPickerPopup
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        agents={agents}
        selectedAgentId={currentAgentTypeId}
        onSelect={(agentId) => {
          onAssign(agentId);
          setIsOpen(false);
        }}
      />
    </div>
  );
}

Step 2: Commit

git add src/components/agents/agent-assign-button.tsx
git commit -m "feat: add AgentAssignButton component"

Task 6: Create AgentSpawnButton Component

Files:

  • Create: src/components/agents/agent-spawn-button.tsx

Step 1: Create spawn button with color states

// src/components/agents/agent-spawn-button.tsx
'use client';

import { Rocket, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import type { WorkerStatus } from './hooks/use-agent-status';

export interface AgentSpawnButtonProps {
  beadId: string;
  agentTypeId?: string;
  workerStatus: WorkerStatus;
  workerDisplayName?: string;
  workerError?: string;
  onSpawn: () => void;
  size?: 'sm' | 'md';
  disabled?: boolean;
}

const STATUS_CONFIG: Record<WorkerStatus, {
  icon: React.ReactNode;
  color: string;
  bgColor: string;
  borderColor: string;
  title: string;
  pulsing?: boolean;
}> = {
  idle: {
    icon: <Rocket className="w-3 h-3" />,
    color: '#6b7280',
    bgColor: 'rgba(107, 114, 128, 0.1)',
    borderColor: 'rgba(107, 114, 128, 0.3)',
    title: 'No agent assigned',
  },
  spawning: {
    icon: <Loader2 className="w-3 h-3 animate-spin" />,
    color: '#3b82f6',
    bgColor: 'rgba(59, 130, 246, 0.1)',
    borderColor: 'rgba(59, 130, 246, 0.3)',
    title: 'Spawning...',
    pulsing: true,
  },
  working: {
    icon: <Rocket className="w-3 h-3" />,
    color: '#22c55e',
    bgColor: 'rgba(34, 197, 94, 0.1)',
    borderColor: 'rgba(34, 197, 94, 0.3)',
    title: 'Working',
    pulsing: true,
  },
  blocked: {
    icon: <AlertCircle className="w-3 h-3" />,
    color: '#ef4444',
    bgColor: 'rgba(239, 68, 68, 0.1)',
    borderColor: 'rgba(239, 68, 68, 0.3)',
    title: 'Blocked',
  },
  completed: {
    icon: <CheckCircle className="w-3 h-3" />,
    color: '#22c55e',
    bgColor: 'rgba(34, 197, 94, 0.1)',
    borderColor: 'rgba(34, 197, 94, 0.3)',
    title: 'Completed',
  },
  failed: {
    icon: <AlertCircle className="w-3 h-3" />,
    color: '#ef4444',
    bgColor: 'rgba(239, 68, 68, 0.1)',
    borderColor: 'rgba(239, 68, 68, 0.3)',
    title: 'Failed',
  },
};

export function AgentSpawnButton({
  beadId,
  agentTypeId,
  workerStatus,
  workerDisplayName,
  workerError,
  onSpawn,
  size = 'sm',
  disabled = false,
}: AgentSpawnButtonProps) {
  const config = STATUS_CONFIG[workerStatus];
  const sizeClasses = size === 'sm' ? 'h-6 w-6' : 'h-7 w-7';
  
  // No agent assigned - don't show button
  if (!agentTypeId && workerStatus === 'idle') {
    return null;
  }

  const canSpawn = workerStatus === 'idle' && agentTypeId;
  const showTooltip = workerStatus === 'working' || workerStatus === 'blocked' || workerStatus === 'completed';

  return (
    <div className="relative group">
      <button
        type="button"
        onClick={() => canSpawn && !disabled && onSpawn()}
        disabled={disabled || !canSpawn}
        className={`inline-flex ${sizeClasses} items-center justify-center rounded-md border transition-colors ${
          disabled || !canSpawn ? 'cursor-default' : 'hover:opacity-80'
        } ${config.pulsing ? 'animate-pulse' : ''}`}
        style={{
          backgroundColor: config.bgColor,
          borderColor: config.borderColor,
          color: config.color,
        }}
        title={workerDisplayName ? `${config.title}: ${workerDisplayName}` : config.title}
      >
        {config.icon}
      </button>

      {/* Tooltip for active workers */}
      {showTooltip && (
        <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block z-50">
          <div className="rounded-md bg-[var(--surface-elevated)] border border-[var(--border-subtle)] px-3 py-2 shadow-lg min-w-[160px]">
            <p className="text-xs font-medium text-[var(--text-primary)]">
              {workerDisplayName || 'Agent'}
            </p>
            <p className="text-[10px] text-[var(--text-tertiary)] capitalize">
              {workerStatus}
            </p>
            {workerError && (
              <p className="text-[10px] text-red-400 mt-1 truncate">
                {workerError}
              </p>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Step 2: Commit

git add src/components/agents/agent-spawn-button.tsx
git commit -m "feat: add AgentSpawnButton with color states"

Task 7: Create AgentActionRow Component

Files:

  • Create: src/components/agents/agent-action-row.tsx

Step 1: Create combined action row

// src/components/agents/agent-action-row.tsx
'use client';

import { AgentAssignButton } from './agent-assign-button';
import { AgentSpawnButton } from './agent-spawn-button';
import { useAgentStatus, useSpawnAgent } from './hooks';
import type { AgentArchetype } from '../../lib/types-swarm';

export interface AgentActionRowProps {
  beadId: string;
  beadStatus: string;
  agents: AgentArchetype[];
  projectRoot: string;
  currentAgentTypeId?: string;
  onAgentAssigned?: (agentTypeId: string) => void;
  onAgentSpawned?: (workerId: string, displayName: string) => void;
  size?: 'sm' | 'md';
}

export function AgentActionRow({
  beadId,
  beadStatus,
  agents,
  projectRoot,
  currentAgentTypeId,
  onAgentAssigned,
  onAgentSpawned,
  size = 'sm',
}: AgentActionRowProps) {
  const { workerStatus, workerDisplayName, workerError } = useAgentStatus(beadId);
  const { spawn, isSpawning } = useSpawnAgent(projectRoot);

  const handleAssign = async (agentTypeId: string) => {
    // Call API to assign agent type to bead
    try {
      const response = await fetch('/api/beads/assign-agent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ beadId, agentTypeId }),
      });
      if (response.ok && onAgentAssigned) {
        onAgentAssigned(agentTypeId);
      }
    } catch (error) {
      console.error('Failed to assign agent:', error);
    }
  };

  const handleSpawn = async () => {
    if (!currentAgentTypeId) return;
    
    const result = await spawn(beadId, currentAgentTypeId);
    if (result.success && onAgentSpawned) {
      onAgentSpawned(result.workerId!, result.displayName!);
    }
  };

  // Don't show for closed beads
  if (beadStatus === 'closed') {
    return null;
  }

  return (
    <div className="flex items-center gap-1.5">
      <AgentAssignButton
        beadId={beadId}
        agents={agents}
        currentAgentTypeId={currentAgentTypeId}
        onAssign={handleAssign}
        size={size}
      />
      <AgentSpawnButton
        beadId={beadId}
        agentTypeId={currentAgentTypeId}
        workerStatus={isSpawning ? 'spawning' : workerStatus}
        workerDisplayName={workerDisplayName}
        workerError={workerError}
        onSpawn={handleSpawn}
        size={size}
      />
    </div>
  );
}

Step 2: Commit

git add src/components/agents/agent-action-row.tsx
git commit -m "feat: add AgentActionRow combining assign + spawn"

Task 8: Create agents index

Files:

  • Create: src/components/agents/index.ts

Step 1: Create index exports

// src/components/agents/index.ts
export { AgentActionRow, type AgentActionRowProps } from './agent-action-row';
export { AgentAssignButton, type AgentAssignButtonProps } from './agent-assign-button';
export { AgentSpawnButton, type AgentSpawnButtonProps } from './agent-spawn-button';
export { AgentPickerPopup, type AgentPickerPopupProps } from './agent-picker-popup';
export * from './hooks';

Step 2: Commit

git add src/components/agents/index.ts
git commit -m "feat: add agents components index"

Task 9: Add AgentActionRow to SocialCard

Files:

  • Modify: src/components/social/social-card.tsx

Step 1: Import and add AgentActionRow

Find the actions area in social-card.tsx (around the rocket button) and replace with:

// Add import at top
import { AgentActionRow } from '../agents';

// In the component props, ensure these are available:
// - projectRoot: string
// - archetypes: AgentArchetype[]

// Find the button area (around line 369) and add:
{projectRoot && (
  <AgentActionRow
    beadId={data.id}
    beadStatus={data.status}
    agents={archetypes}
    projectRoot={projectRoot}
    currentAgentTypeId={data.agentTypeId}
    size="sm"
  />
)}

Step 2: Commit

git add src/components/social/social-card.tsx
git commit -m "feat: add AgentActionRow to SocialCard"

Task 10: Add AgentActionRow to GraphNodeCard

Files:

  • Modify: src/components/graph/graph-node-card.tsx

Step 1: Import and add AgentActionRow

// Add import
import { AgentActionRow } from '../agents';

// Find the actions area in the node card and add:
<AgentActionRow
  beadId={data.id}
  beadStatus={data.status}
  agents={archetypes || []}
  projectRoot={projectRoot}
  currentAgentTypeId={data.agentTypeId}
  size="sm"
/>

Step 2: Commit

git add src/components/graph/graph-node-card.tsx
git commit -m "feat: add AgentActionRow to GraphNodeCard"

Task 11: Add AgentActionRow to BlockedTriageModal

Files:

  • Modify: src/components/shared/blocked-triage-modal.tsx

Step 1: Import and add AgentActionRow

// Add import
import { AgentActionRow } from '../agents';

// In each task row, add the action row:
<AgentActionRow
  beadId={issue.id}
  beadStatus={issue.status}
  agents={archetypes}
  projectRoot={projectRoot}
  currentAgentTypeId={issue.agentTypeId}
  size="md"
/>

Step 2: Commit

git add src/components/shared/blocked-triage-modal.tsx
git commit -m "feat: add AgentActionRow to BlockedTriageModal"

Task 12: Create Spawn API Endpoint

Files:

  • Create: src/app/api/runtime/spawn/route.ts

Step 1: Create spawn API

// src/app/api/runtime/spawn/route.ts
import { NextResponse } from 'next/server';
import { workerSessionManager } from '../../../../lib/worker-session-manager';

export async function POST(request: Request) {
  try {
    const { projectRoot, beadId, agentTypeId } = await request.json();

    if (!projectRoot || !beadId || !agentTypeId) {
      return NextResponse.json({
        ok: false,
        error: 'projectRoot, beadId, and agentTypeId are required',
      });
    }

    // Spawn worker via session manager
    const worker = await workerSessionManager.spawnWorker({
      projectRoot,
      taskId: beadId,
      taskContext: `Work on ${beadId}`,
      agentType: agentTypeId,
      beadId,
    });

    return NextResponse.json({
      ok: true,
      workerId: worker.id,
      displayName: worker.displayName,
      agentTypeId: worker.agentTypeId,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ ok: false, error: message });
  }
}

Step 2: Commit

git add src/app/api/runtime/spawn/route.ts
git commit -m "feat: add spawn API endpoint"

Task 13: Create Assign Agent API Endpoint

Files:

  • Create: src/app/api/beads/assign-agent/route.ts

Step 1: Create assign API

// src/app/api/beads/assign-agent/route.ts
import { NextResponse } from 'next/server';
import { execFileSync } from 'child_process';

export async function POST(request: Request) {
  try {
    const { beadId, agentTypeId } = await request.json();

    if (!beadId || !agentTypeId) {
      return NextResponse.json({
        ok: false,
        error: 'beadId and agentTypeId are required',
      });
    }

    // Use bd CLI to add agent label
    execFileSync('bd', [
      'label',
      'add',
      beadId,
      `agent:${agentTypeId}`,
    ], {
      encoding: 'utf-8',
      timeout: 10000,
    });

    return NextResponse.json({ ok: true, beadId, agentTypeId });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ ok: false, error: message });
  }
}

Step 2: Commit

git add src/app/api/beads/assign-agent/route.ts
git commit -m "feat: add assign-agent API endpoint"

Task 14: Update useAgentStatus to fetch real data

Files:

  • Modify: src/components/agents/hooks/use-agent-status.ts

Step 1: Implement real status fetching

// src/components/agents/hooks/use-agent-status.ts
import { useState, useEffect } from 'react';

export type WorkerStatus = 'idle' | 'spawning' | 'working' | 'blocked' | 'completed' | 'failed';

export interface AgentStatus {
  agentTypeId?: string;
  workerStatus: WorkerStatus;
  workerDisplayName?: string;
  workerError?: string;
  isLoading: boolean;
}

export function useAgentStatus(beadId: string): AgentStatus {
  const [status, setStatus] = useState<AgentStatus>({
    workerStatus: 'idle',
    isLoading: true,
  });

  useEffect(() => {
    let cancelled = false;

    const fetchStatus = async () => {
      try {
        const response = await fetch(`/api/runtime/worker-status?beadId=${encodeURIComponent(beadId)}`);
        const data = await response.json();
        
        if (!cancelled) {
          setStatus({
            agentTypeId: data.agentTypeId,
            workerStatus: data.workerStatus || 'idle',
            workerDisplayName: data.workerDisplayName,
            workerError: data.workerError,
            isLoading: false,
          });
        }
      } catch (error) {
        if (!cancelled) {
          setStatus({ workerStatus: 'idle', isLoading: false });
        }
      }
    };

    fetchStatus();
    const interval = setInterval(fetchStatus, 5000); // Poll every 5s

    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [beadId]);

  return status;
}

Step 2: Commit

git add src/components/agents/hooks/use-agent-status.ts
git commit -m "feat: implement real status fetching in useAgentStatus"

Task 15: Create Worker Status API

Files:

  • Create: src/app/api/runtime/worker-status/route.ts

Step 1: Create status API

// src/app/api/runtime/worker-status/route.ts
import { NextResponse } from 'next/server';
import { workerSessionManager } from '../../../../lib/worker-session-manager';

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const beadId = searchParams.get('beadId');

    if (!beadId) {
      return NextResponse.json({ ok: false, error: 'beadId required' });
    }

    // Find worker for this bead
    const workers = workerSessionManager.getAllWorkers();
    const worker = workers.find(w => w.beadId === beadId);

    if (!worker) {
      return NextResponse.json({
        ok: true,
        workerStatus: 'idle',
        agentTypeId: null,
      });
    }

    return NextResponse.json({
      ok: true,
      workerStatus: worker.status,
      workerDisplayName: worker.displayName,
      workerError: worker.error,
      agentTypeId: worker.agentTypeId,
    });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ ok: false, error: message });
  }
}

Step 2: Commit

git add src/app/api/runtime/worker-status/route.ts
git commit -m "feat: add worker-status API endpoint"

Task 16: Add agentTypeId to BeadIssue type

Files:

  • Modify: src/lib/types.ts

Step 1: Add agentTypeId field

// In BeadIssue interface, add:
agentTypeId?: string;
agentInstanceId?: string;

Step 2: Commit

git add src/lib/types.ts
git commit -m "feat: add agentTypeId and agentInstanceId to BeadIssue"

Task 17: Test the complete flow

Step 1: Run TypeScript check

cd /home/clawdbot/clawd/repos/beadboard
npx tsc --noEmit

Expected: No errors related to agents components

Step 2: Test in browser

  1. Open app
  2. Go to social view
  3. Click 👤 on a card → picker opens
  4. Select Engineer → badge appears, 🚀 turns blue
  5. Click 🚀 → spawns worker → 🚀 turns green
  6. Hover 🚀 → shows worker name and status

Step 3: Commit test results

git add -A
git commit -m "test: Phase 4 launch-anywhere UX complete"

Success Criteria

  • Social cards show 👤 assign button + colored 🚀 spawn button
  • Graph nodes show same two-icon system
  • Blocked triage modal has agent actions
  • Clicking 👤 opens agent picker
  • Selecting agent updates UI (badge + blue rocket)
  • Clicking 🚀 spawns worker
  • Rocket colors: blue=ready, green=working, red=blocked, gray=done
  • Tooltips show worker status on hover
  • Orchestrator is an option in agent picker

Estimated Effort

6-8 hours


Blocked Items

None identified.


Files Summary

File Action
src/components/agents/hooks/use-agent-status.ts Create
src/components/agents/hooks/use-spawn-agent.ts Create
src/components/agents/hooks/index.ts Create
src/components/agents/agent-picker-popup.tsx Create
src/components/agents/agent-assign-button.tsx Create
src/components/agents/agent-spawn-button.tsx Create
src/components/agents/agent-action-row.tsx Create
src/components/agents/index.ts Create
src/components/social/social-card.tsx Modify
src/components/graph/graph-node-card.tsx Modify
src/components/shared/blocked-triage-modal.tsx Modify
src/app/api/runtime/spawn/route.ts Create
src/app/api/beads/assign-agent/route.ts Create
src/app/api/runtime/worker-status/route.ts Create
src/lib/types.ts Modify