Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
aa3f21c
feat(swarm): add core types and constants
Suhaib3100 Jan 27, 2026
bb705ea
feat(swarm): add SwarmRegistry for tracking active swarms
Suhaib3100 Jan 27, 2026
11beffd
feat(swarm): add SwarmMessagingBus for inter-agent communication
Suhaib3100 Jan 27, 2026
f1d8983
feat(swarm): add WorkerLifecycleManager for worker management
Suhaib3100 Jan 27, 2026
da19dc6
feat(swarm): add TaskPlanner for LLM-based task decomposition
Suhaib3100 Jan 27, 2026
1dabfef
feat(swarm): add ResultAggregator for merging worker results
Suhaib3100 Jan 27, 2026
321a690
feat(swarm): add SwarmCoordinator as main orchestrator
Suhaib3100 Jan 27, 2026
ddf7671
feat(swarm): add HTTP API routes for swarm management
Suhaib3100 Jan 27, 2026
27c70b7
docs(swarm): add design document and update exports
Suhaib3100 Jan 27, 2026
72ba4c8
feat(swarm): add enterprise-grade advanced features
Suhaib3100 Jan 27, 2026
094d3d3
feat(swarm): add Chromium extension + UI components
Suhaib3100 Jan 27, 2026
613af66
docs: update design doc with full implementation status
Suhaib3100 Jan 27, 2026
6cddf84
refactor(swarm): simplify UI to match existing project patterns
Suhaib3100 Jan 27, 2026
2b8c784
feat(swarm): integrate swarm UI into chat
Suhaib3100 Jan 27, 2026
232947e
feat(swarm): enable swarm mode in server config
Suhaib3100 Jan 27, 2026
1640b9e
fix: address PR review comments
Suhaib3100 Jan 27, 2026
fdf69f3
merge: resolve conflicts with origin/main - combine swarm and shutdow…
Suhaib3100 Jan 27, 2026
ccb6bc2
fix: improve swarm startup robustness
Suhaib3100 Jan 27, 2026
5569206
feat(swarm): fix SSE streaming, remove duplicate requests, improve UI
Suhaib3100 Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions apps/agent/components/swarm/SwarmPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SwarmPanel - Compact visualization for active swarm (matches existing UI patterns)
*/
import type { FC } from 'react'
import { cn } from '@/lib/utils'
import { Progress } from '@/components/ui/progress'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import type { SwarmState, SwarmStatus } from './types'
import {
CheckCircle,
XCircle,
Loader2,
Zap,
Square,
} from 'lucide-react'

interface SwarmPanelProps {
swarm: SwarmState
onTerminate?: () => void
className?: string
}

const statusLabels: Record<SwarmStatus, string> = {
idle: 'Idle',
planning: 'Planning...',
spawning: 'Spawning workers...',
executing: 'Executing...',
aggregating: 'Aggregating...',
completed: 'Completed',
failed: 'Failed',
terminated: 'Terminated',
}

/**
* Compact swarm progress panel that fits in the chat UI.
*/
export const SwarmPanel: FC<SwarmPanelProps> = ({
swarm,
onTerminate,
className,
}) => {
const isActive = ['planning', 'spawning', 'executing', 'aggregating'].includes(swarm.status)
const isCompleted = swarm.status === 'completed'
const isFailed = swarm.status === 'failed'

const completedWorkers = swarm.workers.filter(w => w.status === 'completed').length
const totalWorkers = swarm.workers.length

const duration = swarm.completedAt
? Math.round((swarm.completedAt - swarm.startedAt) / 1000)
: Math.round((Date.now() - swarm.startedAt) / 1000)

return (
<div
className={cn(
'flex items-center gap-3 rounded-lg border px-3 py-2',
isActive && 'border-[var(--accent-orange)]/30 bg-[var(--accent-orange)]/5',
isCompleted && 'border-green-500/30 bg-green-500/5',
isFailed && 'border-red-500/30 bg-red-500/5',
!isActive && !isCompleted && !isFailed && 'border-border/50 bg-muted/50',
className,
)}
>
{/* Status icon */}
<div className="flex-shrink-0">
{isActive && (
<Loader2 className="h-4 w-4 animate-spin text-[var(--accent-orange)]" />
)}
{isCompleted && <CheckCircle className="h-4 w-4 text-green-500" />}
{isFailed && <XCircle className="h-4 w-4 text-red-500" />}
{!isActive && !isCompleted && !isFailed && (
<Zap className="h-4 w-4 text-muted-foreground" />
)}
</div>

{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between text-xs">
<span className="font-medium truncate">
{statusLabels[swarm.status]}
</span>
<span className="text-muted-foreground ml-2">
{completedWorkers}/{totalWorkers} • {duration}s
</span>
</div>
<Progress value={swarm.progress} className="mt-1 h-1" />
</div>

{/* Workers dots */}
<TooltipProvider delayDuration={0}>
<div className="flex gap-0.5">
{swarm.workers.slice(0, 8).map((worker) => (
<Tooltip key={worker.id}>
<TooltipTrigger asChild>
<div
className={cn(
'h-2 w-2 rounded-full',
worker.status === 'executing' && 'bg-[var(--accent-orange)] animate-pulse',
worker.status === 'completed' && 'bg-green-500',
worker.status === 'failed' && 'bg-red-500',
worker.status === 'pending' && 'bg-muted-foreground/30',
worker.status === 'spawning' && 'bg-yellow-500',
worker.status === 'ready' && 'bg-blue-500',
)}
/>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
Worker {worker.id.slice(-2)}: {worker.status}
{worker.progress > 0 && ` (${worker.progress}%)`}
</TooltipContent>
</Tooltip>
))}
{swarm.workers.length > 8 && (
<span className="text-xs text-muted-foreground">
+{swarm.workers.length - 8}
</span>
)}
</div>
</TooltipProvider>

{/* Stop button */}
{isActive && onTerminate && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onTerminate}
className="flex-shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
>
<Square className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent side="top">Stop swarm</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)
}
63 changes: 63 additions & 0 deletions apps/agent/components/swarm/SwarmTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SwarmTrigger - Toggle button for swarm mode (matches ChatModeToggle style)
*/

import { Users } from 'lucide-react'
import type { FC } from 'react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'

interface SwarmTriggerProps {
enabled: boolean
onToggle: (enabled: boolean) => void
workerCount?: number
className?: string
}

/**
* Simple toggle button for enabling swarm mode.
* Matches the ChatModeToggle visual style.
*/
export const SwarmTrigger: FC<SwarmTriggerProps> = ({
enabled,
onToggle,
workerCount = 3,
className,
}) => {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onToggle(!enabled)}
className={cn(
'flex items-center gap-1.5 rounded-full border px-2.5 py-1.5 font-medium text-xs transition-all',
enabled
? 'border-[var(--accent-orange)]/30 bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]'
: 'border-border/50 bg-muted text-muted-foreground hover:text-foreground',
className,
)}
>
<Users className="h-3 w-3" />
<span>{enabled ? `Swarm ×${workerCount}` : 'Swarm'}</span>
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[220px]">
{enabled
? `Spawns ${workerCount} parallel AI agents`
: 'Enable parallel AI agents for complex tasks'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
130 changes: 130 additions & 0 deletions apps/agent/components/swarm/SwarmWorkerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SwarmWorkerCard - Small inline worker indicator (matches existing patterns)
*/
import type { FC } from 'react'
import { cn } from '@/lib/utils'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import type { SwarmWorker, WorkerStatus } from './types'
import {
CheckCircle,
XCircle,
Loader2,
Clock,
Zap,
} from 'lucide-react'

interface SwarmWorkerCardProps {
worker: SwarmWorker
compact?: boolean
onFocus?: (workerId: string) => void
onTerminate?: (workerId: string) => void
}

const statusConfig: Record<WorkerStatus, { icon: FC<{ className?: string }>; color: string }> = {
pending: { icon: Clock, color: 'text-muted-foreground' },
spawning: { icon: Loader2, color: 'text-yellow-500' },
ready: { icon: Zap, color: 'text-blue-500' },
executing: { icon: Loader2, color: 'text-[var(--accent-orange)]' },
completed: { icon: CheckCircle, color: 'text-green-500' },
failed: { icon: XCircle, color: 'text-red-500' },
terminated: { icon: XCircle, color: 'text-muted-foreground' },
}

/**
* Small worker indicator that can be shown inline.
*/
export const SwarmWorkerCard: FC<SwarmWorkerCardProps> = ({
worker,
compact = true,
onFocus,
}) => {
const config = statusConfig[worker.status]
const Icon = config.icon
const isActive = worker.status === 'executing' || worker.status === 'spawning'

const duration = worker.completedAt && worker.startedAt
? Math.round((worker.completedAt - worker.startedAt) / 1000)
: worker.startedAt
? Math.round((Date.now() - worker.startedAt) / 1000)
: 0

if (compact) {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onFocus?.(worker.id)}
className={cn(
'flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors',
'hover:bg-muted',
worker.status === 'failed' && 'bg-red-500/10',
worker.status === 'completed' && 'bg-green-500/10',
isActive && 'bg-[var(--accent-orange)]/10',
)}
>
<Icon
className={cn('h-3 w-3', config.color, isActive && 'animate-spin')}
/>
<span className="text-muted-foreground">
#{worker.id.slice(-2)}
</span>
</button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs max-w-[200px]">
<div className="font-medium">{worker.task}</div>
<div className="text-muted-foreground">
{worker.status} • {duration}s
{worker.progress > 0 && ` • ${worker.progress}%`}
</div>
{worker.error && (
<div className="text-red-400 mt-1 truncate">{worker.error}</div>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

// Expanded view
return (
<div
className={cn(
'flex items-center gap-2 rounded border px-2 py-1.5',
'border-border/50 bg-muted/30',
worker.status === 'failed' && 'border-red-500/30 bg-red-500/5',
worker.status === 'completed' && 'border-green-500/30 bg-green-500/5',
isActive && 'border-[var(--accent-orange)]/30 bg-[var(--accent-orange)]/5',
)}
>
<Icon
className={cn('h-4 w-4 flex-shrink-0', config.color, isActive && 'animate-spin')}
/>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate">{worker.task}</div>
<div className="text-xs text-muted-foreground">
{worker.status} • {duration}s
</div>
</div>
{onFocus && (
<button
type="button"
onClick={() => onFocus(worker.id)}
className="text-xs text-muted-foreground hover:text-foreground"
>
Focus
</button>
)}
</div>
)
}
11 changes: 11 additions & 0 deletions apps/agent/components/swarm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Swarm UI Components
*/
export { SwarmPanel } from './SwarmPanel'
export { SwarmWorkerCard } from './SwarmWorkerCard'
export { SwarmTrigger, type SwarmConfig } from './SwarmTrigger'
export * from './types'
Loading