From 3d04cb2081d318897fdcc1d7bf4126fe67dcaba6 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Tue, 3 Mar 2026 14:12:20 +0100 Subject: [PATCH] fix: Surface active architect agents in initiative activity state Auto-spawned discuss/plan/refine agents were invisible because: 1. listInitiatives only filtered for mode='detail' agents 2. deriveInitiativeActivity returned 'idle' for zero phases before checking for active agents Broadened agent filter to all architect modes (discuss, plan, detail, refine), moved active agent check before zero-phases early return, and added 'discussing'/'refining' activity states with pulsing indicators. --- .../trpc/routers/initiative-activity.ts | 33 ++++++++++++------- apps/server/trpc/routers/initiative.ts | 11 ++++--- apps/web/src/components/InitiativeCard.tsx | 2 ++ docs/frontend.md | 2 +- packages/shared/src/types.ts | 6 ++-- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/apps/server/trpc/routers/initiative-activity.ts b/apps/server/trpc/routers/initiative-activity.ts index 50222ee..b9a11e9 100644 --- a/apps/server/trpc/routers/initiative-activity.ts +++ b/apps/server/trpc/routers/initiative-activity.ts @@ -5,16 +5,23 @@ import type { Initiative, Phase } from '../../db/schema.js'; import type { InitiativeActivity, InitiativeActivityState } from '@codewalk-district/shared'; -export interface ActiveDetailAgent { +export interface ActiveArchitectAgent { initiativeId: string; mode: string; status: string; } +const MODE_TO_STATE: Record = { + discuss: 'discussing', + plan: 'detailing', + detail: 'detailing', + refine: 'refining', +}; + export function deriveInitiativeActivity( initiative: Initiative, phases: Phase[], - activeDetailAgents?: ActiveDetailAgent[], + activeArchitectAgents?: ActiveArchitectAgent[], ): InitiativeActivity { const phasesTotal = phases.length; const phasesCompleted = phases.filter(p => p.status === 'completed').length; @@ -26,6 +33,18 @@ export function deriveInitiativeActivity( if (initiative.status === 'completed') { return { ...base, state: 'complete' }; } + + // Check for active architect agents BEFORE zero-phases check + // so auto-spawned discuss/plan/refine agents surface activity + const activeAgent = activeArchitectAgents?.find( + a => a.initiativeId === initiative.id + && (a.status === 'running' || a.status === 'waiting_for_input'), + ); + if (activeAgent) { + const state = MODE_TO_STATE[activeAgent.mode] ?? 'detailing'; + return { ...base, state }; + } + if (phasesTotal === 0) { return { ...base, state: 'idle' }; } @@ -53,15 +72,5 @@ export function deriveInitiativeActivity( 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 05583c4..c7c121f 100644 --- a/apps/server/trpc/routers/initiative.ts +++ b/apps/server/trpc/routers/initiative.ts @@ -98,11 +98,12 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) { ? await repo.findByStatus(input.status) : await repo.findAll(); - // Fetch active detail agents once for all initiatives + // Fetch active architect agents once for all initiatives + const ARCHITECT_MODES = ['discuss', 'plan', 'detail', 'refine']; const allAgents = ctx.agentManager ? await ctx.agentManager.list() : []; - const activeDetailAgents = allAgents + const activeArchitectAgents = allAgents .filter(a => - a.mode === 'detail' + ARCHITECT_MODES.includes(a.mode ?? '') && (a.status === 'running' || a.status === 'waiting_for_input') && !a.userDismissedAt, ) @@ -112,13 +113,13 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) { const phaseRepo = ctx.phaseRepository; return Promise.all(initiatives.map(async (init) => { const phases = await phaseRepo.findByInitiativeId(init.id); - return { ...init, activity: deriveInitiativeActivity(init, phases, activeDetailAgents) }; + return { ...init, activity: deriveInitiativeActivity(init, phases, activeArchitectAgents) }; })); } return initiatives.map(init => ({ ...init, - activity: deriveInitiativeActivity(init, [], activeDetailAgents), + activity: deriveInitiativeActivity(init, [], activeArchitectAgents), })); }), diff --git a/apps/web/src/components/InitiativeCard.tsx b/apps/web/src/components/InitiativeCard.tsx index aa896fd..42ef07b 100644 --- a/apps/web/src/components/InitiativeCard.tsx +++ b/apps/web/src/components/InitiativeCard.tsx @@ -33,7 +33,9 @@ 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 "discussing": return { label: "Discussing", variant: "active", pulse: true }; case "detailing": return { label: "Detailing", variant: "active", pulse: true }; + case "refining": return { label: "Refining", 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/docs/frontend.md b/docs/frontend.md index 4a3330b..dccd1d3 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` > `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. +Activity states (priority order): active architect agents > `pending_review` > `executing` > `blocked` > `complete` > `ready` > `planning` > `idle` > `archived`. Each state maps to a `StatusVariant` + pulse animation in `InitiativeCard`'s `activityVisual()` function. Active architect agents (modes: discuss, plan, detail, refine) are checked first — mapping to `discussing`, `detailing`, `detailing`, `refining` states respectively — so auto-spawned agents surface activity even when no phases exist yet. `PhaseSidebarItem` also shows a spinner when a detail agent is active for its phase. diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 022ba0a..db8ef20 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -4,9 +4,11 @@ export type { PendingQuestions, QuestionItem } from '../../../apps/server/agent/ export type ExecutionMode = 'yolo' | 'review_per_phase'; export type InitiativeActivityState = - | 'idle' // Active but no phases + | 'idle' // Active but no phases and no agents + | 'discussing' // Discuss agent actively scoping the initiative | 'planning' // All phases pending (no work started) - | 'detailing' // Detail agent actively decomposing phases into tasks + | 'detailing' // Detail/plan agent actively decomposing phases into tasks + | 'refining' // Refine agent actively working on content | 'ready' // Phases approved, waiting to execute | 'executing' // At least one phase in_progress | 'pending_review' // At least one phase pending_review