diff --git a/apps/server/trpc/routers/initiative-activity.ts b/apps/server/trpc/routers/initiative-activity.ts index bfed6b7..50222ee 100644 --- a/apps/server/trpc/routers/initiative-activity.ts +++ b/apps/server/trpc/routers/initiative-activity.ts @@ -5,7 +5,17 @@ import type { Initiative, Phase } from '../../db/schema.js'; import type { InitiativeActivity, InitiativeActivityState } from '@codewalk-district/shared'; -export function deriveInitiativeActivity(initiative: Initiative, phases: Phase[]): InitiativeActivity { +export interface ActiveDetailAgent { + initiativeId: string; + mode: string; + status: string; +} + +export function deriveInitiativeActivity( + initiative: Initiative, + phases: Phase[], + activeDetailAgents?: ActiveDetailAgent[], +): InitiativeActivity { const phasesTotal = phases.length; const phasesCompleted = phases.filter(p => p.status === 'completed').length; const base = { phasesTotal, phasesCompleted }; @@ -43,5 +53,15 @@ export function deriveInitiativeActivity(initiative: Initiative, phases: Phase[] return { ...base, state: 'ready', activePhase: { id: approved.id, name: approved.name } }; } + // Check for active detail agents (detailing trumps planning) + const detailing = activeDetailAgents?.some( + a => a.initiativeId === initiative.id + && a.mode === 'detail' + && (a.status === 'running' || a.status === 'waiting_for_input'), + ); + if (detailing) { + return { ...base, state: 'detailing' }; + } + return { ...base, state: 'planning' }; } diff --git a/apps/server/trpc/routers/initiative.ts b/apps/server/trpc/routers/initiative.ts index 5c9ed1e..7b3ce4d 100644 --- a/apps/server/trpc/routers/initiative.ts +++ b/apps/server/trpc/routers/initiative.ts @@ -68,17 +68,27 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) { ? await repo.findByStatus(input.status) : await repo.findAll(); + // Fetch active detail agents once for all initiatives + const allAgents = ctx.agentManager ? await ctx.agentManager.list() : []; + const activeDetailAgents = allAgents + .filter(a => + a.mode === 'detail' + && (a.status === 'running' || a.status === 'waiting_for_input') + && !a.userDismissedAt, + ) + .map(a => ({ initiativeId: a.initiativeId ?? '', mode: a.mode ?? '', status: a.status })); + if (ctx.phaseRepository) { const phaseRepo = ctx.phaseRepository; return Promise.all(initiatives.map(async (init) => { const phases = await phaseRepo.findByInitiativeId(init.id); - return { ...init, activity: deriveInitiativeActivity(init, phases) }; + return { ...init, activity: deriveInitiativeActivity(init, phases, activeDetailAgents) }; })); } return initiatives.map(init => ({ ...init, - activity: deriveInitiativeActivity(init, []), + activity: deriveInitiativeActivity(init, [], activeDetailAgents), })); }), diff --git a/apps/server/trpc/routers/phase-dispatch.ts b/apps/server/trpc/routers/phase-dispatch.ts index 8cf95f0..3ccb0c8 100644 --- a/apps/server/trpc/routers/phase-dispatch.ts +++ b/apps/server/trpc/routers/phase-dispatch.ts @@ -6,7 +6,7 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import type { Task } from '../../db/schema.js'; import type { ProcedureBuilder } from '../trpc.js'; -import { requirePhaseDispatchManager, requireTaskRepository } from './_helpers.js'; +import { requirePhaseDispatchManager, requirePhaseRepository, requireTaskRepository } from './_helpers.js'; export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { return { @@ -18,6 +18,22 @@ export function phaseDispatchProcedures(publicProcedure: ProcedureBuilder) { return { success: true }; }), + queueAllPhases: publicProcedure + .input(z.object({ initiativeId: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const phaseDispatchManager = requirePhaseDispatchManager(ctx); + const phaseRepo = requirePhaseRepository(ctx); + const phases = await phaseRepo.findByInitiativeId(input.initiativeId); + let queued = 0; + for (const phase of phases) { + if (phase.status === 'approved') { + await phaseDispatchManager.queuePhase(phase.id); + queued++; + } + } + return { success: true, queued }; + }), + dispatchNextPhase: publicProcedure .mutation(async ({ ctx }) => { const phaseDispatchManager = requirePhaseDispatchManager(ctx); diff --git a/apps/web/src/components/ExecutionTab.tsx b/apps/web/src/components/ExecutionTab.tsx index f5352e3..6d399d3 100644 --- a/apps/web/src/components/ExecutionTab.tsx +++ b/apps/web/src/components/ExecutionTab.tsx @@ -232,6 +232,7 @@ export function ExecutionTab({ dependencies={depNamesByPhase.get(phase.id) ?? []} isSelected={phase.id === activePhaseId} onClick={() => setSelectedPhaseId(phase.id)} + detailAgent={detailAgentByPhase.get(phase.id) ?? null} /> ))} {isAddingPhase && ( diff --git a/apps/web/src/components/InitiativeCard.tsx b/apps/web/src/components/InitiativeCard.tsx index 5674f78..aa896fd 100644 --- a/apps/web/src/components/InitiativeCard.tsx +++ b/apps/web/src/components/InitiativeCard.tsx @@ -33,6 +33,7 @@ function activityVisual(state: string): { label: string; variant: StatusVariant; switch (state) { case "executing": return { label: "Executing", variant: "active", pulse: true }; case "pending_review": return { label: "Pending Review", variant: "warning", pulse: true }; + case "detailing": return { label: "Detailing", variant: "active", pulse: true }; case "ready": return { label: "Ready", variant: "active", pulse: false }; case "blocked": return { label: "Blocked", variant: "error", pulse: false }; case "complete": return { label: "Complete", variant: "success", pulse: false }; diff --git a/apps/web/src/components/execution/PhaseSidebarItem.tsx b/apps/web/src/components/execution/PhaseSidebarItem.tsx index 6f4ea1d..2aa7567 100644 --- a/apps/web/src/components/execution/PhaseSidebarItem.tsx +++ b/apps/web/src/components/execution/PhaseSidebarItem.tsx @@ -1,3 +1,4 @@ +import { Loader2 } from "lucide-react"; import { StatusBadge } from "@/components/StatusBadge"; import { cn } from "@/lib/utils"; @@ -12,6 +13,7 @@ interface PhaseSidebarItemProps { dependencies: string[]; isSelected: boolean; onClick: () => void; + detailAgent?: { status: string } | null; } export function PhaseSidebarItem({ @@ -21,7 +23,35 @@ export function PhaseSidebarItem({ dependencies, isSelected, onClick, + detailAgent, }: PhaseSidebarItemProps) { + const isDetailing = + detailAgent?.status === "running" || + detailAgent?.status === "waiting_for_input"; + const detailDone = detailAgent?.status === "idle"; + + function renderTaskStatus() { + if (isDetailing) { + return ( + + + Detailing… + + ); + } + if (detailDone && taskCount.total === 0) { + return Review changes; + } + if (taskCount.total === 0) { + return Needs decomposition; + } + return ( + + {taskCount.complete}/{taskCount.total} tasks + + ); + } + return ( + + )} + + + ); } diff --git a/docs/frontend.md b/docs/frontend.md index eb9d9c7..3c9873e 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -180,4 +180,4 @@ Configured in `src/lib/trpc.ts`. Uses `@trpc/react-query` with TanStack Query fo `listInitiatives` returns an `activity` field on each initiative, computed server-side from phase statuses via `deriveInitiativeActivity()` in `apps/server/trpc/routers/initiative-activity.ts`. This eliminates per-card N+1 `listPhases` queries. -Activity states (priority order): `pending_review` > `executing` > `blocked` > `complete` > `ready` > `planning` > `idle` > `archived`. Each state maps to a `StatusVariant` + pulse animation in `InitiativeCard`'s `activityVisual()` function. +Activity states (priority order): `pending_review` > `executing` > `blocked` > `complete` > `ready` > `detailing` > `planning` > `idle` > `archived`. Each state maps to a `StatusVariant` + pulse animation in `InitiativeCard`'s `activityVisual()` function. The `detailing` state is derived from active detail agents (mode='detail', status running/waiting_for_input) and shows a pulsing indigo dot. `PhaseSidebarItem` also shows a spinner when a detail agent is active for its phase. diff --git a/docs/server-api.md b/docs/server-api.md index fabb816..33bef1b 100644 --- a/docs/server-api.md +++ b/docs/server-api.md @@ -111,6 +111,7 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE | Procedure | Type | Description | |-----------|------|-------------| | queuePhase | mutation | Queue approved phase | +| queueAllPhases | mutation | Queue all approved phases for initiative | | dispatchNextPhase | mutation | Start next ready phase | | getPhaseQueueState | query | Queue state | | createChildTasks | mutation | Create tasks from detail parent | diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index edbf246..022ba0a 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -6,6 +6,7 @@ export type ExecutionMode = 'yolo' | 'review_per_phase'; export type InitiativeActivityState = | 'idle' // Active but no phases | 'planning' // All phases pending (no work started) + | 'detailing' // Detail agent actively decomposing phases into tasks | 'ready' // Phases approved, waiting to execute | 'executing' // At least one phase in_progress | 'pending_review' // At least one phase pending_review