diff --git a/components/solo/solo-board.tsx b/components/solo/solo-board.tsx index 934f984..fb50be8 100644 --- a/components/solo/solo-board.tsx +++ b/components/solo/solo-board.tsx @@ -92,6 +92,7 @@ export function SoloBoard({ const { tasks, initTasks, optimisticMove, rollback, markPending, clearPending } = useSoloStore() const realtimeStatus = useSoloStore((s) => s.realtimeStatus) const showConnectingIndicator = useSoloStore((s) => s.showConnectingIndicator) + const connectedWorkers = useSoloStore((s) => s.connectedWorkers) const [activeDragId, setActiveDragId] = useState(null) const [selectedTask, setSelectedTask] = useState(null) const [sheetOpen, setSheetOpen] = useState(false) @@ -192,6 +193,13 @@ export function SoloBoard({ status={realtimeStatus} showConnectingIndicator={showConnectingIndicator} /> +
+ 0 ? 'bg-status-done' : 'bg-muted-foreground/40' + )} /> + {connectedWorkers > 0 ? 'Agent verbonden' : 'Geen agent'} +
{sprintGoal && (

{sprintGoal}

diff --git a/components/solo/task-detail-dialog.tsx b/components/solo/task-detail-dialog.tsx index 9755865..d9b6db1 100644 --- a/components/solo/task-detail-dialog.tsx +++ b/components/solo/task-detail-dialog.tsx @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { DemoTooltip } from '@/components/shared/demo-tooltip' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { useSoloStore } from '@/stores/solo-store' import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs' import { cn } from '@/lib/utils' @@ -46,6 +47,7 @@ type SaveState = 'idle' | 'saving' | 'saved' function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailContentProps) { const { updatePlan } = useSoloStore() const job = useSoloStore(s => s.claudeJobsByTaskId[task.id]) + const connectedWorkers = useSoloStore(s => s.connectedWorkers) const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '') const [saveState, setSaveState] = useState('idle') const [, startTransition] = useTransition() @@ -166,9 +168,27 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte {!isDemo && !job && ( - + + + + Voer uit + + } + /> + {connectedWorkers === 0 && ( + + Geen Claude Code-sessie verbonden. Start claude lokaal en zeg 'wacht op jobs'. + + )} + + )} {job?.status === 'queued' && ( diff --git a/lib/realtime/use-solo-realtime.ts b/lib/realtime/use-solo-realtime.ts index 1bb8baa..928dd80 100644 --- a/lib/realtime/use-solo-realtime.ts +++ b/lib/realtime/use-solo-realtime.ts @@ -37,6 +37,9 @@ export function useSoloRealtime(productId: string | null) { const handleEvent = useSoloStore.getState().handleRealtimeEvent const handleJobEvent = useSoloStore.getState().handleJobEvent const initJobs = useSoloStore.getState().initJobs + const setWorkers = useSoloStore.getState().setWorkers + const incrementWorkers = useSoloStore.getState().incrementWorkers + const decrementWorkers = useSoloStore.getState().decrementWorkers if (!productId) { // Geen actief product (gebruiker zit niet op /solo) — stream uit @@ -95,12 +98,27 @@ export function useSoloRealtime(productId: string | null) { } }) + source.addEventListener('workers_initial', (e) => { + if (!e.data) return + try { + const { count } = JSON.parse(e.data) as { count: number } + setWorkers(count) + } catch { + // ignore malformed payload + } + }) + source.onmessage = (e) => { if (!e.data) return try { - const raw = JSON.parse(e.data) as RealtimeEvent | ClaudeJobEvent - if ('type' in raw && (raw.type === 'claude_job_enqueued' || raw.type === 'claude_job_status')) { - handleJobEvent(raw) + const raw = JSON.parse(e.data) as RealtimeEvent | ClaudeJobEvent | { type: string } + if ('type' in raw) { + if (raw.type === 'claude_job_enqueued' || raw.type === 'claude_job_status') { + handleJobEvent(raw as ClaudeJobEvent) + return + } + if (raw.type === 'worker_connected') { incrementWorkers(); return } + if (raw.type === 'worker_disconnected') { decrementWorkers(); return } return } const payload = raw as RealtimeEvent diff --git a/stores/solo-store.ts b/stores/solo-store.ts index c26f547..46cf569 100644 --- a/stores/solo-store.ts +++ b/stores/solo-store.ts @@ -57,6 +57,7 @@ interface SoloStore { showConnectingIndicator: boolean claudeJobsByTaskId: Record + connectedWorkers: number initTasks: (tasks: SoloTask[]) => void optimisticMove: (taskId: string, toStatus: TaskStatus) => TaskStatus | null @@ -71,6 +72,10 @@ interface SoloStore { initJobs: (jobs: JobState[]) => void handleJobEvent: (event: ClaudeJobEvent) => void + setWorkers: (count: number) => void + incrementWorkers: () => void + decrementWorkers: () => void + handleRealtimeEvent: (event: RealtimeEvent) => void } @@ -80,6 +85,7 @@ export const useSoloStore = create((set, get) => ({ realtimeStatus: 'connecting', showConnectingIndicator: false, claudeJobsByTaskId: {}, + connectedWorkers: 0, initTasks: (tasks) => set({ tasks: Object.fromEntries(tasks.map(t => [t.id, t])) }), @@ -124,6 +130,10 @@ export const useSoloStore = create((set, get) => ({ initJobs: (jobs) => set({ claudeJobsByTaskId: Object.fromEntries(jobs.map(j => [j.task_id, j])) }), + setWorkers: (count) => set({ connectedWorkers: Math.max(0, count) }), + incrementWorkers: () => set(s => ({ connectedWorkers: s.connectedWorkers + 1 })), + decrementWorkers: () => set(s => ({ connectedWorkers: Math.max(0, s.connectedWorkers - 1) })), + handleJobEvent: (event) => { const { job_id, task_id } = event if (event.type === 'claude_job_enqueued') {