feat: Show resolving_conflict activity state on initiative cards

Add 'resolving_conflict' to InitiativeActivityState and detect active
conflict agents (name starts with conflict-) in deriveInitiativeActivity.
Conflict resolution takes priority over pending_review since the agent
is actively working.

- Add resolving_conflict to shared types and activity derivation
- Include conflict agents in listInitiatives agent filter (name + mode)
- Map resolving_conflict to urgent variant with pulse in InitiativeCard
- Add merge: prefix to INITIATIVE_LIST_RULES for merge event routing
- Add spawnConflictResolutionAgent to INVALIDATION_MAP
- Add getActiveConflictAgent to detail page agent: SSE invalidation
This commit is contained in:
Lukas May
2026-03-06 13:32:37 +01:00
parent 05efa9c08e
commit e3246baf51
8 changed files with 42 additions and 20 deletions

View File

@@ -9,6 +9,7 @@ export interface ActiveArchitectAgent {
initiativeId: string;
mode: string;
status: string;
name?: string;
}
const MODE_TO_STATE: Record<string, InitiativeActivityState> = {
@@ -30,6 +31,18 @@ export function deriveInitiativeActivity(
if (initiative.status === 'archived') {
return { ...base, state: 'archived' };
}
// Check for active conflict resolution agent — takes priority over pending_review
// because the agent is actively working to resolve merge conflicts
const conflictAgent = activeArchitectAgents?.find(
a => a.initiativeId === initiative.id
&& a.name?.startsWith('conflict-')
&& (a.status === 'running' || a.status === 'waiting_for_input'),
);
if (conflictAgent) {
return { ...base, state: 'resolving_conflict' };
}
if (initiative.status === 'pending_review') {
return { ...base, state: 'pending_review' };
}
@@ -41,6 +54,7 @@ export function deriveInitiativeActivity(
// so architect agents (discuss/plan/detail/refine) surface activity
const activeAgent = activeArchitectAgents?.find(
a => a.initiativeId === initiative.id
&& !a.name?.startsWith('conflict-')
&& (a.status === 'running' || a.status === 'waiting_for_input'),
);
if (activeAgent) {

View File

@@ -129,16 +129,16 @@ export function initiativeProcedures(publicProcedure: ProcedureBuilder) {
: await repo.findAll();
}
// Fetch active architect agents once for all initiatives
// Fetch active agents once for all initiatives (architect + conflict)
const ARCHITECT_MODES = ['discuss', 'plan', 'detail', 'refine'];
const allAgents = ctx.agentManager ? await ctx.agentManager.list() : [];
const activeArchitectAgents = allAgents
.filter(a =>
ARCHITECT_MODES.includes(a.mode ?? '')
(ARCHITECT_MODES.includes(a.mode ?? '') || a.name?.startsWith('conflict-'))
&& (a.status === 'running' || a.status === 'waiting_for_input')
&& !a.userDismissedAt,
)
.map(a => ({ initiativeId: a.initiativeId ?? '', mode: a.mode ?? '', status: a.status }));
.map(a => ({ initiativeId: a.initiativeId ?? '', mode: a.mode ?? '', status: a.status, name: a.name }));
// Batch-fetch projects for all initiatives
const projectRepo = ctx.projectRepository;

View File

@@ -32,11 +32,12 @@ export interface SerializedInitiative {
function activityVisual(state: string): { label: string; variant: StatusVariant; pulse: boolean } {
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 "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 "resolving_conflict": return { label: "Resolving Conflict", variant: "urgent", 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 };

View File

@@ -45,6 +45,10 @@ export function mapEntityStatus(rawStatus: string): StatusVariant {
case "medium":
return "warning";
// Urgent / conflict resolution
case "resolving_conflict":
return "urgent";
// Error / failed
case "crashed":
case "blocked":

View File

@@ -24,6 +24,7 @@ export const INITIATIVE_LIST_RULES: LiveUpdateRule[] = [
{ prefix: 'task:', invalidate: ['listInitiatives'] },
{ prefix: 'phase:', invalidate: ['listInitiatives'] },
{ prefix: 'agent:', invalidate: ['listInitiatives'] },
{ prefix: 'merge:', invalidate: ['listInitiatives'] },
];
export function useLiveUpdates(rules: LiveUpdateRule[]) {

View File

@@ -44,6 +44,7 @@ const INVALIDATION_MAP: Partial<Record<MutationName, QueryName[]>> = {
spawnArchitectDiscuss: ["listAgents"],
spawnArchitectPlan: ["listAgents"],
spawnArchitectDetail: ["listAgents", "listInitiativeTasks"],
spawnConflictResolutionAgent: ["listAgents", "listInitiatives", "getInitiative"],
// --- Initiatives ---
createInitiative: ["listInitiatives"],

View File

@@ -198,4 +198,4 @@ Components: `ChatSlideOver`, `ChatBubble`, `ChatInput`, `ChangeSetInline` in `sr
`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): 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.
Activity states (priority order): conflict agent > `archived` > active architect agents > `pending_review` > `executing` > `blocked` > `complete` > `ready` > `planning` > `idle`. Each state maps to a `StatusVariant` + pulse animation in `InitiativeCard`'s `activityVisual()` function. Active conflict agents (name starts with `conflict-`) are checked first — returning `resolving_conflict` (urgent variant, pulsing). Active architect agents (modes: discuss, plan, detail, refine) are checked next — 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.

View File

@@ -4,17 +4,18 @@ export type { PendingQuestions, QuestionItem } from '../../../apps/server/agent/
export type ExecutionMode = 'yolo' | 'review_per_phase';
export type InitiativeActivityState =
| 'idle' // Active but no phases and no agents
| 'discussing' // Discuss agent actively scoping the initiative
| 'planning' // All phases pending (no work started)
| '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
| 'blocked' // At least one phase blocked (none in_progress/pending_review)
| 'complete' // All phases completed
| 'archived'; // Initiative archived
| 'idle' // Active but no phases and no agents
| 'discussing' // Discuss agent actively scoping the initiative
| 'planning' // All phases pending (no work started)
| 'detailing' // Detail/plan agent actively decomposing phases into tasks
| 'refining' // Refine agent actively working on content
| 'resolving_conflict' // Conflict resolution agent actively fixing merge conflicts
| 'ready' // Phases approved, waiting to execute
| 'executing' // At least one phase in_progress
| 'pending_review' // At least one phase pending_review
| 'blocked' // At least one phase blocked (none in_progress/pending_review)
| 'complete' // All phases completed
| 'archived'; // Initiative archived
export interface InitiativeActivity {
state: InitiativeActivityState;