summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 02:55:45 +0000
committersoryu <soryu@soryu.co>2026-01-25 03:53:14 +0000
commit9dc282e4777abf41178c44f14f8be3de1c725b10 (patch)
tree2a18dcd3bfaf0bc3a2c0734209d810e28bfcf2dc
parent34bc1296a53d264216c12cbaa74ca9d68dfe8f22 (diff)
downloadsoryu-makima/dynamic-contract-templates.tar.gz
soryu-makima/dynamic-contract-templates.zip
feat: Update create contract modal to use dynamic templatesmakima/dynamic-contract-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 <noreply@anthropic.com>
-rw-r--r--makima/frontend/src/lib/api.ts37
-rw-r--r--makima/frontend/src/routes/contracts.tsx121
-rw-r--r--makima/src/server/handlers/templates.rs70
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<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 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<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 {