import { useState, useEffect, useCallback } from "react";
import type {
ContractWithRelations,
ContractPhase,
ContractStatus,
ContractRepository,
FileSummary,
TaskSummary,
TemplateSummary,
} from "../../lib/api";
import {
listTemplates,
getTemplate,
createFile,
} from "../../lib/api";
import { PhaseProgressBar } from "./PhaseProgressBar";
import { PhaseHint } from "./PhaseHint";
import { RepositoryPanel } from "./RepositoryPanel";
import { ContractCliInput } from "./ContractCliInput";
import { PhaseDeliverablesPanel } from "./PhaseDeliverablesPanel";
import { CommandModePanel } from "./CommandModePanel";
import { TaskTree } from "../mesh/TaskTree";
type Tab = "overview" | "repos" | "files" | "tasks";
interface ContractDetailProps {
contract: ContractWithRelations;
loading: boolean;
onBack: () => void;
onUpdate: (name: string, description: string) => void;
onDelete: () => void;
onPhaseChange: (phase: ContractPhase) => void;
onStatusChange: (status: ContractStatus) => void;
onFileSelect: (id: string) => void;
onTaskSelect: (id: string) => void;
onTaskCreate: (name: string, plan: string, repositoryUrl?: string) => void;
onRefresh: () => void;
// Repository callbacks
onAddRemoteRepo: (name: string, url: string, isPrimary: boolean) => void;
onAddLocalRepo: (name: string, path: string, isPrimary: boolean) => void;
onCreateManagedRepo: (name: string, isPrimary: boolean) => void;
onDeleteRepo: (repoId: string) => void;
onSetRepoPrimary: (repoId: string) => void;
// File creation callback for phase deliverables
onCreateFileFromTemplate?: (templateId: string, suggestedName: string) => void;
}
const statusConfig: Record<ContractStatus, { label: string; color: string }> = {
active: { label: "Active", color: "text-green-400" },
completed: { label: "Completed", color: "text-blue-400" },
archived: { label: "Archived", color: "text-[#555]" },
};
export function ContractDetail({
contract,
loading,
onBack,
onUpdate,
onDelete,
onPhaseChange,
onStatusChange,
onFileSelect,
onTaskSelect,
onTaskCreate,
onRefresh,
onAddRemoteRepo,
onAddLocalRepo,
onCreateManagedRepo,
onDeleteRepo,
onSetRepoPrimary,
onCreateFileFromTemplate,
}: ContractDetailProps) {
const [activeTab, setActiveTab] = useState<Tab>("overview");
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(contract.name);
const [description, setDescription] = useState(contract.description || "");
const handleSave = () => {
onUpdate(name, description);
setIsEditing(false);
};
const handleCancel = () => {
setName(contract.name);
setDescription(contract.description || "");
setIsEditing(false);
};
if (loading) {
return (
<div className="panel h-full flex items-center justify-center">
<div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
</div>
);
}
const tabs: { key: Tab; label: string; count?: number }[] = [
{ key: "overview", label: "Overview" },
{ key: "repos", label: "Repositories", count: contract.repositories.length },
{ key: "files", label: "Files", count: contract.files.length },
{ key: "tasks", label: "Tasks", count: contract.tasks.length },
];
return (
<div className="panel h-full flex flex-col min-h-0">
{/* Header */}
<div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
<div className="flex items-center justify-between mb-3">
<button
onClick={onBack}
className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
>
← Back to list
</button>
<div className="flex items-center gap-2">
{isEditing ? (
<>
<button
onClick={handleCancel}
className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
>
Save
</button>
</>
) : (
<>
<button
onClick={() => setIsEditing(true)}
className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors uppercase"
>
Edit
</button>
<button
onClick={onDelete}
className="px-3 py-1.5 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
>
Delete
</button>
</>
)}
</div>
</div>
{isEditing ? (
<div className="space-y-3">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
placeholder="Contract name"
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
rows={2}
placeholder="Description (optional)"
/>
</div>
) : (
<>
<div className="flex items-center gap-3 mb-2">
<h2 className="font-mono text-lg text-[#dbe7ff]">
{contract.name}
</h2>
<span
className={`font-mono text-xs uppercase ${
statusConfig[contract.status].color
}`}
>
{statusConfig[contract.status].label}
</span>
{contract.localOnly && (
<span className="px-2 py-0.5 font-mono text-[10px] uppercase text-amber-400 border border-amber-400/30 bg-amber-400/10">
Local-Only
</span>
)}
</div>
{contract.description && (
<p className="font-mono text-sm text-[#9bc3ff] mb-3">
{contract.description}
</p>
)}
</>
)}
{/* Phase progress */}
<div className="mt-4 pt-4 border-t border-dashed border-[rgba(117,170,252,0.2)]">
<PhaseProgressBar
currentPhase={contract.phase}
onPhaseClick={onPhaseChange}
/>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-[rgba(117,170,252,0.2)]">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`
px-4 py-2 font-mono text-xs uppercase tracking-wider transition-colors
${
activeTab === tab.key
? "text-[#dbe7ff] border-b-2 border-[#75aafc]"
: "text-[#555] hover:text-[#9bc3ff]"
}
`}
>
{tab.label}
{tab.count !== undefined && tab.count > 0 && (
<span className="ml-1 text-[10px]">({tab.count})</span>
)}
</button>
))}
</div>
{/* Tab content */}
<div className="flex-1 overflow-y-auto p-4 min-h-0">
{activeTab === "overview" && (
<OverviewTab
contract={contract}
onStatusChange={onStatusChange}
onPhaseChange={onPhaseChange}
onCreateFile={onCreateFileFromTemplate}
onRefresh={onRefresh}
/>
)}
{activeTab === "repos" && (
<RepositoryPanel
repositories={contract.repositories}
onAddRemote={onAddRemoteRepo}
onAddLocal={onAddLocalRepo}
onCreateManaged={onCreateManagedRepo}
onDelete={onDeleteRepo}
onSetPrimary={onSetRepoPrimary}
/>
)}
{activeTab === "files" && (
<FilesTab
files={contract.files}
contractId={contract.id}
contractPhase={contract.phase}
onSelect={onFileSelect}
onRefresh={onRefresh}
/>
)}
{activeTab === "tasks" && (
<TasksTab
tasks={contract.tasks}
repositories={contract.repositories}
supervisorTaskId={contract.supervisorTaskId}
contractType={contract.contractType}
onSelect={onTaskSelect}
onCreate={onTaskCreate}
/>
)}
</div>
{/* Chat Input */}
<ContractCliInput
contractId={contract.id}
contract={contract}
onUpdate={onRefresh}
/>
</div>
);
}
// Overview tab
function OverviewTab({
contract,
onStatusChange,
onPhaseChange,
onCreateFile,
onRefresh,
}: {
contract: ContractWithRelations;
onStatusChange: (status: ContractStatus) => void;
onPhaseChange: (phase: ContractPhase) => void;
onCreateFile?: (templateId: string, suggestedName: string) => void;
onRefresh: () => void;
}) {
return (
<div className="space-y-6">
{/* Command Mode controls */}
<CommandModePanel contract={contract} onUpdate={onRefresh} />
{/* Phase deliverables checklist */}
<PhaseDeliverablesPanel
contract={contract}
onCreateFile={onCreateFile}
/>
{/* Phase hint */}
<PhaseHint contract={contract} onAdvancePhase={onPhaseChange} />
{/* Task progress summary */}
<TaskStatusSummary tasks={contract.tasks} />
{/* Stats */}
<div className="grid grid-cols-3 gap-4">
<StatCard label="Repositories" value={contract.repositories.length} />
<StatCard label="Files" value={contract.files.length} />
<StatCard label="Tasks" value={contract.tasks.length} />
</div>
{/* Status change */}
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">
Status
</h3>
<div className="flex gap-2">
{(["active", "completed", "archived"] as ContractStatus[]).map(
(status) => (
<button
key={status}
onClick={() => onStatusChange(status)}
className={`
px-3 py-1.5 font-mono text-xs uppercase transition-colors
${
contract.status === status
? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
: "text-[#555] border border-transparent hover:text-[#75aafc]"
}
`}
>
{status}
</button>
)
)}
</div>
</div>
{/* Metadata */}
<div>
<h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">
Details
</h3>
<div className="space-y-1 font-mono text-xs text-[#555]">
<p>Created: {new Date(contract.createdAt).toLocaleString()}</p>
<p>Updated: {new Date(contract.updatedAt).toLocaleString()}</p>
<p>Version: {contract.version}</p>
</div>
</div>
</div>
);
}
function StatCard({ label, value }: { label: string; value: number }) {
return (
<div className="p-3 border border-[rgba(117,170,252,0.2)]">
<div className="font-mono text-2xl text-[#dbe7ff]">{value}</div>
<div className="font-mono text-[10px] text-[#555] uppercase">{label}</div>
</div>
);
}
// Task status summary with progress bar
function TaskStatusSummary({ tasks }: { tasks: TaskSummary[] }) {
if (tasks.length === 0) return null;
// Count tasks by status
const statusCounts = {
done: 0,
merged: 0,
running: 0,
pending: 0,
failed: 0,
other: 0,
};
for (const task of tasks) {
switch (task.status) {
case "done":
statusCounts.done++;
break;
case "merged":
statusCounts.merged++;
break;
case "running":
case "initializing":
case "starting":
statusCounts.running++;
break;
case "pending":
statusCounts.pending++;
break;
case "failed":
statusCounts.failed++;
break;
default:
statusCounts.other++;
}
}
const completedCount = statusCounts.done + statusCounts.merged;
const progressPercent = (completedCount / tasks.length) * 100;
// Build summary parts
const parts: string[] = [];
if (completedCount > 0) parts.push(`${completedCount} done`);
if (statusCounts.running > 0) parts.push(`${statusCounts.running} running`);
if (statusCounts.pending > 0) parts.push(`${statusCounts.pending} pending`);
if (statusCounts.failed > 0) parts.push(`${statusCounts.failed} failed`);
return (
<div className="space-y-2">
<h3 className="font-mono text-xs text-[#75aafc] uppercase">
Task Progress
</h3>
{/* Progress bar */}
<div className="h-2 bg-[rgba(117,170,252,0.1)] rounded overflow-hidden">
<div
className="h-full bg-green-400 transition-all duration-300"
style={{ width: `${progressPercent}%` }}
/>
</div>
{/* Summary text */}
<div className="flex items-center justify-between">
<span className="font-mono text-xs text-[#9bc3ff]">
{parts.join(", ")}
</span>
<span className="font-mono text-xs text-[#555]">
{completedCount}/{tasks.length} completed
</span>
</div>
</div>
);
}
// Phase color mapping for badges
const phaseColors: Record<ContractPhase, string> = {
research: "bg-purple-500/20 text-purple-400 border-purple-400/30",
specify: "bg-blue-500/20 text-blue-400 border-blue-400/30",
plan: "bg-cyan-500/20 text-cyan-400 border-cyan-400/30",
execute: "bg-green-500/20 text-green-400 border-green-400/30",
review: "bg-yellow-500/20 text-yellow-400 border-yellow-400/30",
};
// Files tab with template creation
function FilesTab({
files,
contractId,
contractPhase,
onSelect,
onRefresh,
}: {
files: FileSummary[];
contractId: string;
contractPhase: ContractPhase;
onSelect: (id: string) => void;
onRefresh: () => void;
}) {
const [showTemplateModal, setShowTemplateModal] = useState(false);
const [templates, setTemplates] = useState<TemplateSummary[]>([]);
const [loadingTemplates, setLoadingTemplates] = useState(false);
const [creating, setCreating] = useState(false);
const [fileName, setFileName] = useState("");
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
// Load templates when modal opens
useEffect(() => {
if (showTemplateModal) {
setLoadingTemplates(true);
listTemplates(contractPhase)
.then((res) => setTemplates(res.templates))
.catch((err) => console.error("Failed to load templates:", err))
.finally(() => setLoadingTemplates(false));
}
}, [showTemplateModal, contractPhase]);
const handleCreateFromTemplate = useCallback(async () => {
if (!fileName.trim() || !selectedTemplateId) return;
setCreating(true);
try {
// Get the full template with body
const template = await getTemplate(selectedTemplateId);
// Create the file with contract (files must belong to contracts)
await createFile({
contractId,
name: fileName.trim(),
description: template.description,
body: template.suggestedBody,
});
// Reset and close
setShowTemplateModal(false);
setFileName("");
setSelectedTemplateId(null);
onRefresh();
} catch (err) {
console.error("Failed to create file from template:", err);
} finally {
setCreating(false);
}
}, [fileName, selectedTemplateId, contractId, onRefresh]);
const handleCloseModal = () => {
setShowTemplateModal(false);
setFileName("");
setSelectedTemplateId(null);
};
return (
<div className="space-y-4">
{/* Create from template button */}
<button
onClick={() => setShowTemplateModal(true)}
className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
>
+ Create from Template
</button>
{/* Template Selection Modal */}
{showTemplateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="font-mono text-sm text-[#75aafc] uppercase">
Create File from Template
</h3>
<span className={`px-2 py-0.5 text-[10px] font-mono uppercase border rounded ${phaseColors[contractPhase]}`}>
{contractPhase} phase
</span>
</div>
<div className="space-y-4 flex-1 overflow-y-auto">
{/* File name input */}
<div>
<label className="block font-mono text-xs text-[#555] uppercase mb-1">
File Name
</label>
<input
type="text"
value={fileName}
onChange={(e) => setFileName(e.target.value)}
placeholder="e.g., Project Requirements"
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
autoFocus
/>
</div>
{/* Template selection */}
<div>
<label className="block font-mono text-xs text-[#555] uppercase mb-2">
Select Template
</label>
{loadingTemplates ? (
<p className="font-mono text-xs text-[#555]">Loading templates...</p>
) : templates.length === 0 ? (
<p className="font-mono text-xs text-[#555]">No templates available for {contractPhase} phase</p>
) : (
<div className="space-y-2 max-h-60 overflow-y-auto">
{templates.map((template) => (
<button
key={template.id}
onClick={() => setSelectedTemplateId(template.id)}
className={`w-full text-left p-3 border transition-colors ${
selectedTemplateId === template.id
? "border-[#75aafc] bg-[rgba(117,170,252,0.1)]"
: "border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)]"
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-sm text-[#dbe7ff]">
{template.name}
</span>
<span className="font-mono text-[10px] text-[#555]">
{template.elementCount} elements
</span>
</div>
<p className="font-mono text-xs text-[#555]">
{template.description}
</p>
</button>
))}
</div>
)}
</div>
</div>
<div className="flex gap-2 justify-end mt-4 pt-4 border-t border-[rgba(117,170,252,0.2)]">
<button
onClick={handleCloseModal}
className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleCreateFromTemplate}
disabled={!fileName.trim() || !selectedTemplateId || creating}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{creating ? "Creating..." : "Create File"}
</button>
</div>
</div>
</div>
)}
{/* File list */}
{files.length === 0 ? (
<p className="font-mono text-xs text-[#555]">
No files in this contract. Create one from a template above.
</p>
) : (
<div className="space-y-2">
{files.map((file) => (
<button
key={file.id}
onClick={() => onSelect(file.id)}
className="w-full text-left p-3 border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-[#dbe7ff]">
{file.name}
</span>
{file.contractPhase && (
<span
className={`px-1.5 py-0.5 text-[9px] font-mono uppercase border rounded ${
phaseColors[file.contractPhase]
}`}
title={`Added during ${file.contractPhase} phase`}
>
{file.contractPhase}
</span>
)}
</div>
<span className="font-mono text-[10px] text-[#555]">
v{file.version}
</span>
</div>
{file.description && (
<p className="font-mono text-xs text-[#555] mt-1 truncate">
{file.description}
</p>
)}
</button>
))}
</div>
)}
</div>
);
}
// Tasks tab - now using TaskTree for supervisor view
function TasksTab({
tasks,
repositories,
supervisorTaskId,
contractType,
onSelect,
onCreate,
}: {
tasks: TaskSummary[];
repositories: ContractRepository[];
supervisorTaskId: string | null;
contractType: string;
onSelect: (id: string) => void;
onCreate: (name: string, plan: string, repositoryUrl?: string) => void;
}) {
const [isCreating, setIsCreating] = useState(false);
const [taskName, setTaskName] = useState("");
const [taskPlan, setTaskPlan] = useState("# Plan\n\nDescribe what this task should accomplish...");
// Find primary repository or first ready one
const readyRepos = repositories.filter((r) => r.status === "ready");
const primaryRepo = readyRepos.find((r) => r.isPrimary) || readyRepos[0];
const [selectedRepoId, setSelectedRepoId] = useState<string>(primaryRepo?.id || "");
const handleCreate = () => {
if (!taskName.trim()) return;
const selectedRepo = repositories.find((r) => r.id === selectedRepoId);
// Get the URL - for remote repos it's repositoryUrl, for local it's the local path
const repoUrl = selectedRepo?.repositoryUrl || selectedRepo?.localPath;
onCreate(taskName.trim(), taskPlan, repoUrl || undefined);
setIsCreating(false);
setTaskName("");
setTaskPlan("# Plan\n\nDescribe what this task should accomplish...");
setSelectedRepoId(primaryRepo?.id || "");
};
const handleCancel = () => {
setIsCreating(false);
setTaskName("");
setTaskPlan("# Plan\n\nDescribe what this task should accomplish...");
setSelectedRepoId(primaryRepo?.id || "");
};
return (
<div className="space-y-4">
{/* TaskTree with supervisor view */}
<TaskTree
tasks={tasks}
supervisorTaskId={supervisorTaskId}
onSelect={onSelect}
/>
{/* Manual task creation - show for task-type contracts or contracts without supervisors */}
{(contractType === "task" || !supervisorTaskId) && (
<>
<div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
<button
onClick={() => setIsCreating(true)}
className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
>
+ Create Task Manually
</button>
</div>
{/* Create Task Modal */}
{isCreating && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]">
<h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4">
Create Task
</h3>
<div className="space-y-4">
<div>
<label className="block font-mono text-xs text-[#555] uppercase mb-1">
Name
</label>
<input
type="text"
value={taskName}
onChange={(e) => setTaskName(e.target.value)}
placeholder="Task name"
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
autoFocus
/>
</div>
{/* Repository selection */}
{readyRepos.length > 0 && (
<div>
<label className="block font-mono text-xs text-[#555] uppercase mb-1">
Repository
</label>
<select
value={selectedRepoId}
onChange={(e) => setSelectedRepoId(e.target.value)}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
>
<option value="">No repository</option>
{readyRepos.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
{repo.isPrimary ? " (Primary)" : ""}
{" - "}
{repo.sourceType}
</option>
))}
</select>
</div>
)}
<div>
<label className="block font-mono text-xs text-[#555] uppercase mb-1">
Plan
</label>
<textarea
value={taskPlan}
onChange={(e) => setTaskPlan(e.target.value)}
rows={6}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none"
/>
</div>
<div className="flex gap-2 justify-end">
<button
onClick={handleCancel}
className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={!taskName.trim()}
className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Create
</button>
</div>
</div>
</div>
</div>
)}
</>
)}
</div>
);
}