From c0f220582cd61f0d88e42dfbc29d55b3be1e3b19 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 8 Feb 2026 16:18:13 +0000 Subject: Fix directive deletion and stop local only on contracts --- .../src/components/directives/DirectiveDetail.tsx | 188 +++----------- .../src/components/directives/StepDiagram.tsx | 281 ++++++++++++++------- makima/src/db/repository.rs | 21 ++ makima/src/orchestration/directive.rs | 9 +- 4 files changed, 264 insertions(+), 235 deletions(-) (limited to 'makima') diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index 06d8ba2..6bdf5aa 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -3,8 +3,6 @@ import { useNavigate } from "react-router"; import type { DirectiveWithChains, DirectiveStatus, - ChainWithSteps, - ChainStep, ContractPhase, } from "../../lib/api"; import { getDirective } from "../../lib/api"; @@ -32,122 +30,6 @@ const statusColors: Record = { failed: "text-red-400", }; -const stepStatusColors: Record = { - pending: "text-[#888]", - running: "text-yellow-400", - passed: "text-green-400", - failed: "text-red-400", -}; - -const stepStatusIcons: Record = { - pending: "\u25CB", // ○ - running: "\u25D4", // ◔ - passed: "\u25CF", // ● - failed: "\u2715", // ✕ -}; - -function StepRow({ step }: { step: ChainStep }) { - const navigate = useNavigate(); - const color = stepStatusColors[step.status] || "text-[#888]"; - const icon = stepStatusIcons[step.status] || "\u25CB"; - const summary = step.contractSummary; - - return ( -
- {icon} -
-
- - {step.name} - - - {step.status} - -
- {summary && ( -
- - - {summary.tasksDone}/{summary.taskCount} tasks - - {step.contractId && ( - - )} -
- )} - {!summary && step.contractId && ( - - )} - {step.description && ( -

- {step.description} -

- )} -
-
- ); -} - -function ChainCard({ chainWithSteps }: { chainWithSteps: ChainWithSteps }) { - const chain = chainWithSteps; - const steps = chainWithSteps.steps || []; - - return ( -
-
-
- - {chain.name} - - - gen {chain.generation} · {chain.status} - -
- {chain.description && ( -

- {chain.description} -

- )} -
- - {chain.completedSteps}/{chain.totalSteps} steps - - {chain.failedSteps > 0 && ( - {chain.failedSteps} failed - )} - {chain.currentConfidence != null && ( - - confidence: {(chain.currentConfidence * 100).toFixed(0)}% - - )} -
-
- {steps.length > 0 && ( -
- {steps.map((step) => ( - - ))} -
- )} -
- ); -} - function JsonSection({ label, data, @@ -484,39 +366,53 @@ export function DirectiveDetail({ {activeTab === "chain" && (
- {/* Step diagram */} - {directive.chains.length > 0 && ( -
-

- Step Dependencies -

- c.steps)} - /> -
- )} - - {/* Chain cards */} -
-

- Chains ({directive.chains.length}) -

- {directive.chains.length === 0 ? ( -

+ {directive.chains.length === 0 ? ( +

+
+ {directive.status === "planning" ? "\u2699" : "\u25CB"} +
+

{directive.status === "planning" - ? "Planning in progress... chains will appear when the planner completes." + ? "Planning in progress... the chain will appear when the planner submits a plan." : directive.status === "draft" ? "No chains yet. Start the directive to begin planning." : "No chains created for this directive."}

- ) : ( -
- {directive.chains.map((cws) => ( - - ))} -
- )} -
+
+ ) : ( + <> + {/* Chain metadata header */} + {directive.chains.map((chain) => ( +
+
+ + {chain.name} + + + gen {chain.generation} + + + {chain.status} + + + {chain.completedSteps}/{chain.totalSteps} steps + {chain.failedSteps > 0 && ( + + ({chain.failedSteps} failed) + + )} + +
+ +
+ ))} + + )}
)} diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx index 5c65ae1..91a3438 100644 --- a/makima/frontend/src/components/directives/StepDiagram.tsx +++ b/makima/frontend/src/components/directives/StepDiagram.tsx @@ -6,23 +6,49 @@ interface StepDiagramProps { steps: ChainStep[]; } -const statusBorderColors: Record = { - pending: "border-[#555]", - running: "border-yellow-400", - passed: "border-green-400", - failed: "border-red-400", +const statusColors: Record = { + pending: { + border: "border-[#444]", + dot: "bg-[#555]", + bg: "bg-[rgba(40,40,50,0.6)]", + glow: "", + }, + running: { + border: "border-yellow-400/60", + dot: "bg-yellow-400", + bg: "bg-[rgba(80,70,20,0.3)]", + glow: "shadow-[0_0_8px_rgba(250,204,21,0.15)]", + }, + evaluating: { + border: "border-blue-400/60", + dot: "bg-blue-400", + bg: "bg-[rgba(20,50,80,0.3)]", + glow: "shadow-[0_0_8px_rgba(96,165,250,0.15)]", + }, + passed: { + border: "border-green-400/60", + dot: "bg-green-400", + bg: "bg-[rgba(20,60,30,0.3)]", + glow: "", + }, + failed: { + border: "border-red-400/60", + dot: "bg-red-400", + bg: "bg-[rgba(60,20,20,0.3)]", + glow: "", + }, }; -const statusDotColors: Record = { - pending: "bg-[#555]", - running: "bg-yellow-400", - passed: "bg-green-400", - failed: "bg-red-400", +const statusLabels: Record = { + pending: "Pending", + running: "Running", + evaluating: "Evaluating", + passed: "Passed", + failed: "Failed", }; /** - * Assign depth to each step via topological sort. - * Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc. + * Assign depth to each step via topological sort based on dependsOn UUIDs. */ function assignDepths(steps: ChainStep[]): Map { const depths = new Map(); @@ -50,9 +76,108 @@ function assignDepths(steps: ChainStep[]): Map { return depths; } -export function StepDiagram({ steps }: StepDiagramProps) { +function StepCard({ step }: { step: ChainStep }) { const navigate = useNavigate(); + const colors = statusColors[step.status] || statusColors.pending; + const summary = step.contractSummary; + const hasContract = !!step.contractId; + + return ( +
{ + if (hasContract) navigate(`/contracts/${step.contractId}`); + }} + title={hasContract ? "View contract" : undefined} + > + {/* Status header */} +
+
+
+ + {step.name} + +
+ + {statusLabels[step.status] || step.status} + +
+ + {/* Description */} + {step.description && ( +

+ {step.description} +

+ )} + + {/* Contract progress */} + {summary && ( +
+
+ +
+
+ + {summary.tasksDone}/{summary.taskCount} tasks + + {summary.tasksRunning > 0 && ( + + {summary.tasksRunning} running + + )} + {summary.tasksFailed > 0 && ( + + {summary.tasksFailed} failed + + )} +
+
+ )} + {/* Contract link arrow */} + {hasContract && !summary && ( +
+ + view contract → + +
+ )} +
+ ); +} + +/** Vertical connector between levels */ +function LevelConnector({ count }: { count: number }) { + return ( +
+
+ {Array.from({ length: count }).map((_, i) => ( +
+
+
+
+
+ ))} +
+
+ ); +} + +export function StepDiagram({ steps }: StepDiagramProps) { if (steps.length === 0) { return (

No steps to display.

@@ -62,7 +187,7 @@ export function StepDiagram({ steps }: StepDiagramProps) { const depths = assignDepths(steps); const maxDepth = Math.max(...Array.from(depths.values())); - // Group steps by depth + // Group steps by depth level const levels: ChainStep[][] = []; for (let d = 0; d <= maxDepth; d++) { levels.push( @@ -72,81 +197,65 @@ export function StepDiagram({ steps }: StepDiagramProps) { ); } - // Build position map for connectors - const stepPositions = new Map(); - levels.forEach((level, li) => { - level.forEach((step, si) => { - stepPositions.set(step.id, { level: li, index: si }); - }); - }); + // Compute overall progress + const passedCount = steps.filter(s => s.status === "passed").length; + const failedCount = steps.filter(s => s.status === "failed").length; + const runningCount = steps.filter(s => s.status === "running" || s.status === "evaluating").length; return ( -
- {levels.map((level, li) => ( -
- {li > 0 && ( -
-
-
- )} - {level.map((step) => { - const borderColor = - statusBorderColors[step.status] || "border-[#555]"; - const dotColor = statusDotColors[step.status] || "bg-[#555]"; - const summary = step.contractSummary; - const hasContract = !!step.contractId; - - return ( -
{ - if (hasContract) navigate(`/contracts/${step.contractId}`); - }} - title={hasContract ? "View contract" : undefined} - > -
-
- - {step.name} - - {hasContract && ( - - → - - )} -
- {summary && ( - <> -
- -
-
- {summary.tasksDone}/{summary.taskCount} tasks - {summary.tasksRunning > 0 && ( - - {summary.tasksRunning} running - - )} - {summary.tasksFailed > 0 && ( - - {summary.tasksFailed} failed - - )} -
- - )} -
- ); - })} +
+ {/* Progress summary */} +
+ + {levels.length} level{levels.length !== 1 ? "s" : ""} · {steps.length} step{steps.length !== 1 ? "s" : ""} + + {passedCount > 0 && ( + {passedCount} passed + )} + {runningCount > 0 && ( + {runningCount} active + )} + {failedCount > 0 && ( + {failedCount} failed + )} +
+
- ))} + + {Math.round((passedCount / steps.length) * 100)}% + +
+ + {/* Chain flow */} +
+ {levels.map((level, li) => ( +
+ {/* Level label */} + {levels.length > 1 && ( +
+ + {li === 0 ? "Start" : li === maxDepth ? "Final" : `Level ${li}`} + +
+ )} + + {/* Steps at this level */} +
+ {level.map((step) => ( + + ))} +
+ + {/* Connector to next level */} + {li < maxDepth && ( + + )} +
+ ))} +
); } diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 63493b9..6b3f15f 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5130,11 +5130,32 @@ pub async fn update_directive_for_owner( } /// Delete a directive by ID, scoped to owner. +/// Also deletes all contracts (and their cascaded tasks/files) associated with this directive. pub async fn delete_directive_for_owner( pool: &PgPool, id: Uuid, owner_id: Uuid, ) -> Result { + // First verify the directive exists and belongs to the owner + let directive = get_directive_for_owner(pool, id, owner_id).await?; + let Some(_directive) = directive else { + return Ok(false); + }; + + // Delete all contracts linked to this directive (tasks/files cascade from contracts). + // This covers step contracts (directive_id FK) and the orchestrator contract. + sqlx::query( + r#" + DELETE FROM contracts + WHERE directive_id = $1 + OR id = (SELECT orchestrator_contract_id FROM directives WHERE id = $1) + "#, + ) + .bind(id) + .execute(pool) + .await?; + + // Now delete the directive itself (chains, steps, events, evaluations cascade via FK) let result = sqlx::query( r#" DELETE FROM directives diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 80e2a8b..044fce6 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -758,7 +758,10 @@ async fn dispatch_step( .await .map_err(|e| format!("Failed to update step status: {}", e))?; - // Create contract for this step + // Create contract for this step. + // Step contracts use the directive's repository config — not local_only, + // so they can branch and merge to share work across steps. + let has_repo = directive.repository_url.is_some() || directive.local_path.is_some(); let contract = repository::create_contract_for_owner( pool, owner_id, @@ -770,8 +773,8 @@ async fn dispatch_step( initial_phase: step.initial_phase.clone(), autonomous_loop: Some(true), phase_guard: None, - local_only: Some(true), - auto_merge_local: None, + local_only: Some(!has_repo), + auto_merge_local: if has_repo { Some(true) } else { None }, }, ) .await -- cgit v1.2.3