feat: Show detailing status in initiative overview and phase sidebar

Add 'detailing' activity state derived from active detail agents
(mode=detail, status running/waiting_for_input). Initiative cards show
pulsing "Detailing" indicator. Phase sidebar items show spinner during
active detailing and "Review changes" when the agent finishes.
This commit is contained in:
Lukas May
2026-03-03 13:08:05 +01:00
parent 96386e1c3d
commit 411700d37d
13 changed files with 151 additions and 17 deletions

View File

@@ -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' };
}

View File

@@ -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),
}));
}),

View File

@@ -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);