From 9dc282e4777abf41178c44f14f8be3de1c725b10 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 25 Jan 2026 02:55:45 +0000 Subject: feat: Update create contract modal to use dynamic templates - Add ContractTypeTemplate interface and listContractTypes API function to frontend api.ts - Add GET /api/v1/contract-types backend endpoint that returns built-in contract types (simple, specification) with their phases and defaults - Update create contract modal to fetch contract types dynamically when opened, with loading state and fallback to hardcoded types on error - Dynamically render contract type selection buttons from fetched types - Update phase dropdown to use phases from selected contract type - Replace static getValidPhases/getDefaultPhase calls with dynamic data Co-Authored-By: Claude Opus 4.5 --- makima/frontend/src/lib/api.ts | 37 ++++++++++ makima/frontend/src/routes/contracts.tsx | 121 +++++++++++++++++++++---------- makima/src/server/handlers/templates.rs | 70 ++++++++++++++++++ 3 files changed, 189 insertions(+), 39 deletions(-) diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 1ae4103..64ce591 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1507,6 +1507,43 @@ export function getDefaultPhase(contractType: ContractType): ContractPhase { return "research"; } +// ============================================================================= +// Contract Type Templates +// ============================================================================= + +/** Contract type template returned by the API */ +export interface ContractTypeTemplate { + /** Template identifier (e.g., "simple", "specification") */ + id: string; + /** Display name */ + name: string; + /** Description of the contract type workflow */ + description: string; + /** Ordered list of phases for this contract type */ + phases: ContractPhase[]; + /** Default starting phase */ + defaultPhase: ContractPhase; + /** Whether this is a built-in type (always available) */ + isBuiltin: boolean; +} + +/** Response from list contract types endpoint */ +export interface ListContractTypesResponse { + types: ContractTypeTemplate[]; +} + +/** + * List available contract types/templates. + * Returns built-in types (simple, specification) and any custom types. + */ +export async function listContractTypes(): Promise { + const res = await authFetch(`${API_BASE}/api/v1/contract-types`); + if (!res.ok) { + throw new Error(`Failed to list contract types: ${res.statusText}`); + } + return res.json(); +} + export interface ContractRepository { id: string; contractId: string; diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index 6acda29..aa5bf3d 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -6,7 +6,12 @@ import { ContractDetail } from "../components/contracts/ContractDetail"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useContracts } from "../hooks/useContracts"; import { useAuth } from "../contexts/AuthContext"; -import { createTask, getDaemonDirectories, getRepositorySuggestions } from "../lib/api"; +import { + createTask, + getDaemonDirectories, + getRepositorySuggestions, + listContractTypes, +} from "../lib/api"; import type { ContractWithRelations, ContractSummary, @@ -17,8 +22,8 @@ import type { RepositorySourceType, DaemonDirectory, RepositoryHistoryEntry, + ContractTypeTemplate, } from "../lib/api"; -import { getValidPhases, getDefaultPhase } from "../lib/api"; export default function ContractsPage() { const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); @@ -85,6 +90,43 @@ function ContractsPageContent() { const [suggestedDirectories, setSuggestedDirectories] = useState([]); const [repoSuggestions, setRepoSuggestions] = useState([]); const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); + const [contractTypes, setContractTypes] = useState([]); + const [contractTypesLoading, setContractTypesLoading] = useState(false); + + // Fetch contract types when modal opens + useEffect(() => { + if (isCreating) { + setContractTypesLoading(true); + listContractTypes() + .then((res) => { + setContractTypes(res.types); + setContractTypesLoading(false); + }) + .catch((err) => { + console.error("Failed to fetch contract types:", err); + // Fall back to built-in types + setContractTypes([ + { + id: "simple", + name: "Simple", + description: "Plan \u2192 Execute: Simple workflow with a plan document", + phases: ["plan", "execute"], + defaultPhase: "plan", + isBuiltin: true, + }, + { + id: "specification", + name: "Specification", + description: "Research \u2192 Specify \u2192 Plan \u2192 Execute \u2192 Review: Full specification-driven development with TDD", + phases: ["research", "specify", "plan", "execute", "review"], + defaultPhase: "research", + isBuiltin: true, + }, + ]); + setContractTypesLoading(false); + }); + } + }, [isCreating]); // Fetch repository suggestions when modal opens and repo type changes useEffect(() => { @@ -170,11 +212,15 @@ function ContractsPageContent() { setCreateError(null); + // Get default phase from contract types or fall back to static function + const selectedType = contractTypes.find((t) => t.id === contractType); + const defaultPhaseForType = selectedType?.defaultPhase || (contractType === "simple" ? "plan" : "research"); + const data: CreateContractRequest = { name: newContractName.trim(), description: newContractDescription.trim() || undefined, contractType: contractType, - initialPhase: initialPhase !== getDefaultPhase(contractType) ? initialPhase : undefined, + initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined, }; try { @@ -224,6 +270,7 @@ function ContractsPageContent() { newContractName, newContractDescription, contractType, + contractTypes, initialPhase, repoType, repoName, @@ -514,41 +561,37 @@ function ContractsPageContent() { -
- - -
-

- {contractType === "simple" - ? "Plan → Execute: Simple workflow with a plan document" - : "Research → Specify → Plan → Execute → Review: Full specification-driven development with TDD"} -

+ {contractTypesLoading ? ( +
+ Loading contract types... +
+ ) : ( + <> +
+ {contractTypes.map((type) => ( + + ))} +
+

+ {contractTypes.find((t) => t.id === contractType)?.description || + "Select a contract type"} +

+ + )} {/* Starting Phase */} @@ -561,7 +604,7 @@ function ContractsPageContent() { onChange={(e) => setInitialPhase(e.target.value as ContractPhase)} className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" > - {getValidPhases(contractType).map((phase) => ( + {(contractTypes.find((t) => t.id === contractType)?.phases || []).map((phase) => ( diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 6d95e86..5cad44f 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -7,6 +7,76 @@ use utoipa::ToSchema; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; +// ============================================================================= +// Contract Type Templates +// ============================================================================= + +/// Contract type template for API response +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractTypeTemplate { + /// Template identifier (e.g., "simple", "specification") + pub id: String, + /// Display name + pub name: String, + /// Description of the contract type workflow + pub description: String, + /// Ordered list of phases for this contract type + pub phases: Vec, + /// Default starting phase + pub default_phase: String, + /// Whether this is a built-in type (always available) + pub is_builtin: bool, +} + +/// Response for listing contract types +#[derive(Debug, Serialize, ToSchema)] +pub struct ListContractTypesResponse { + pub types: Vec, +} + +/// List available contract type templates +#[utoipa::path( + get, + path = "/api/v1/contract-types", + responses( + (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse) + ), + tag = "contract-types" +)] +pub async fn list_contract_types() -> impl IntoResponse { + let types = vec![ + ContractTypeTemplate { + id: "simple".to_string(), + name: "Simple".to_string(), + description: "Plan \u{2192} Execute: Simple workflow with a plan document".to_string(), + phases: vec!["plan".to_string(), "execute".to_string()], + default_phase: "plan".to_string(), + is_builtin: true, + }, + ContractTypeTemplate { + id: "specification".to_string(), + name: "Specification".to_string(), + description: "Research \u{2192} Specify \u{2192} Plan \u{2192} Execute \u{2192} Review: Full specification-driven development with TDD".to_string(), + phases: vec![ + "research".to_string(), + "specify".to_string(), + "plan".to_string(), + "execute".to_string(), + "review".to_string(), + ], + default_phase: "research".to_string(), + is_builtin: true, + }, + ]; + + ( + StatusCode::OK, + Json(ListContractTypesResponse { types }), + ) + .into_response() +} + /// Query parameters for listing templates #[derive(Debug, Deserialize, ToSchema)] pub struct ListTemplatesQuery { -- cgit v1.2.3