summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 03:53:55 +0000
committerGitHub <noreply@github.com>2026-01-25 03:53:55 +0000
commitc908854e7e3571c99cce9f46497ce5337ea0aed1 (patch)
tree3d35137a452c562059ecd8c759393b90937df70c /makima/frontend/src/routes
parent03ab90836707954277597dc21fd8035019d8e221 (diff)
downloadsoryu-c908854e7e3571c99cce9f46497ce5337ea0aed1.tar.gz
soryu-c908854e7e3571c99cce9f46497ce5337ea0aed1.zip
Update create contract page to use dynamic templates (#29)
* feat: Add contract type templates API endpoint Add a new API endpoint to provide contract type templates (workflow definitions) separate from file templates. This enables the create contract page to dynamically show available contract types. Changes: - Add ContractTypeTemplate struct in templates.rs with id, name, description, phases, default_phase, and is_builtin fields - Add built-in contract types: 'simple' (plan/execute) and 'specification' (research/specify/plan/execute/review) - Add GET /api/v1/contract-types endpoint returning all contract types - Add frontend ContractTypeTemplate interface and listContractTypes() API function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/contracts.tsx121
1 files changed, 82 insertions, 39 deletions
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index 6946cb8..4f74692 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,
@@ -516,41 +563,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 */}
@@ -563,7 +606,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>