diff options
| author | soryu <soryu@soryu.co> | 2026-01-25 02:55:45 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-25 02:55:45 +0000 |
| commit | b58a7a92f86784620bd5eb214d7f191c6f68f4d3 (patch) | |
| tree | c927ff7a2796de8f92cd7b8ebcf2a79c63c75901 | |
| parent | 579c983d3efb8f1414ffb45b9e031f741cce5f76 (diff) | |
| download | soryu-makima/task-task-418149db-418149db.tar.gz soryu-makima/task-task-418149db-418149db.zip | |
[WIP] Heartbeat checkpoint - 2026-01-25 02:55:45 UTCmakima/task-task-418149db-418149db
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 37 | ||||
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 121 | ||||
| -rw-r--r-- | makima/src/server/handlers/templates.rs | 70 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 2 |
4 files changed, 191 insertions, 39 deletions
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 76ee4d4..9af7f3c 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<ListContractTypesResponse> { + 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<DaemonDirectory[]>([]); const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]); const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); + const [contractTypes, setContractTypes] = useState<ContractTypeTemplate[]>([]); + 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() { <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> Contract Type </label> - <div className="flex gap-2"> - <button - type="button" - onClick={() => { - setContractType("simple"); - setInitialPhase("plan"); - }} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ - contractType === "simple" - ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" - : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" - }`} - > - Simple - </button> - <button - type="button" - onClick={() => { - setContractType("specification"); - setInitialPhase("research"); - }} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ - contractType === "specification" - ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" - : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" - }`} - > - Specification - </button> - </div> - <p className="mt-1 font-mono text-xs text-[#8b949e]"> - {contractType === "simple" - ? "Plan → Execute: Simple workflow with a plan document" - : "Research → Specify → Plan → Execute → Review: Full specification-driven development with TDD"} - </p> + {contractTypesLoading ? ( + <div className="flex items-center justify-center py-4"> + <span className="font-mono text-xs text-[#8b949e]">Loading contract types...</span> + </div> + ) : ( + <> + <div className="flex gap-2"> + {contractTypes.map((type) => ( + <button + key={type.id} + type="button" + onClick={() => { + setContractType(type.id as ContractType); + setInitialPhase(type.defaultPhase as ContractPhase); + }} + className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${ + contractType === type.id + ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]" + : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]" + }`} + > + {type.name} + </button> + ))} + </div> + <p className="mt-1 font-mono text-xs text-[#8b949e]"> + {contractTypes.find((t) => t.id === contractType)?.description || + "Select a contract type"} + </p> + </> + )} </div> {/* 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) => ( <option key={phase} value={phase}> {phase.charAt(0).toUpperCase() + phase.slice(1)} </option> diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 868d5b4..13d491e 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -6,6 +6,76 @@ use utoipa::ToSchema; use crate::llm::templates; +// ============================================================================= +// 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<String>, + /// 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<ContractTypeTemplate>, +} + +/// 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 { diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index de20569..b4afa78 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -208,6 +208,8 @@ pub fn make_router(state: SharedState) -> Router { // Template endpoints .route("/templates", get(templates::list_templates)) .route("/templates/{id}", get(templates::get_template)) + // Contract type endpoints + .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints .route( "/settings/repository-history", |
