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:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user