feat(ST-1111.10d): show worker presence indicator and gate 'Voer uit' on connected workers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5c226fb042
commit
95b5dd8430
4 changed files with 62 additions and 6 deletions
|
|
@ -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<string | null>(null)
|
||||
const [selectedTask, setSelectedTask] = useState<SoloTask | null>(null)
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
|
|
@ -192,6 +193,13 @@ export function SoloBoard({
|
|||
status={realtimeStatus}
|
||||
showConnectingIndicator={showConnectingIndicator}
|
||||
/>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground ml-1">
|
||||
<span className={cn(
|
||||
'size-2 rounded-full',
|
||||
connectedWorkers > 0 ? 'bg-status-done' : 'bg-muted-foreground/40'
|
||||
)} />
|
||||
{connectedWorkers > 0 ? 'Agent verbonden' : 'Geen agent'}
|
||||
</div>
|
||||
</div>
|
||||
{sprintGoal && (
|
||||
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">{sprintGoal}</p>
|
||||
|
|
|
|||
|
|
@ -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<SaveState>('idle')
|
||||
const [, startTransition] = useTransition()
|
||||
|
|
@ -166,9 +168,27 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
</Link>
|
||||
|
||||
{!isDemo && !job && (
|
||||
<Button size="sm" className="h-7 text-xs" onClick={handleEnqueue} disabled={jobPending}>
|
||||
Voer uit
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={handleEnqueue}
|
||||
disabled={jobPending || connectedWorkers === 0}
|
||||
>
|
||||
Voer uit
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{connectedWorkers === 0 && (
|
||||
<TooltipContent side="top" className="max-w-xs text-xs">
|
||||
Geen Claude Code-sessie verbonden. Start claude lokaal en zeg 'wacht op jobs'.
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{job?.status === 'queued' && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface SoloStore {
|
|||
showConnectingIndicator: boolean
|
||||
|
||||
claudeJobsByTaskId: Record<string, JobState>
|
||||
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<SoloStore>((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<SoloStore>((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') {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue