summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/contracts/ContractDetail.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-11 05:52:14 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:21:16 +0000
commit87044a747b47bd83249d61a45842c7f7b2eae56d (patch)
treeef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/frontend/src/components/contracts/ContractDetail.tsx
parent077820c4167c168072d217a1b01df840463a12a8 (diff)
downloadsoryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz
soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip
Contract system
Diffstat (limited to 'makima/frontend/src/components/contracts/ContractDetail.tsx')
-rw-r--r--makima/frontend/src/components/contracts/ContractDetail.tsx794
1 files changed, 794 insertions, 0 deletions
diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx
new file mode 100644
index 0000000..cf5f8f2
--- /dev/null
+++ b/makima/frontend/src/components/contracts/ContractDetail.tsx
@@ -0,0 +1,794 @@
+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 { 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">
+ {/* 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"
+ >
+ &larr; 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>
+ </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">
+ {activeTab === "overview" && (
+ <OverviewTab
+ contract={contract}
+ onStatusChange={onStatusChange}
+ onPhaseChange={onPhaseChange}
+ onCreateFile={onCreateFileFromTemplate}
+ />
+ )}
+
+ {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}
+ onSelect={onTaskSelect}
+ onCreate={onTaskCreate}
+ />
+ )}
+ </div>
+
+ {/* Chat Input */}
+ <ContractCliInput
+ contractId={contract.id}
+ contract={contract}
+ onUpdate={onRefresh}
+ />
+ </div>
+ );
+}
+
+// Overview tab
+function OverviewTab({
+ contract,
+ onStatusChange,
+ onPhaseChange,
+ onCreateFile,
+}: {
+ contract: ContractWithRelations;
+ onStatusChange: (status: ContractStatus) => void;
+ onPhaseChange: (phase: ContractPhase) => void;
+ onCreateFile?: (templateId: string, suggestedName: string) => void;
+}) {
+ return (
+ <div className="space-y-6">
+ {/* 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,
+ onSelect,
+ onCreate,
+}: {
+ tasks: TaskSummary[];
+ repositories: ContractRepository[];
+ supervisorTaskId: string | null;
+ 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 (hidden when supervisor exists - supervisor creates tasks) */}
+ {!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>
+ );
+}