summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx9
-rw-r--r--makima/frontend/src/components/contracts/CommandModePanel.tsx272
-rw-r--r--makima/frontend/src/components/contracts/ContractCliInput.tsx974
-rw-r--r--makima/frontend/src/components/contracts/ContractContextMenu.tsx160
-rw-r--r--makima/frontend/src/components/contracts/ContractDetail.tsx810
-rw-r--r--makima/frontend/src/components/contracts/ContractList.tsx223
-rw-r--r--makima/frontend/src/components/contracts/PhaseBadge.tsx54
-rw-r--r--makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx339
-rw-r--r--makima/frontend/src/components/contracts/PhaseHint.tsx90
-rw-r--r--makima/frontend/src/components/contracts/PhaseProgressBar.tsx149
-rw-r--r--makima/frontend/src/components/contracts/QuickActionButtons.tsx217
-rw-r--r--makima/frontend/src/components/contracts/RepositoryPanel.tsx325
-rw-r--r--makima/frontend/src/components/contracts/TaskDerivationPreview.tsx221
-rw-r--r--makima/frontend/src/main.tsx26
-rw-r--r--makima/frontend/src/routes/contract-file.tsx659
-rw-r--r--makima/frontend/src/routes/contracts.tsx885
-rw-r--r--makima/frontend/src/routes/document-directives.tsx16
-rw-r--r--makima/frontend/src/routes/tmp.tsx9
18 files changed, 16 insertions, 5422 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 6fe4ba9..7c5dad1 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -20,12 +20,9 @@ const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
{ label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Orders", href: "/orders", requiresAuth: true },
- {
- label: "Contracts",
- href: "/contracts",
- requiresAuth: true,
- hideInDocumentMode: true,
- },
+ // /contracts has been removed in Phase 5; the legacy nav entry is gone.
+ // /exec is still reachable for the standalone task page but hidden when
+ // document mode is on (the unified surface routes through /directives).
{
label: "Exec",
href: "/exec",
diff --git a/makima/frontend/src/components/contracts/CommandModePanel.tsx b/makima/frontend/src/components/contracts/CommandModePanel.tsx
deleted file mode 100644
index b39b309..0000000
--- a/makima/frontend/src/components/contracts/CommandModePanel.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-import { useState, useCallback } from "react";
-import { useNavigate } from "react-router";
-import type { ContractWithRelations } from "../../lib/api";
-import {
- getSupervisorStatus,
- startSupervisor,
- stopSupervisor,
- resumeSupervisor,
- updateContract,
- type SupervisorStatus,
-} from "../../lib/api";
-
-interface CommandModePanelProps {
- contract: ContractWithRelations;
- onUpdate: () => void;
-}
-
-const statusConfig: Record<
- SupervisorStatus["status"],
- { label: string; color: string; bgColor: string }
-> = {
- not_configured: {
- label: "Not Configured",
- color: "text-[#555]",
- bgColor: "bg-[#555]/10",
- },
- pending: {
- label: "Ready",
- color: "text-yellow-400",
- bgColor: "bg-yellow-400/10",
- },
- starting: {
- label: "Starting...",
- color: "text-blue-400",
- bgColor: "bg-blue-400/10",
- },
- running: {
- label: "Running",
- color: "text-green-400",
- bgColor: "bg-green-400/10",
- },
- paused: {
- label: "Paused",
- color: "text-orange-400",
- bgColor: "bg-orange-400/10",
- },
- done: {
- label: "Completed",
- color: "text-blue-400",
- bgColor: "bg-blue-400/10",
- },
- failed: {
- label: "Failed",
- color: "text-red-400",
- bgColor: "bg-red-400/10",
- },
-};
-
-export function CommandModePanel({ contract, onUpdate }: CommandModePanelProps) {
- const navigate = useNavigate();
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
-
- const supervisorStatus = getSupervisorStatus(contract);
-
- const handleGoToSupervisor = useCallback(() => {
- if (supervisorStatus.supervisorTaskId) {
- navigate(`/exec/${supervisorStatus.supervisorTaskId}`);
- }
- }, [supervisorStatus.supervisorTaskId, navigate]);
- const config = statusConfig[supervisorStatus.status];
-
- const handleStart = useCallback(async () => {
- if (!supervisorStatus.supervisorTaskId) return;
-
- setLoading(true);
- setError(null);
-
- try {
- await startSupervisor(supervisorStatus.supervisorTaskId);
- onUpdate();
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to start command mode");
- } finally {
- setLoading(false);
- }
- }, [supervisorStatus.supervisorTaskId, onUpdate]);
-
- const handleStop = useCallback(async () => {
- if (!supervisorStatus.supervisorTaskId) return;
-
- setLoading(true);
- setError(null);
-
- try {
- await stopSupervisor(supervisorStatus.supervisorTaskId);
- onUpdate();
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to stop command mode");
- } finally {
- setLoading(false);
- }
- }, [supervisorStatus.supervisorTaskId, onUpdate]);
-
- const handleResume = useCallback(async () => {
- setLoading(true);
- setError(null);
-
- try {
- await resumeSupervisor(contract.id, { resumeMode: "continue" });
- // After resuming, we need to start the task
- if (supervisorStatus.supervisorTaskId) {
- await startSupervisor(supervisorStatus.supervisorTaskId);
- }
- onUpdate();
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to resume command mode");
- } finally {
- setLoading(false);
- }
- }, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);
-
- const handlePhaseGuardChange = useCallback(async (enabled: boolean) => {
- setLoading(true);
- setError(null);
-
- try {
- await updateContract(contract.id, {
- phaseGuard: enabled,
- version: contract.version,
- });
- onUpdate();
- } catch (e) {
- setError(e instanceof Error ? e.message : "Failed to update phase guard setting");
- } finally {
- setLoading(false);
- }
- }, [contract.id, contract.version, onUpdate]);
-
- return (
- <div className="space-y-3">
- <div className="flex items-center justify-between">
- <h3 className="font-mono text-xs text-[#75aafc] uppercase">
- Command Mode
- </h3>
- <div className="flex items-center gap-2">
- {supervisorStatus.supervisorTaskId && (
- <button
- onClick={handleGoToSupervisor}
- className="px-2 py-1 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] hover:bg-[rgba(117,170,252,0.1)] transition-colors flex items-center gap-1"
- >
- <span className="text-[#75aafc]">▶</span>
- Supervisor
- </button>
- )}
- <div
- className={`px-2 py-1 rounded font-mono text-xs ${config.color} ${config.bgColor}`}
- >
- {config.label}
- </div>
- </div>
- </div>
-
- <p className="font-mono text-xs text-[#555]">
- {supervisorStatus.status === "not_configured" ? (
- "This contract does not have a Command Mode supervisor configured."
- ) : supervisorStatus.status === "running" ? (
- "Command Mode is actively working on this contract, spawning tasks and managing progress."
- ) : supervisorStatus.status === "pending" ? (
- "Command Mode is ready to start. Click 'Enable' to begin autonomous work."
- ) : supervisorStatus.status === "paused" ? (
- "Command Mode is paused. Click 'Resume' to continue work."
- ) : supervisorStatus.status === "failed" ? (
- "Command Mode encountered an error. You can resume to retry."
- ) : supervisorStatus.status === "done" ? (
- "Command Mode has completed its work on this contract."
- ) : (
- "Command Mode is initializing..."
- )}
- </p>
-
- {error && (
- <div className="px-3 py-2 bg-red-500/10 border border-red-400/30 font-mono text-xs text-red-400">
- {error}
- </div>
- )}
-
- <div className="flex gap-2">
- {supervisorStatus.canStart && (
- <button
- onClick={handleStart}
- disabled={loading}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-green-600/20 border border-green-400/50 hover:bg-green-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {loading ? "Starting..." : "Enable Command Mode"}
- </button>
- )}
-
- {supervisorStatus.canResume && (
- <button
- onClick={handleResume}
- disabled={loading}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-blue-600/20 border border-blue-400/50 hover:bg-blue-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {loading ? "Resuming..." : "Resume Command Mode"}
- </button>
- )}
-
- {supervisorStatus.canStop && (
- <button
- onClick={handleStop}
- disabled={loading}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-orange-600/20 border border-orange-400/50 hover:bg-orange-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {loading ? "Stopping..." : "Pause Command Mode"}
- </button>
- )}
- </div>
-
- {/* Phase Guard Toggle */}
- <div className="pt-3 border-t border-dashed border-[rgba(117,170,252,0.2)]">
- <label className="flex items-start gap-3 cursor-pointer group">
- <div className="relative mt-0.5">
- <input
- type="checkbox"
- checked={contract.phaseGuard ?? false}
- onChange={(e) => handlePhaseGuardChange(e.target.checked)}
- disabled={loading}
- className="sr-only peer"
- />
- <div className="w-9 h-5 bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.3)] rounded-full peer-checked:bg-[rgba(117,170,252,0.3)] transition-colors peer-disabled:opacity-50" />
- <div className="absolute left-0.5 top-0.5 w-4 h-4 bg-[#555] rounded-full transition-transform peer-checked:translate-x-4 peer-checked:bg-[#75aafc] peer-disabled:opacity-50" />
- </div>
- <div className="flex-1">
- <div className="flex items-center gap-2">
- <span className="font-mono text-sm text-[#dbe7ff] group-hover:text-white transition-colors">
- Phase Guard
- </span>
- {contract.phaseGuard && (
- <span className="px-1.5 py-0.5 text-[9px] font-mono uppercase bg-yellow-500/20 text-yellow-400 border border-yellow-400/30 rounded">
- active
- </span>
- )}
- </div>
- <div className="font-mono text-xs text-[#555] mt-0.5">
- Ask for confirmation before advancing to the next phase
- </div>
- </div>
- </label>
- </div>
-
- {/* Show running indicator when active */}
- {supervisorStatus.status === "running" && (
- <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
- <div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
- <span className="font-mono text-xs text-green-400">
- Command Mode is actively working
- </span>
- </div>
- )}
-
- {supervisorStatus.status === "starting" && (
- <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
- <div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
- <span className="font-mono text-xs text-blue-400">
- Initializing command mode...
- </span>
- </div>
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/ContractCliInput.tsx b/makima/frontend/src/components/contracts/ContractCliInput.tsx
deleted file mode 100644
index 54d9f3a..0000000
--- a/makima/frontend/src/components/contracts/ContractCliInput.tsx
+++ /dev/null
@@ -1,974 +0,0 @@
-import { useState, useCallback, useRef, useEffect, useMemo } from "react";
-import {
- getContractChatHistory,
- clearContractChatHistory,
- startTask,
- sendTaskMessage,
- type UserQuestion,
- type ContractWithRelations,
- type TaskStatus,
-} from "../../lib/api";
-import { SimpleMarkdown } from "../SimpleMarkdown";
-import {
- QuickActionButtons,
- type QuickAction,
-} from "./QuickActionButtons";
-import { TaskDerivationPreview, type ParsedTask } from "./TaskDerivationPreview";
-import { useTaskSubscription, type TaskOutputEvent } from "../../hooks/useTaskSubscription";
-
-interface ContractCliInputProps {
- contractId: string;
- contract: ContractWithRelations;
- onUpdate: () => void;
-}
-
-interface Message {
- id: string;
- type: "user" | "assistant" | "error" | "question";
- content: string;
- toolCalls?: { name: string; success: boolean; message: string }[];
- questions?: UserQuestion[];
- quickActions?: QuickAction[];
-}
-
-export function ContractCliInput({ contractId, contract, onUpdate }: ContractCliInputProps) {
- const [input, setInput] = useState("");
- const [loading, setLoading] = useState(false);
- const [historyLoading, setHistoryLoading] = useState(true);
- const [messages, setMessages] = useState<Message[]>([]);
- const [expanded, setExpanded] = useState(false);
- const [fullscreen, setFullscreen] = useState(false);
- const [pendingQuestions, setPendingQuestions] = useState<UserQuestion[] | null>(null);
- const [userAnswers, setUserAnswers] = useState<Map<string, string[]>>(new Map());
- const [customInputs, setCustomInputs] = useState<Map<string, string>>(new Map());
-
- // Task derivation state
- const [parsedTasks, setParsedTasks] = useState<ParsedTask[] | null>(null);
- const [parsedTaskGroups, setParsedTaskGroups] = useState<string[]>([]);
- const [parsedTasksFileName, setParsedTasksFileName] = useState<string>("");
- const [creatingTasks, setCreatingTasks] = useState(false);
-
- // Supervisor state
- const [supervisorStarting, setSupervisorStarting] = useState(false);
- const [supervisorOutput, setSupervisorOutput] = useState<TaskOutputEvent[]>([]);
- const [supervisorQuestion, setSupervisorQuestion] = useState<{
- id: string;
- question: string;
- options: string[];
- allowMultiple?: boolean;
- allowCustom?: boolean;
- } | null>(null);
-
- const inputRef = useRef<HTMLInputElement>(null);
- const messagesRef = useRef<HTMLDivElement>(null);
-
- // Find the supervisor task for this contract
- // First try by supervisorTaskId on the contract, then fall back to isSupervisor flag
- const supervisorTask = useMemo(() => {
- // Use contract.supervisorTaskId if available (most reliable)
- if (contract.supervisorTaskId) {
- const taskById = contract.tasks.find((t) => t.id === contract.supervisorTaskId);
- if (taskById) return taskById;
- }
- // Fallback to finding by isSupervisor flag
- return contract.tasks.find((t) => t.isSupervisor);
- }, [contract.tasks, contract.supervisorTaskId]);
-
- // Log for debugging
- useEffect(() => {
- console.log("Supervisor lookup:", {
- contractId: contract.id,
- supervisorTaskId: contract.supervisorTaskId,
- tasksCount: contract.tasks.length,
- foundSupervisor: supervisorTask ? { id: supervisorTask.id, status: supervisorTask.status, isSupervisor: supervisorTask.isSupervisor } : null,
- allTasks: contract.tasks.map(t => ({ id: t.id, name: t.name, isSupervisor: t.isSupervisor }))
- });
- }, [contract.id, contract.supervisorTaskId, contract.tasks, supervisorTask]);
-
- const supervisorTaskId = supervisorTask?.id ?? null;
- const supervisorStatus = supervisorTask?.status as TaskStatus | undefined;
- const isSupervisorRunning = supervisorStatus === "running";
- const isSupervisorPending = supervisorStatus === "pending";
-
- // Subscribe to supervisor output when it's running
- const handleSupervisorOutput = useCallback((event: TaskOutputEvent) => {
- // Check for question pattern in output
- // Pattern: {"__supervisor_question__": {"id": "...", "question": "...", "options": [...]}}
- if (!event.isPartial && event.content) {
- const questionMatch = event.content.match(/\{"__supervisor_question__":\s*(\{[^}]+\})\}/);
- if (questionMatch) {
- try {
- const questionData = JSON.parse(questionMatch[1]);
- if (questionData.id && questionData.question && questionData.options) {
- setSupervisorQuestion({
- id: questionData.id,
- question: questionData.question,
- options: questionData.options,
- allowMultiple: questionData.allowMultiple ?? false,
- allowCustom: questionData.allowCustom ?? true,
- });
- // Don't add this to output since it's a control message
- return;
- }
- } catch {
- // Not valid JSON, continue as normal output
- }
- }
- }
-
- setSupervisorOutput((prev) => {
- // If it's a partial message, update the last message
- if (event.isPartial && prev.length > 0) {
- const lastEvent = prev[prev.length - 1];
- if (lastEvent.messageType === event.messageType && lastEvent.isPartial) {
- return [...prev.slice(0, -1), { ...event, content: lastEvent.content + event.content }];
- }
- }
- return [...prev, event];
- });
- }, []);
-
- useTaskSubscription({
- taskId: supervisorTaskId,
- subscribeOutput: isSupervisorRunning,
- onOutput: handleSupervisorOutput,
- });
-
- // Auto-start supervisor function - starts and waits for it to be running
- const ensureSupervisorStarted = useCallback(async (): Promise<boolean> => {
- if (!supervisorTask) {
- console.warn("No supervisor task found for contract");
- return false;
- }
-
- if (isSupervisorRunning) {
- return true; // Already running
- }
-
- if (isSupervisorPending) {
- try {
- setSupervisorStarting(true);
- await startTask(supervisorTask.id);
-
- // Poll for the task to be running (up to 10 seconds)
- for (let i = 0; i < 20; i++) {
- await new Promise(resolve => setTimeout(resolve, 500));
- onUpdate(); // Refresh contract to get updated task status
- // Note: We can't check the new status here directly since state updates are async
- // The UI will update when onUpdate triggers a re-render
- }
-
- // Return true - the caller should check if supervisor is running after this
- return true;
- } catch (err) {
- console.error("Failed to start supervisor:", err);
- return false;
- } finally {
- setSupervisorStarting(false);
- }
- }
-
- // Supervisor exists but is in some other state (paused, done, failed, etc.)
- // Can still send messages to paused tasks
- return supervisorStatus === "paused";
- }, [supervisorTask, isSupervisorRunning, isSupervisorPending, supervisorStatus, onUpdate]);
-
- // Handle answering supervisor questions
- const [supervisorAnswers, setSupervisorAnswers] = useState<string[]>([]);
- const [supervisorCustomInput, setSupervisorCustomInput] = useState("");
-
- const handleSupervisorOptionToggle = useCallback((option: string) => {
- setSupervisorAnswers((prev) => {
- if (supervisorQuestion?.allowMultiple) {
- if (prev.includes(option)) {
- return prev.filter((a) => a !== option);
- }
- return [...prev, option];
- }
- return [option];
- });
- }, [supervisorQuestion?.allowMultiple]);
-
- const handleSubmitSupervisorAnswer = useCallback(async () => {
- if (!supervisorQuestion || !supervisorTask) return;
-
- const customAnswer = supervisorCustomInput.trim();
- const allAnswers = customAnswer
- ? [...supervisorAnswers, customAnswer]
- : supervisorAnswers;
-
- if (allAnswers.length === 0) return;
-
- // Format answer message for supervisor
- const answerMessage = `__supervisor_answer__ ${JSON.stringify({
- id: supervisorQuestion.id,
- answers: allAnswers,
- })}`;
-
- try {
- await sendTaskMessage(supervisorTask.id, answerMessage);
-
- // Add user message to chat
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- {
- id: userMsgId,
- type: "user",
- content: `[Answer to: ${supervisorQuestion.question}]\n${allAnswers.join(", ")}`,
- },
- ]);
- } catch (err) {
- console.error("Failed to send supervisor answer:", err);
- } finally {
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }
- }, [supervisorQuestion, supervisorTask, supervisorAnswers, supervisorCustomInput]);
-
- const handleCancelSupervisorQuestion = useCallback(() => {
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }, []);
-
- // Load chat history on mount
- useEffect(() => {
- let mounted = true;
-
- async function loadHistory() {
- try {
- const history = await getContractChatHistory(contractId);
- if (!mounted) return;
-
- // Convert saved messages to display messages
- const displayMessages: Message[] = history.messages.map((msg) => ({
- id: msg.id,
- type: msg.role as "user" | "assistant" | "error",
- content: msg.content,
- toolCalls: msg.toolCalls as { name: string; success: boolean; message: string }[] | undefined,
- }));
-
- setMessages(displayMessages);
-
- // Auto-expand if there's history
- if (displayMessages.length > 0) {
- setExpanded(true);
- }
- } catch (err) {
- console.error("Failed to load contract chat history:", err);
- } finally {
- if (mounted) {
- setHistoryLoading(false);
- }
- }
- }
-
- loadHistory();
-
- return () => {
- mounted = false;
- };
- }, [contractId]);
-
- // Auto-scroll to bottom when messages change
- useEffect(() => {
- if (messagesRef.current) {
- messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
- }
- }, [messages]);
-
- // Auto-start supervisor when component mounts if it's pending (but not for completed contracts)
- useEffect(() => {
- if (supervisorTask && isSupervisorPending && !supervisorStarting && contract.status !== 'completed') {
- console.log("Auto-starting supervisor task on mount...");
- ensureSupervisorStarted().then((started) => {
- if (started) {
- console.log("Supervisor started successfully");
- }
- });
- }
- }, [supervisorTask?.id, contract.status]); // Only run when task ID or contract status changes
-
- // Convert supervisor output events to messages
- useEffect(() => {
- if (supervisorOutput.length === 0) return;
-
- // Get the latest event
- const latestEvent = supervisorOutput[supervisorOutput.length - 1];
-
- // Only add complete messages (not partials) to the message history
- if (!latestEvent.isPartial && latestEvent.content.trim()) {
- const msgId = `supervisor-${Date.now()}`;
- let msgType: "assistant" | "error" = "assistant";
- let content = latestEvent.content;
-
- // Format based on message type
- switch (latestEvent.messageType) {
- case "assistant":
- content = latestEvent.content;
- break;
- case "tool_use":
- content = `_Using tool: ${latestEvent.toolName}_`;
- break;
- case "tool_result":
- content = latestEvent.isError
- ? `Tool error: ${latestEvent.content}`
- : `Tool result: ${latestEvent.content.slice(0, 200)}${latestEvent.content.length > 200 ? "..." : ""}`;
- msgType = latestEvent.isError ? "error" : "assistant";
- break;
- case "error":
- msgType = "error";
- break;
- case "result":
- // Final result - show cost info if available
- if (latestEvent.costUsd) {
- content = `${latestEvent.content}\n\n_Cost: $${latestEvent.costUsd.toFixed(4)}_`;
- }
- break;
- default:
- // system, raw, etc.
- break;
- }
-
- setMessages((prev) => {
- // Don't add duplicate messages
- if (prev.some((m) => m.content === content)) return prev;
- return [
- ...prev,
- { id: msgId, type: msgType, content },
- ];
- });
- }
- }, [supervisorOutput]);
-
- const handleSubmit = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- if (!input.trim() || loading) return;
-
- const userMessage = input.trim();
- setInput("");
- setExpanded(true);
-
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: userMessage },
- ]);
-
- setLoading(true);
-
- try {
- // Supervisor is the ONLY way to interact with contracts
- if (!supervisorTask) {
- throw new Error("No supervisor task found. Please create a contract with a supervisor.");
- }
-
- // Ensure supervisor is started (this will start it if pending)
- await ensureSupervisorStarted();
-
- // Send message to supervisor task stdin
- await sendTaskMessage(supervisorTask.id, userMessage);
-
- // Response will come through WebSocket subscription
- // No need for a placeholder message - output will stream in
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- inputRef.current?.focus();
- }
- },
- [input, loading, supervisorTask, ensureSupervisorStarted]
- );
-
- const handleOptionToggle = useCallback((questionId: string, option: string, allowMultiple: boolean) => {
- setUserAnswers((prev) => {
- const newMap = new Map(prev);
- const currentAnswers = newMap.get(questionId) || [];
-
- if (allowMultiple) {
- if (currentAnswers.includes(option)) {
- newMap.set(questionId, currentAnswers.filter((a) => a !== option));
- } else {
- newMap.set(questionId, [...currentAnswers, option]);
- }
- } else {
- newMap.set(questionId, [option]);
- }
-
- return newMap;
- });
- }, []);
-
- const handleCustomInputChange = useCallback((questionId: string, value: string) => {
- setCustomInputs((prev) => {
- const newMap = new Map(prev);
- newMap.set(questionId, value);
- return newMap;
- });
- }, []);
-
- const handleSubmitAnswers = useCallback(async () => {
- if (!pendingQuestions || loading) return;
-
- const answers = pendingQuestions.map((q) => {
- const selectedOptions = userAnswers.get(q.id) || [];
- const customInput = customInputs.get(q.id)?.trim();
- const finalAnswers = customInput
- ? [...selectedOptions, customInput]
- : selectedOptions;
-
- return {
- id: q.id,
- answers: finalAnswers,
- };
- });
-
- const answerText = answers
- .map((a) => {
- const question = pendingQuestions.find((q) => q.id === a.id);
- return `${question?.question || a.id}: ${a.answers.join(", ")}`;
- })
- .join("\n");
-
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
-
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: `[Answers]\n${answerText}` },
- ]);
-
- setLoading(true);
-
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, answerText);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- }
- }, [pendingQuestions, userAnswers, customInputs, loading, supervisorTask, ensureSupervisorStarted]);
-
- const handleCancelQuestions = useCallback(() => {
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- }, []);
-
- const clearMessages = useCallback(() => {
- setMessages([]);
- setPendingQuestions(null);
- setUserAnswers(new Map());
- setCustomInputs(new Map());
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- setSupervisorOutput([]);
- setSupervisorQuestion(null);
- setSupervisorAnswers([]);
- setSupervisorCustomInput("");
- }, []);
-
- // Handle creating tasks from the preview
- const handleCreateDerivedTasks = useCallback(
- async (selectedTasks: ParsedTask[]) => {
- if (selectedTasks.length === 0) {
- setParsedTasks(null);
- return;
- }
-
- setCreatingTasks(true);
-
- // Build a message asking the supervisor to create these tasks
- const taskList = selectedTasks
- .map((t, i) => `${i + 1}. ${t.name}${t.description ? `: ${t.description}` : ""}`)
- .join("\n");
-
- const message = `Create these ${selectedTasks.length} tasks as chained tasks:\n${taskList}`;
-
- // Add user message
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: message },
- ]);
-
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, message);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setCreatingTasks(false);
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- }
- },
- [supervisorTask, ensureSupervisorStarted]
- );
-
- const handleCancelTaskDerivation = useCallback(() => {
- setParsedTasks(null);
- setParsedTaskGroups([]);
- setParsedTasksFileName("");
- }, []);
-
- const handleQuickAction = useCallback(
- async (action: QuickAction) => {
- // Convert the action into a chat message that triggers the appropriate behavior
- let message = "";
- switch (action.type) {
- case "create_file":
- message = "Create the suggested file from the template.";
- break;
- case "create_task":
- message = "Yes, create the tasks.";
- break;
- case "derive_tasks":
- message = "Show me the tasks to review and create them.";
- break;
- case "run_task":
- message = "Run the next task.";
- break;
- case "advance_phase":
- if (action.data?.phase) {
- message = `Advance to the ${action.data.phase} phase.`;
- } else {
- message = "Advance to the next phase.";
- }
- break;
- case "update_file":
- message = "Update the file with the task output.";
- break;
- default:
- return;
- }
-
- setExpanded(true);
-
- // Submit the message
- const userMsgId = Date.now().toString();
- setMessages((prev) => [
- ...prev,
- { id: userMsgId, type: "user", content: message },
- ]);
-
- setLoading(true);
- try {
- if (!supervisorTask) {
- throw new Error("No supervisor task found");
- }
- await ensureSupervisorStarted();
- await sendTaskMessage(supervisorTask.id, message);
- // Response will come through WebSocket
- } catch (err) {
- const errorMsgId = (Date.now() + 1).toString();
- setMessages((prev) => [
- ...prev,
- {
- id: errorMsgId,
- type: "error",
- content: err instanceof Error ? err.message : "An error occurred",
- },
- ]);
- } finally {
- setLoading(false);
- }
- },
- [supervisorTask, ensureSupervisorStarted]
- );
-
- return (
- <div className="border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]">
- {/* Header bar with supervisor status and toggle */}
- <div className="px-3 py-2 flex items-center justify-between border-b border-[rgba(117,170,252,0.2)]">
- <div className="flex items-center gap-3">
- <span className="font-mono text-[10px] text-[#555] uppercase tracking-wide">
- Supervisor
- </span>
- {supervisorTask && (
- <span className={`font-mono text-[10px] px-2 py-0.5 border ${
- isSupervisorRunning
- ? "text-green-400 border-green-400/30 bg-green-400/10"
- : isSupervisorPending || supervisorStarting
- ? "text-yellow-400 border-yellow-400/30 bg-yellow-400/10"
- : "text-[#555] border-[rgba(117,170,252,0.2)]"
- }`}>
- {supervisorStarting ? "Starting..." : isSupervisorRunning ? "Running" : supervisorStatus || "Unknown"}
- </span>
- )}
- {!supervisorTask && (
- <span className="font-mono text-[10px] text-red-400">
- No supervisor
- </span>
- )}
- </div>
- {messages.length > 0 && (
- <button
- type="button"
- onClick={() => setExpanded(!expanded)}
- className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- {expanded ? "Hide Messages" : `Show Messages (${messages.length})`}
- </button>
- )}
- </div>
-
- {/* History loading indicator */}
- {historyLoading && (
- <div className="px-3 py-2 text-[10px] font-mono text-[#555] flex items-center gap-2 border-b border-[rgba(117,170,252,0.2)]">
- <span className="animate-pulse">Loading history...</span>
- </div>
- )}
-
- {/* Messages Panel (expandable) */}
- {expanded && messages.length > 0 && !historyLoading && (
- <div className="relative border-b border-[rgba(117,170,252,0.2)]">
- {/* Expand/Collapse button */}
- <div className="absolute top-2 right-2 z-10 flex gap-1">
- <button
- type="button"
- onClick={() => setFullscreen(!fullscreen)}
- className="px-2 py-1 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] transition-colors"
- title={fullscreen ? "Collapse" : "Expand"}
- >
- {fullscreen ? "▼ Collapse" : "▲ Expand"}
- </button>
- </div>
- <div
- ref={messagesRef}
- className={`overflow-y-auto p-3 pr-24 space-y-2 transition-all duration-200 ${
- fullscreen ? "max-h-[60vh]" : "max-h-48"
- }`}
- >
- {messages.map((msg) => (
- <div key={msg.id} className="font-mono text-xs">
- {msg.type === "user" && (
- <div className="flex gap-2">
- <span className="text-[#9bc3ff]">&gt;</span>
- <span className="text-white/80 whitespace-pre-wrap">{msg.content}</span>
- </div>
- )}
- {(msg.type === "assistant" || msg.type === "question") && (
- <div className="pl-4 space-y-1">
- <SimpleMarkdown content={msg.content} className="text-[#75aafc]" />
- {msg.toolCalls && msg.toolCalls.length > 0 && (
- <div className="text-[#555] text-[10px] space-y-0.5">
- {msg.toolCalls.map((tc, i) => (
- <div key={i}>
- <span
- className={
- tc.success ? "text-green-500" : "text-red-400"
- }
- >
- {tc.success ? "+" : "x"}
- </span>{" "}
- {tc.name}: {tc.message}
- </div>
- ))}
- </div>
- )}
- {msg.quickActions && msg.quickActions.length > 0 && (
- <QuickActionButtons
- actions={msg.quickActions}
- onAction={handleQuickAction}
- loading={loading}
- />
- )}
- </div>
- )}
- {msg.type === "error" && (
- <div className="pl-4 text-red-400">{msg.content}</div>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
-
- {/* Pending Questions UI */}
- {pendingQuestions && pendingQuestions.length > 0 && (
- <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3">
- <div className="text-[#9bc3ff] font-mono text-xs uppercase tracking-wide">
- Questions from AI
- </div>
- {pendingQuestions.map((q) => (
- <div key={q.id} className="space-y-2">
- <div className="text-white/90 font-mono text-sm">{q.question}</div>
- <div className="flex flex-wrap gap-2">
- {q.options.map((option) => {
- const isSelected = (userAnswers.get(q.id) || []).includes(option);
- return (
- <button
- key={option}
- type="button"
- onClick={() => handleOptionToggle(q.id, option, q.allowMultiple)}
- className={`px-2 py-1 font-mono text-xs border transition-colors ${
- isSelected
- ? "bg-[#3f6fb3] border-[#75aafc] text-white"
- : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-[#3f6fb3]"
- }`}
- >
- {q.allowMultiple && (
- <span className="mr-1">{isSelected ? "[x]" : "[ ]"}</span>
- )}
- {option}
- </button>
- );
- })}
- </div>
- {q.allowCustom && (
- <input
- type="text"
- value={customInputs.get(q.id) || ""}
- onChange={(e) => handleCustomInputChange(q.id, e.target.value)}
- placeholder="Or type a custom answer..."
- className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] placeholder-[#555]"
- />
- )}
- </div>
- ))}
- <div className="flex gap-2 pt-2">
- <button
- type="button"
- onClick={handleSubmitAnswers}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Submit Answers"}
- </button>
- <button
- type="button"
- onClick={handleCancelQuestions}
- disabled={loading}
- className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
-
- {/* Supervisor Question UI */}
- {supervisorQuestion && (
- <div className="p-3 border-b border-[rgba(117,170,252,0.2)] space-y-3 bg-[rgba(117,170,252,0.05)]">
- <div className="text-green-400 font-mono text-xs uppercase tracking-wide flex items-center gap-2">
- <span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
- Question from Supervisor
- </div>
- <div className="text-white/90 font-mono text-sm">{supervisorQuestion.question}</div>
- <div className="flex flex-wrap gap-2">
- {supervisorQuestion.options.map((option) => {
- const isSelected = supervisorAnswers.includes(option);
- return (
- <button
- key={option}
- type="button"
- onClick={() => handleSupervisorOptionToggle(option)}
- className={`px-2 py-1 font-mono text-xs border transition-colors ${
- isSelected
- ? "bg-green-500/30 border-green-400 text-white"
- : "bg-transparent border-[rgba(117,170,252,0.25)] text-[#9bc3ff] hover:border-green-400"
- }`}
- >
- {supervisorQuestion.allowMultiple && (
- <span className="mr-1">{isSelected ? "[x]" : "[ ]"}</span>
- )}
- {option}
- </button>
- );
- })}
- </div>
- {supervisorQuestion.allowCustom && (
- <input
- type="text"
- value={supervisorCustomInput}
- onChange={(e) => setSupervisorCustomInput(e.target.value)}
- placeholder="Or type a custom answer..."
- className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-xs px-2 py-1 outline-none focus:border-green-400 placeholder-[#555]"
- />
- )}
- <div className="flex gap-2 pt-2">
- <button
- type="button"
- onClick={handleSubmitSupervisorAnswer}
- disabled={supervisorAnswers.length === 0 && !supervisorCustomInput.trim()}
- className="px-3 py-1 font-mono text-xs text-green-400 border border-green-400/50 hover:border-green-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- Send Answer
- </button>
- <button
- type="button"
- onClick={handleCancelSupervisorQuestion}
- className="px-3 py-1 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Dismiss
- </button>
- </div>
- </div>
- )}
-
- {/* Contract Context Badge */}
- <div className="px-3 pt-2 pb-1 flex items-center gap-2 text-[10px] font-mono text-[#555]">
- <span className="text-[#75aafc]">{contract.phase}</span>
- <span>|</span>
- {supervisorTask && (
- <>
- <span
- className={
- isSupervisorRunning
- ? "text-green-400"
- : isSupervisorPending
- ? "text-yellow-400"
- : supervisorStarting
- ? "text-cyan-400 animate-pulse"
- : "text-[#555]"
- }
- >
- Supervisor: {supervisorStarting ? "starting..." : supervisorTask.status}
- </span>
- <span>|</span>
- </>
- )}
- <span>{contract.files.length} files</span>
- <span>|</span>
- <span>{contract.tasks.length} tasks</span>
- <span>|</span>
- <span>{contract.repositories.length} repos</span>
- <span>|</span>
- <button
- type="button"
- onClick={() => {
- const prompt = "Guide me to complete this phase and advance to the next. Analyze my current deliverables, identify what's missing, and suggest specific next steps.";
- setInput(prompt);
- // Auto-submit the prompt
- setTimeout(() => {
- const form = document.querySelector('form');
- if (form) form.requestSubmit();
- }, 0);
- }}
- disabled={loading || !!pendingQuestions}
- className="text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50 transition-colors cursor-pointer"
- >
- Progress →
- </button>
- {messages.length > 0 && (
- <>
- <span>|</span>
- <button
- type="button"
- onClick={async () => {
- if (window.confirm("Clear all chat history for this contract?")) {
- try {
- await clearContractChatHistory(contractId);
- setMessages([]);
- } catch (err) {
- console.error("Failed to clear history:", err);
- }
- }
- }}
- disabled={loading}
- className="text-[#555] hover:text-red-400 disabled:opacity-50 transition-colors cursor-pointer"
- >
- Clear
- </button>
- </>
- )}
- </div>
-
- {/* Input Bar */}
- <form onSubmit={handleSubmit} className="flex items-center gap-2 px-3 pb-3">
- <span className="text-[#9bc3ff] font-mono text-sm">&gt;</span>
- <input
- ref={inputRef}
- type="text"
- value={input}
- onChange={(e) => setInput(e.target.value)}
- placeholder={
- loading
- ? "Processing..."
- : supervisorStarting
- ? "Starting supervisor..."
- : supervisorQuestion
- ? "Answer supervisor question above..."
- : pendingQuestions
- ? "Answer questions above first..."
- : isSupervisorRunning
- ? "Message supervisor..."
- : "Create a task, add a file, or ask about the contract..."
- }
- disabled={loading || !!pendingQuestions || !!supervisorQuestion}
- className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]"
- />
- {messages.length > 0 && (
- <button
- type="button"
- onClick={clearMessages}
- className="text-[#555] hover:text-[#9bc3ff] font-mono text-xs transition-colors"
- >
- clear
- </button>
- )}
- <button
- type="submit"
- disabled={loading || !input.trim() || !!pendingQuestions || !!supervisorQuestion}
- className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
- >
- {loading ? "..." : "Send"}
- </button>
- </form>
-
- {/* Task Derivation Preview Modal */}
- {parsedTasks && parsedTasks.length > 0 && (
- <TaskDerivationPreview
- tasks={parsedTasks}
- groups={parsedTaskGroups}
- fileName={parsedTasksFileName}
- onCreateTasks={handleCreateDerivedTasks}
- onCancel={handleCancelTaskDerivation}
- loading={creatingTasks}
- />
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/ContractContextMenu.tsx b/makima/frontend/src/components/contracts/ContractContextMenu.tsx
deleted file mode 100644
index f31beb5..0000000
--- a/makima/frontend/src/components/contracts/ContractContextMenu.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-import { useEffect, useRef } from "react";
-import type { ContractSummary } from "../../lib/api";
-
-interface ContractContextMenuProps {
- x: number;
- y: number;
- contract: ContractSummary;
- onClose: () => void;
- onMarkComplete: () => void;
- onMarkActive: () => void;
- onArchive: () => void;
- onDelete: () => void;
- onGoToSupervisor: () => void;
-}
-
-export function ContractContextMenu({
- x,
- y,
- contract,
- onClose,
- onMarkComplete,
- onMarkActive,
- onArchive,
- onDelete,
- onGoToSupervisor,
-}: ContractContextMenuProps) {
- const menuRef = useRef<HTMLDivElement>(null);
-
- // Close on click outside
- useEffect(() => {
- const handleClickOutside = (e: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
- onClose();
- }
- };
-
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- onClose();
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- document.addEventListener("keydown", handleKeyDown);
-
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- document.removeEventListener("keydown", handleKeyDown);
- };
- }, [onClose]);
-
- // Adjust position if menu would overflow viewport
- useEffect(() => {
- if (menuRef.current) {
- const rect = menuRef.current.getBoundingClientRect();
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- if (rect.right > viewportWidth) {
- menuRef.current.style.left = `${x - rect.width}px`;
- }
- if (rect.bottom > viewportHeight) {
- menuRef.current.style.top = `${y - rect.height}px`;
- }
- }
- }, [x, y]);
-
- const menuItemClass =
- "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2";
- const dividerClass = "border-t border-[rgba(117,170,252,0.2)] my-1";
-
- const showMarkComplete = contract.status !== "completed";
- const showMarkActive = contract.status !== "active";
- const showArchive = contract.status !== "archived";
- const showGoToSupervisor = !!contract.supervisorTaskId;
-
- return (
- <div
- ref={menuRef}
- className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]"
- style={{ left: x, top: y }}
- >
- {/* Header showing contract name */}
- <div className="px-3 py-1.5 text-[10px] font-mono text-[#555] uppercase border-b border-[rgba(117,170,252,0.2)] truncate max-w-[200px]">
- {contract.name}
- </div>
-
- {/* Status actions */}
- {showMarkComplete && (
- <button
- className={menuItemClass}
- onClick={() => {
- onMarkComplete();
- onClose();
- }}
- >
- <span className="text-[#75aafc]">✓</span>
- Mark as Complete
- </button>
- )}
-
- {showMarkActive && (
- <button
- className={menuItemClass}
- onClick={() => {
- onMarkActive();
- onClose();
- }}
- >
- <span className="text-[#75aafc]">●</span>
- Mark as Active
- </button>
- )}
-
- {showArchive && (
- <button
- className={menuItemClass}
- onClick={() => {
- onArchive();
- onClose();
- }}
- >
- <span className="text-[#75aafc]">▣</span>
- Archive
- </button>
- )}
-
- {/* Supervisor link */}
- {showGoToSupervisor && (
- <>
- <div className={dividerClass} />
- <button
- className={menuItemClass}
- onClick={() => {
- onGoToSupervisor();
- onClose();
- }}
- >
- <span className="text-[#75aafc]">▶</span>
- Go to Supervisor Task
- </button>
- </>
- )}
-
- <div className={dividerClass} />
-
- {/* Delete action */}
- <button
- className={`${menuItemClass} text-red-400 hover:bg-red-400/10`}
- onClick={() => {
- onDelete();
- onClose();
- }}
- >
- <span className="text-red-400">✕</span>
- Delete
- </button>
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx
deleted file mode 100644
index 02c129e..0000000
--- a/makima/frontend/src/components/contracts/ContractDetail.tsx
+++ /dev/null
@@ -1,810 +0,0 @@
-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"
- >
- &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>
- {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}
- contractType={contract.contractType}
- 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>
- );
-}
diff --git a/makima/frontend/src/components/contracts/ContractList.tsx b/makima/frontend/src/components/contracts/ContractList.tsx
deleted file mode 100644
index 1eee6a3..0000000
--- a/makima/frontend/src/components/contracts/ContractList.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import { useState } from "react";
-import type { ContractSummary, ContractStatus } from "../../lib/api";
-import { PhaseBadge } from "./PhaseBadge";
-import { PhaseProgressBarCompact } from "./PhaseProgressBar";
-import { ContractContextMenu } from "./ContractContextMenu";
-
-interface ContractListProps {
- contracts: ContractSummary[];
- loading: boolean;
- onSelect: (id: string) => void;
- onCreate: () => void;
- selectedId?: string;
- onMarkComplete?: (contract: ContractSummary) => void;
- onMarkActive?: (contract: ContractSummary) => void;
- onArchive?: (contract: ContractSummary) => void;
- onDelete?: (contract: ContractSummary) => void;
- onGoToSupervisor?: (contract: ContractSummary) => void;
-}
-
-const statusColors: Record<ContractStatus, string> = {
- active: "text-green-400",
- completed: "text-blue-400",
- archived: "text-[#555]",
-};
-
-export function ContractList({
- contracts,
- loading,
- onSelect,
- onCreate,
- selectedId,
- onMarkComplete,
- onMarkActive,
- onArchive,
- onDelete,
- onGoToSupervisor,
-}: ContractListProps) {
- const [filter, setFilter] = useState<ContractStatus | "all">("all");
- const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
- const [contextMenuContract, setContextMenuContract] = useState<ContractSummary | null>(null);
-
- const handleContextMenu = (e: React.MouseEvent, contract: ContractSummary) => {
- e.preventDefault();
- setContextMenuPosition({ x: e.clientX, y: e.clientY });
- setContextMenuContract(contract);
- };
-
- const closeContextMenu = () => {
- setContextMenuPosition(null);
- setContextMenuContract(null);
- };
-
- const filteredContracts =
- filter === "all"
- ? contracts
- : contracts.filter((c) => c.status === filter);
-
- if (loading) {
- return (
- <div className="panel h-full flex items-center justify-center">
- <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
- </div>
- );
- }
-
- 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">
- <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider">
- Contracts
- </h2>
- <button
- onClick={onCreate}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- + New
- </button>
- </div>
-
- {/* Filter tabs */}
- <div className="flex gap-1">
- {(["all", "active", "completed", "archived"] as const).map((status) => (
- <button
- key={status}
- onClick={() => setFilter(status)}
- className={`
- px-2 py-1 font-mono text-[10px] uppercase tracking-wider transition-colors
- ${
- filter === status
- ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]"
- : "text-[#555] hover:text-[#75aafc]"
- }
- `}
- >
- {status}
- </button>
- ))}
- </div>
- </div>
-
- {/* Contract list */}
- <div className="flex-1 min-h-0 overflow-y-auto">
- {filteredContracts.length === 0 ? (
- <div className="p-4 text-center">
- <p className="font-mono text-sm text-[#555]">
- {filter === "all"
- ? "No contracts yet"
- : `No ${filter} contracts`}
- </p>
- </div>
- ) : (
- <div className="divide-y divide-[rgba(117,170,252,0.15)]">
- {filteredContracts.map((contract) => (
- <button
- key={contract.id}
- onClick={() => onSelect(contract.id)}
- onContextMenu={(e) => handleContextMenu(e, contract)}
- className={`
- w-full text-left p-4 transition-colors
- ${
- selectedId === contract.id
- ? "bg-[rgba(117,170,252,0.1)]"
- : "hover:bg-[rgba(117,170,252,0.05)]"
- }
- `}
- >
- <div className="flex items-start justify-between gap-2 mb-2">
- <div className="flex items-center gap-2 min-w-0">
- <h3 className="font-mono text-sm text-[#dbe7ff] truncate">
- {contract.name}
- </h3>
- {contract.localOnly && (
- <span className="px-1.5 py-0.5 font-mono text-[9px] uppercase text-amber-400 border border-amber-400/30 bg-amber-400/10 shrink-0">
- Local
- </span>
- )}
- </div>
- <span
- className={`text-[10px] font-mono uppercase shrink-0 ${
- statusColors[contract.status]
- }`}
- >
- {contract.status}
- </span>
- </div>
-
- {contract.description && (
- <p className="font-mono text-xs text-[#555] mb-2 line-clamp-2">
- {contract.description}
- </p>
- )}
-
- <div className="flex items-center justify-between">
- <PhaseProgressBarCompact currentPhase={contract.phase} contractType={contract.contractType} />
- <div className="flex items-center gap-3 text-[10px] font-mono text-[#555]">
- {contract.fileCount > 0 && (
- <span>{contract.fileCount} files</span>
- )}
- {contract.taskCount > 0 && (
- <span>{contract.taskCount} tasks</span>
- )}
- {contract.repositoryCount > 0 && (
- <span>{contract.repositoryCount} repos</span>
- )}
- </div>
- </div>
- </button>
- ))}
- </div>
- )}
- </div>
-
- {/* Context Menu */}
- {contextMenuPosition && contextMenuContract && (
- <ContractContextMenu
- x={contextMenuPosition.x}
- y={contextMenuPosition.y}
- contract={contextMenuContract}
- onClose={closeContextMenu}
- onMarkComplete={() => onMarkComplete?.(contextMenuContract)}
- onMarkActive={() => onMarkActive?.(contextMenuContract)}
- onArchive={() => onArchive?.(contextMenuContract)}
- onDelete={() => onDelete?.(contextMenuContract)}
- onGoToSupervisor={() => onGoToSupervisor?.(contextMenuContract)}
- />
- )}
- </div>
- );
-}
-
-export function ContractCard({
- contract,
- onClick,
-}: {
- contract: ContractSummary;
- onClick: () => void;
-}) {
- return (
- <button
- onClick={onClick}
- className="w-full text-left p-4 border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] transition-colors"
- >
- <div className="flex items-start justify-between gap-2 mb-2">
- <h3 className="font-mono text-sm text-[#dbe7ff]">{contract.name}</h3>
- <PhaseBadge phase={contract.phase} />
- </div>
-
- {contract.description && (
- <p className="font-mono text-xs text-[#555] mb-3 line-clamp-2">
- {contract.description}
- </p>
- )}
-
- <div className="flex items-center gap-4 text-[10px] font-mono text-[#555]">
- <span>{contract.fileCount} files</span>
- <span>{contract.taskCount} tasks</span>
- <span>{contract.repositoryCount} repos</span>
- </div>
- </button>
- );
-}
diff --git a/makima/frontend/src/components/contracts/PhaseBadge.tsx b/makima/frontend/src/components/contracts/PhaseBadge.tsx
deleted file mode 100644
index 0f46b9b..0000000
--- a/makima/frontend/src/components/contracts/PhaseBadge.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { ContractPhase } from "../../lib/api";
-
-interface PhaseBadgeProps {
- phase: ContractPhase;
- size?: "sm" | "md";
-}
-
-const phaseConfig: Record<
- ContractPhase,
- { label: string; color: string; bgColor: string }
-> = {
- research: {
- label: "Research",
- color: "text-purple-400",
- bgColor: "bg-purple-400/10 border-purple-400/30",
- },
- specify: {
- label: "Specify",
- color: "text-blue-400",
- bgColor: "bg-blue-400/10 border-blue-400/30",
- },
- plan: {
- label: "Plan",
- color: "text-cyan-400",
- bgColor: "bg-cyan-400/10 border-cyan-400/30",
- },
- execute: {
- label: "Execute",
- color: "text-yellow-400",
- bgColor: "bg-yellow-400/10 border-yellow-400/30",
- },
- review: {
- label: "Review",
- color: "text-green-400",
- bgColor: "bg-green-400/10 border-green-400/30",
- },
-};
-
-export function PhaseBadge({ phase, size = "sm" }: PhaseBadgeProps) {
- const config = phaseConfig[phase];
- const sizeClasses = size === "sm" ? "px-2 py-0.5 text-[10px]" : "px-3 py-1 text-xs";
-
- return (
- <span
- className={`${sizeClasses} ${config.color} ${config.bgColor} border font-mono uppercase tracking-wider`}
- >
- {config.label}
- </span>
- );
-}
-
-export function getPhaseLabel(phase: ContractPhase): string {
- return phaseConfig[phase].label;
-}
diff --git a/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx b/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
deleted file mode 100644
index b2c2e58..0000000
--- a/makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
+++ /dev/null
@@ -1,339 +0,0 @@
-import { useMemo } from "react";
-import type { ContractWithRelations, ContractPhase, ContractType } from "../../lib/api";
-
-// Phase deliverables configuration (mirrors backend phase_guidance.rs)
-// IDs must match backend phase_guidance.rs exactly for mark_deliverable_complete
-interface PhaseDeliverable {
- id: string; // Must match backend deliverable ID
- name: string;
- priority: "required" | "recommended" | "optional";
- description: string;
-}
-
-interface PhaseConfig {
- deliverables: PhaseDeliverable[];
- requiresRepository: boolean;
- requiresTasks: boolean;
- guidance: string;
-}
-
-// Contract type specific deliverables (must match backend phase_guidance.rs)
-type ContractTypeDeliverables = Partial<Record<ContractPhase, PhaseConfig>>;
-
-const CONTRACT_TYPE_DELIVERABLES: Record<ContractType, ContractTypeDeliverables> = {
- simple: {
- plan: {
- deliverables: [
- { id: "plan-document", name: "Plan", priority: "required", description: "Implementation plan detailing the approach and tasks" },
- ],
- requiresRepository: true,
- requiresTasks: false,
- guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.",
- },
- execute: {
- deliverables: [
- { id: "pull-request", name: "Pull Request", priority: "required", description: "Pull request with the implemented changes" },
- ],
- requiresRepository: true,
- requiresTasks: true,
- guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.",
- },
- },
- specification: {
- research: {
- deliverables: [
- { id: "research-notes", name: "Research Notes", priority: "required", description: "Document findings and insights during research" },
- ],
- requiresRepository: false,
- requiresTasks: false,
- guidance: "Focus on understanding the problem space and document your findings in the Research Notes before moving to Specify phase.",
- },
- specify: {
- deliverables: [
- { id: "requirements-document", name: "Requirements Document", priority: "required", description: "Define functional and non-functional requirements" },
- ],
- requiresRepository: false,
- requiresTasks: false,
- guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.",
- },
- plan: {
- deliverables: [
- { id: "plan-document", name: "Plan", priority: "required", description: "Implementation plan detailing the approach and tasks" },
- ],
- requiresRepository: true,
- requiresTasks: false,
- guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.",
- },
- execute: {
- deliverables: [
- { id: "pull-request", name: "Pull Request", priority: "required", description: "Pull request with the implemented changes" },
- ],
- requiresRepository: true,
- requiresTasks: true,
- guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks before moving to Review phase.",
- },
- review: {
- deliverables: [
- { id: "release-notes", name: "Release Notes", priority: "required", description: "Document changes for release communication" },
- ],
- requiresRepository: false,
- requiresTasks: false,
- guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.",
- },
- },
- execute: {
- execute: {
- deliverables: [], // No deliverables for execute-only contract type
- requiresRepository: true,
- requiresTasks: true,
- guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.",
- },
- },
-};
-
-// Get phase config for a specific contract type and phase
-function getPhaseConfig(contractType: ContractType, phase: ContractPhase): PhaseConfig {
- const typeConfig = CONTRACT_TYPE_DELIVERABLES[contractType];
- const phaseConfig = typeConfig?.[phase];
-
- if (phaseConfig) {
- return phaseConfig;
- }
-
- // Fallback for unknown phase/type combinations
- return {
- deliverables: [],
- requiresRepository: false,
- requiresTasks: false,
- guidance: `Unknown phase "${phase}" for contract type "${contractType}"`,
- };
-}
-
-interface DeliverableStatus {
- id: string;
- name: string;
- priority: "required" | "recommended" | "optional";
- description: string;
- completed: boolean;
- fileId?: string;
- actualName?: string;
-}
-
-interface PhaseDeliverablesProps {
- contract: ContractWithRelations;
- onCreateFile?: (templateId: string, suggestedName: string) => void;
-}
-
-export function PhaseDeliverablesPanel({ contract, onCreateFile }: PhaseDeliverablesProps) {
- // Get phase config based on contract type AND phase
- const phaseConfig = useMemo(
- () => getPhaseConfig(contract.contractType, contract.phase),
- [contract.contractType, contract.phase]
- );
-
- // Calculate deliverable status
- const deliverableStatuses = useMemo((): DeliverableStatus[] => {
- return phaseConfig.deliverables.map((deliverable) => {
- // Find matching file by name similarity
- const matchedFile = contract.files.find((f) => {
- const nameLower = f.name.toLowerCase();
- const deliverableLower = deliverable.name.toLowerCase();
- return (
- f.contractPhase === contract.phase &&
- (nameLower.includes(deliverableLower) || deliverableLower.includes(nameLower) || nameLower.includes(deliverable.id.replace("-", " ")))
- );
- });
-
- return {
- ...deliverable,
- completed: !!matchedFile,
- fileId: matchedFile?.id,
- actualName: matchedFile?.name,
- };
- });
- }, [contract.files, contract.phase, phaseConfig.deliverables]);
-
- // Check repository status
- const hasRepository = contract.repositories.length > 0;
-
- // Check task status
- const taskStats = useMemo(() => {
- const total = contract.tasks.length;
- const done = contract.tasks.filter((t) => t.status === "done" || t.status === "merged").length;
- const pending = contract.tasks.filter((t) => t.status === "pending").length;
- const running = contract.tasks.filter((t) => ["running", "initializing", "starting"].includes(t.status)).length;
- const failed = contract.tasks.filter((t) => t.status === "failed").length;
- return { total, done, pending, running, failed };
- }, [contract.tasks]);
-
- // Calculate completion percentage
- const completionPercent = useMemo(() => {
- let completed = 0;
- let total = 0;
-
- // Count required and recommended deliverables
- deliverableStatuses.forEach((s) => {
- if (s.priority !== "optional") {
- total++;
- if (s.completed) completed++;
- }
- });
-
- // Count repository if required
- if (phaseConfig.requiresRepository) {
- total++;
- if (hasRepository) completed++;
- }
-
- // Count tasks if required
- if (phaseConfig.requiresTasks && taskStats.total > 0) {
- total++;
- if (taskStats.done === taskStats.total) completed++;
- }
-
- return total > 0 ? Math.round((completed / total) * 100) : 100;
- }, [deliverableStatuses, hasRepository, phaseConfig, taskStats]);
-
- const priorityColors = {
- required: "text-red-400",
- recommended: "text-yellow-400",
- optional: "text-[#555]",
- };
-
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <h3 className="font-mono text-xs text-[#75aafc] uppercase">
- Phase Deliverables
- </h3>
- <div className="flex items-center gap-2">
- <div className="w-24 h-1.5 bg-[rgba(117,170,252,0.1)] rounded overflow-hidden">
- <div
- className={`h-full transition-all duration-300 ${
- completionPercent === 100 ? "bg-green-400" : "bg-[#75aafc]"
- }`}
- style={{ width: `${completionPercent}%` }}
- />
- </div>
- <span className="font-mono text-[10px] text-[#555]">{completionPercent}%</span>
- </div>
- </div>
-
- {/* Guidance text */}
- <p className="font-mono text-xs text-[#555] italic">{phaseConfig.guidance}</p>
-
- {/* Deliverables checklist */}
- <div className="space-y-2">
- {deliverableStatuses.map((status) => (
- <div
- key={status.id}
- className={`flex items-center justify-between p-2 border ${
- status.completed
- ? "border-green-400/20 bg-green-400/5"
- : "border-[rgba(117,170,252,0.15)]"
- }`}
- >
- <div className="flex items-center gap-2">
- <span
- className={`font-mono text-xs ${
- status.completed ? "text-green-400" : "text-[#555]"
- }`}
- >
- {status.completed ? "[+]" : "[ ]"}
- </span>
- <div>
- <div className="flex items-center gap-2">
- <span className="font-mono text-xs text-[#dbe7ff]">
- {status.completed ? status.actualName : status.name}
- </span>
- {!status.completed && (
- <span className={`font-mono text-[9px] uppercase ${priorityColors[status.priority]}`}>
- {status.priority}
- </span>
- )}
- </div>
- <span className="font-mono text-[10px] text-[#555]">
- {status.description}
- </span>
- </div>
- </div>
- {!status.completed && onCreateFile && (
- <button
- onClick={() => onCreateFile(status.id, status.name)}
- className="px-2 py-1 font-mono text-[10px] text-[#75aafc] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
- >
- Create
- </button>
- )}
- </div>
- ))}
- </div>
-
- {/* Repository status */}
- {phaseConfig.requiresRepository && (
- <div
- className={`flex items-center gap-2 p-2 border ${
- hasRepository
- ? "border-green-400/20 bg-green-400/5"
- : "border-[rgba(117,170,252,0.15)]"
- }`}
- >
- <span
- className={`font-mono text-xs ${
- hasRepository ? "text-green-400" : "text-[#555]"
- }`}
- >
- {hasRepository ? "[+]" : "[ ]"}
- </span>
- <div>
- <span className="font-mono text-xs text-[#dbe7ff]">
- Repository Configured
- </span>
- {!hasRepository && (
- <span className="font-mono text-[9px] uppercase text-red-400 ml-2">
- required
- </span>
- )}
- </div>
- </div>
- )}
-
- {/* Task status */}
- {phaseConfig.requiresTasks && (
- <div
- className={`flex items-center justify-between p-2 border ${
- taskStats.total > 0 && taskStats.done === taskStats.total
- ? "border-green-400/20 bg-green-400/5"
- : "border-[rgba(117,170,252,0.15)]"
- }`}
- >
- <div className="flex items-center gap-2">
- <span
- className={`font-mono text-xs ${
- taskStats.total > 0 && taskStats.done === taskStats.total
- ? "text-green-400"
- : "text-[#555]"
- }`}
- >
- {taskStats.total > 0 && taskStats.done === taskStats.total ? "[+]" : "[ ]"}
- </span>
- <span className="font-mono text-xs text-[#dbe7ff]">
- Tasks Completed
- </span>
- </div>
- {taskStats.total > 0 ? (
- <span className="font-mono text-[10px] text-[#9bc3ff]">
- {taskStats.done}/{taskStats.total}
- {taskStats.running > 0 && ` (${taskStats.running} running)`}
- {taskStats.failed > 0 && (
- <span className="text-red-400"> ({taskStats.failed} failed)</span>
- )}
- </span>
- ) : (
- <span className="font-mono text-[10px] text-[#555]">No tasks yet</span>
- )}
- </div>
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/PhaseHint.tsx b/makima/frontend/src/components/contracts/PhaseHint.tsx
deleted file mode 100644
index 95573ed..0000000
--- a/makima/frontend/src/components/contracts/PhaseHint.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import type { ContractPhase, ContractWithRelations } from "../../lib/api";
-
-interface PhaseHintProps {
- contract: ContractWithRelations;
- onAdvancePhase: (phase: ContractPhase) => void;
-}
-
-const phaseOrder: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
-
-interface HintConfig {
- condition: (contract: ContractWithRelations) => boolean;
- message: (contract: ContractWithRelations) => string;
- nextPhase: ContractPhase;
-}
-
-const phaseHints: Record<ContractPhase, HintConfig | null> = {
- research: {
- condition: (c) => c.files.length >= 1,
- message: (c) =>
- `You have ${c.files.length} file${c.files.length === 1 ? "" : "s"}. Ready to specify requirements?`,
- nextPhase: "specify",
- },
- specify: {
- condition: (c) => c.files.length >= 2,
- message: () => "Spec files ready. Create implementation plan?",
- nextPhase: "plan",
- },
- plan: {
- condition: (c) => c.files.length >= 1 && c.repositories.length >= 1,
- message: () => "Plan documented. Ready to create tasks?",
- nextPhase: "execute",
- },
- execute: {
- condition: (c) => {
- // Show hint only when all tasks are complete
- const doneTasks = c.tasks.filter(
- (t) => t.status === "done" || t.status === "merged"
- );
- return c.tasks.length > 0 && doneTasks.length === c.tasks.length;
- },
- message: (c) => {
- const doneTasks = c.tasks.filter(
- (t) => t.status === "done" || t.status === "merged"
- );
- return `All ${doneTasks.length} task${doneTasks.length === 1 ? "" : "s"} complete. Ready for review?`;
- },
- nextPhase: "review",
- },
- review: null, // No hint for review phase - it's the final phase
-};
-
-export function PhaseHint({ contract, onAdvancePhase }: PhaseHintProps) {
- const hintConfig = phaseHints[contract.phase];
-
- // No hint for this phase
- if (!hintConfig) return null;
-
- // Condition not met
- if (!hintConfig.condition(contract)) return null;
-
- return (
- <div className="flex items-center gap-3 p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.2)]">
- <span className="font-mono text-xs text-[#9bc3ff]">
- {hintConfig.message(contract)}
- </span>
- <button
- onClick={() => onAdvancePhase(hintConfig.nextPhase)}
- className="px-3 py-1 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase whitespace-nowrap"
- >
- Advance to {hintConfig.nextPhase}
- </button>
- </div>
- );
-}
-
-export function getNextPhase(currentPhase: ContractPhase): ContractPhase | null {
- const currentIndex = phaseOrder.indexOf(currentPhase);
- if (currentIndex === -1 || currentIndex === phaseOrder.length - 1) {
- return null;
- }
- return phaseOrder[currentIndex + 1];
-}
-
-export function getPreviousPhase(currentPhase: ContractPhase): ContractPhase | null {
- const currentIndex = phaseOrder.indexOf(currentPhase);
- if (currentIndex <= 0) {
- return null;
- }
- return phaseOrder[currentIndex - 1];
-}
diff --git a/makima/frontend/src/components/contracts/PhaseProgressBar.tsx b/makima/frontend/src/components/contracts/PhaseProgressBar.tsx
deleted file mode 100644
index 9589db9..0000000
--- a/makima/frontend/src/components/contracts/PhaseProgressBar.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import type { ContractPhase, ContractType } from "../../lib/api";
-import { getValidPhases } from "../../lib/api";
-
-interface PhaseProgressBarProps {
- currentPhase: ContractPhase;
- contractType?: ContractType;
- onPhaseClick?: (phase: ContractPhase) => void;
- readonly?: boolean;
-}
-
-const phases: ContractPhase[] = ["research", "specify", "plan", "execute", "review"];
-
-const phaseLabels: Record<ContractPhase, string> = {
- research: "Research",
- specify: "Specify",
- plan: "Plan",
- execute: "Execute",
- review: "Review",
-};
-
-const phaseColors: Record<ContractPhase, { active: string; inactive: string; completed: string }> = {
- research: {
- active: "bg-purple-400 border-purple-400",
- inactive: "bg-transparent border-purple-400/30",
- completed: "bg-purple-400/50 border-purple-400/50",
- },
- specify: {
- active: "bg-blue-400 border-blue-400",
- inactive: "bg-transparent border-blue-400/30",
- completed: "bg-blue-400/50 border-blue-400/50",
- },
- plan: {
- active: "bg-cyan-400 border-cyan-400",
- inactive: "bg-transparent border-cyan-400/30",
- completed: "bg-cyan-400/50 border-cyan-400/50",
- },
- execute: {
- active: "bg-yellow-400 border-yellow-400",
- inactive: "bg-transparent border-yellow-400/30",
- completed: "bg-yellow-400/50 border-yellow-400/50",
- },
- review: {
- active: "bg-green-400 border-green-400",
- inactive: "bg-transparent border-green-400/30",
- completed: "bg-green-400/50 border-green-400/50",
- },
-};
-
-export function PhaseProgressBar({
- currentPhase,
- contractType,
- onPhaseClick,
- readonly = false,
-}: PhaseProgressBarProps) {
- const visiblePhases = contractType ? getValidPhases(contractType) : phases;
- const currentIndex = visiblePhases.indexOf(currentPhase);
-
- return (
- <div className="flex items-center gap-1">
- {visiblePhases.map((phase, index) => {
- const isActive = phase === currentPhase;
- const isCompleted = index < currentIndex;
- const colors = phaseColors[phase];
- const colorClass = isActive
- ? colors.active
- : isCompleted
- ? colors.completed
- : colors.inactive;
-
- const canClick = !readonly && onPhaseClick;
-
- return (
- <div key={phase} className="flex items-center">
- {/* Phase node */}
- <button
- onClick={() => canClick && onPhaseClick(phase)}
- disabled={readonly}
- className={`
- relative group flex flex-col items-center
- ${canClick ? "cursor-pointer" : "cursor-default"}
- `}
- >
- {/* Circle */}
- <div
- className={`
- w-3 h-3 rounded-full border-2 transition-all
- ${colorClass}
- ${canClick && !isActive ? "hover:scale-110" : ""}
- `}
- />
- {/* Label */}
- <span
- className={`
- absolute top-4 font-mono text-[9px] uppercase tracking-wide whitespace-nowrap
- ${isActive ? "text-[#dbe7ff]" : "text-[#555]"}
- ${canClick && !isActive ? "group-hover:text-[#75aafc]" : ""}
- `}
- >
- {phaseLabels[phase]}
- </span>
- </button>
-
- {/* Connector line */}
- {index < visiblePhases.length - 1 && (
- <div
- className={`
- w-8 h-0.5 mx-1
- ${index < currentIndex ? "bg-[#3f6fb3]" : "bg-[rgba(117,170,252,0.15)]"}
- `}
- />
- )}
- </div>
- );
- })}
- </div>
- );
-}
-
-export function PhaseProgressBarCompact({
- currentPhase,
- contractType,
-}: {
- currentPhase: ContractPhase;
- contractType?: ContractType;
-}) {
- const visiblePhases = contractType ? getValidPhases(contractType) : phases;
- const currentIndex = visiblePhases.indexOf(currentPhase);
-
- return (
- <div className="flex items-center gap-0.5">
- {visiblePhases.map((phase, index) => {
- const isActive = phase === currentPhase;
- const isCompleted = index < currentIndex;
- const colors = phaseColors[phase];
-
- return (
- <div
- key={phase}
- className={`
- w-2 h-2 rounded-full border
- ${isActive ? colors.active : isCompleted ? colors.completed : colors.inactive}
- `}
- title={phaseLabels[phase]}
- />
- );
- })}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/QuickActionButtons.tsx b/makima/frontend/src/components/contracts/QuickActionButtons.tsx
deleted file mode 100644
index 4dbb90c..0000000
--- a/makima/frontend/src/components/contracts/QuickActionButtons.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { useCallback } from "react";
-
-export type QuickActionType =
- | "create_file"
- | "create_task"
- | "run_task"
- | "advance_phase"
- | "derive_tasks"
- | "update_file";
-
-export interface QuickAction {
- type: QuickActionType;
- label: string;
- description?: string;
- data?: Record<string, unknown>;
-}
-
-interface QuickActionButtonsProps {
- actions: QuickAction[];
- onAction: (action: QuickAction) => void;
- loading?: boolean;
-}
-
-const ACTION_ICONS: Record<QuickActionType, string> = {
- create_file: "[+]",
- create_task: "[T]",
- run_task: "[>]",
- advance_phase: "[→]",
- derive_tasks: "[≡]",
- update_file: "[*]",
-};
-
-const ACTION_COLORS: Record<QuickActionType, string> = {
- create_file: "border-blue-400/30 hover:border-blue-400/60 text-blue-400",
- create_task: "border-green-400/30 hover:border-green-400/60 text-green-400",
- run_task: "border-yellow-400/30 hover:border-yellow-400/60 text-yellow-400",
- advance_phase: "border-purple-400/30 hover:border-purple-400/60 text-purple-400",
- derive_tasks: "border-cyan-400/30 hover:border-cyan-400/60 text-cyan-400",
- update_file: "border-orange-400/30 hover:border-orange-400/60 text-orange-400",
-};
-
-export function QuickActionButtons({
- actions,
- onAction,
- loading = false,
-}: QuickActionButtonsProps) {
- const handleClick = useCallback(
- (action: QuickAction) => {
- if (!loading) {
- onAction(action);
- }
- },
- [onAction, loading]
- );
-
- if (actions.length === 0) return null;
-
- return (
- <div className="flex flex-wrap gap-2 mt-2">
- {actions.map((action, index) => (
- <button
- key={`${action.type}-${index}`}
- onClick={() => handleClick(action)}
- disabled={loading}
- className={`
- flex items-center gap-1.5 px-2 py-1
- font-mono text-[10px] uppercase
- border transition-colors
- disabled:opacity-50 disabled:cursor-not-allowed
- ${ACTION_COLORS[action.type]}
- `}
- title={action.description}
- >
- <span>{ACTION_ICONS[action.type]}</span>
- <span>{action.label}</span>
- </button>
- ))}
- </div>
- );
-}
-
-/**
- * Parse tool call results to extract suggested quick actions.
- * This is used by ContractCliInput to detect actionable results.
- */
-export function parseActionsFromToolCalls(
- toolCalls: { name: string; success: boolean; message: string }[]
-): QuickAction[] {
- const actions: QuickAction[] = [];
-
- for (const tc of toolCalls) {
- if (!tc.success) continue;
-
- switch (tc.name) {
- case "derive_tasks_from_file":
- // When tasks are parsed, offer to create them
- if (tc.message.includes("task") || tc.message.includes("Found")) {
- actions.push({
- type: "derive_tasks",
- label: "Review & Create Tasks",
- description: "Review parsed tasks and create them with chaining",
- });
- }
- break;
-
- case "process_task_completion":
- // Check for suggested actions in the result
- if (tc.message.includes("next task")) {
- actions.push({
- type: "run_task",
- label: "Run Next Task",
- description: "Continue with the next chained task",
- });
- }
- if (tc.message.includes("advance") || tc.message.includes("phase")) {
- actions.push({
- type: "advance_phase",
- label: "Advance Phase",
- description: "Move to the next contract phase",
- });
- }
- break;
-
- case "get_phase_checklist":
- // When checklist shows missing items, offer to create them
- if (tc.message.includes("missing") || tc.message.includes("not created")) {
- actions.push({
- type: "create_file",
- label: "Create Missing Files",
- description: "Create files from recommended templates",
- });
- }
- break;
-
- case "advance_phase":
- // After phase transition, suggest creating files
- actions.push({
- type: "create_file",
- label: "Create Phase Files",
- description: "Create recommended files for this phase",
- });
- break;
- }
- }
-
- return actions;
-}
-
-/**
- * Parse LLM response text to detect suggested actions.
- * Used as a fallback when structured action data isn't available.
- */
-export function parseActionsFromText(text: string): QuickAction[] {
- const actions: QuickAction[] = [];
- const lower = text.toLowerCase();
-
- // Detect file creation suggestions
- if (
- lower.includes("create a file") ||
- lower.includes("create the file") ||
- lower.includes("should i create")
- ) {
- actions.push({
- type: "create_file",
- label: "Create File",
- description: "Create the suggested file",
- });
- }
-
- // Detect task creation suggestions
- if (
- lower.includes("create tasks") ||
- lower.includes("create these tasks") ||
- lower.includes("create chained tasks")
- ) {
- actions.push({
- type: "create_task",
- label: "Create Tasks",
- description: "Create the suggested tasks",
- });
- }
-
- // Detect phase advancement suggestions
- if (
- lower.includes("advance to") ||
- lower.includes("ready to move to") ||
- lower.includes("transition to")
- ) {
- const phases = ["specify", "plan", "execute", "review"];
- for (const phase of phases) {
- if (lower.includes(phase)) {
- actions.push({
- type: "advance_phase",
- label: `Advance to ${phase.charAt(0).toUpperCase() + phase.slice(1)}`,
- description: `Move to the ${phase} phase`,
- data: { phase },
- });
- break;
- }
- }
- }
-
- // Detect run task suggestions
- if (
- lower.includes("run the task") ||
- lower.includes("start the task") ||
- lower.includes("run task")
- ) {
- actions.push({
- type: "run_task",
- label: "Run Task",
- description: "Start the suggested task",
- });
- }
-
- return actions;
-}
diff --git a/makima/frontend/src/components/contracts/RepositoryPanel.tsx b/makima/frontend/src/components/contracts/RepositoryPanel.tsx
deleted file mode 100644
index 15741a8..0000000
--- a/makima/frontend/src/components/contracts/RepositoryPanel.tsx
+++ /dev/null
@@ -1,325 +0,0 @@
-import { useState, useEffect } from "react";
-import type {
- ContractRepository,
- RepositorySourceType,
- RepositoryStatus,
- DaemonDirectory,
- RepositoryHistoryEntry,
-} from "../../lib/api";
-import { getDaemonDirectories, getRepositorySuggestions } from "../../lib/api";
-import { DirectoryInput } from "../mesh/DirectoryInput";
-
-interface RepositoryPanelProps {
- repositories: ContractRepository[];
- onAddRemote: (name: string, url: string, isPrimary: boolean) => void;
- onAddLocal: (name: string, path: string, isPrimary: boolean) => void;
- onCreateManaged: (name: string, isPrimary: boolean) => void;
- onDelete: (repoId: string) => void;
- onSetPrimary: (repoId: string) => void;
-}
-
-type AddMode = "remote" | "local" | "managed" | null;
-
-const sourceTypeLabels: Record<RepositorySourceType, string> = {
- remote: "Remote",
- local: "Local",
- managed: "Managed",
-};
-
-const sourceTypeIcons: Record<RepositorySourceType, string> = {
- remote: "GH",
- local: "FS",
- managed: "MK",
-};
-
-const statusColors: Record<RepositoryStatus, string> = {
- ready: "text-green-400",
- pending: "text-yellow-400",
- creating: "text-cyan-400",
- failed: "text-red-400",
-};
-
-export function RepositoryPanel({
- repositories,
- onAddRemote,
- onAddLocal,
- onCreateManaged,
- onDelete,
- onSetPrimary,
-}: RepositoryPanelProps) {
- const [addMode, setAddMode] = useState<AddMode>(null);
- const [name, setName] = useState("");
- const [url, setUrl] = useState("");
- const [path, setPath] = useState("");
- const [isPrimary, setIsPrimary] = useState(false);
- // Daemon directory suggestions for local repositories
- const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]);
- // Repository history suggestions
- const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
- const [showSuggestions, setShowSuggestions] = useState(false);
-
- // Fetch daemon directories when "local" mode is selected
- useEffect(() => {
- if (addMode === "local") {
- getDaemonDirectories()
- .then((res) => setSuggestedDirectories(res.directories))
- .catch(() => setSuggestedDirectories([]));
- }
- }, [addMode]);
-
- // Fetch repository suggestions when mode changes to remote or local
- useEffect(() => {
- if (addMode === "remote" || addMode === "local") {
- getRepositorySuggestions(addMode, undefined, 10)
- .then((res) => {
- setRepoSuggestions(res.entries);
- setShowSuggestions(res.entries.length > 0);
- })
- .catch(() => {
- setRepoSuggestions([]);
- setShowSuggestions(false);
- });
- } else {
- setRepoSuggestions([]);
- setShowSuggestions(false);
- }
- }, [addMode]);
-
- // Apply a suggestion to the form
- const applySuggestion = (suggestion: RepositoryHistoryEntry) => {
- setName(suggestion.name);
- if (suggestion.repositoryUrl) {
- setUrl(suggestion.repositoryUrl);
- }
- if (suggestion.localPath) {
- setPath(suggestion.localPath);
- }
- setShowSuggestions(false);
- };
-
- const handleAdd = () => {
- if (!name.trim()) return;
-
- if (addMode === "remote" && url.trim()) {
- onAddRemote(name.trim(), url.trim(), isPrimary);
- } else if (addMode === "local" && path.trim()) {
- onAddLocal(name.trim(), path.trim(), isPrimary);
- } else if (addMode === "managed") {
- onCreateManaged(name.trim(), isPrimary);
- }
-
- // Reset form
- setAddMode(null);
- setName("");
- setUrl("");
- setPath("");
- setIsPrimary(false);
- };
-
- const handleCancel = () => {
- setAddMode(null);
- setName("");
- setUrl("");
- setPath("");
- setIsPrimary(false);
- };
-
- return (
- <div className="space-y-4">
- {/* Repository list */}
- {repositories.length === 0 ? (
- <p className="font-mono text-xs text-[#555]">
- No repositories configured
- </p>
- ) : (
- <div className="space-y-2">
- {repositories.map((repo) => (
- <div
- key={repo.id}
- className="flex items-center gap-3 p-3 border border-[rgba(117,170,252,0.2)]"
- >
- {/* Type icon */}
- <span className="font-mono text-[10px] text-[#555] uppercase w-6">
- {sourceTypeIcons[repo.sourceType]}
- </span>
-
- {/* Name and details */}
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <span className="font-mono text-sm text-[#dbe7ff] truncate">
- {repo.name}
- </span>
- {repo.isPrimary && (
- <span className="px-1 py-0.5 text-[8px] font-mono uppercase bg-[rgba(117,170,252,0.1)] text-[#75aafc] border border-[rgba(117,170,252,0.3)]">
- Primary
- </span>
- )}
- </div>
- <div className="font-mono text-[10px] text-[#555] truncate">
- {repo.repositoryUrl || repo.localPath || "(pending creation)"}
- </div>
- </div>
-
- {/* Status */}
- <span
- className={`font-mono text-[10px] uppercase ${
- statusColors[repo.status]
- }`}
- >
- {repo.status}
- </span>
-
- {/* Actions */}
- <div className="flex items-center gap-1">
- {!repo.isPrimary && repo.status === "ready" && (
- <button
- onClick={() => onSetPrimary(repo.id)}
- className="p-1 font-mono text-[10px] text-[#555] hover:text-[#75aafc] transition-colors"
- title="Set as primary"
- >
- *
- </button>
- )}
- <button
- onClick={() => onDelete(repo.id)}
- className="p-1 font-mono text-[10px] text-[#555] hover:text-red-400 transition-colors"
- title="Remove"
- >
- x
- </button>
- </div>
- </div>
- ))}
- </div>
- )}
-
- {/* Add repository form */}
- {addMode ? (
- <div className="p-3 border border-[rgba(117,170,252,0.3)] space-y-3">
- <div className="flex items-center justify-between mb-2">
- <span className="font-mono text-xs text-[#75aafc] uppercase">
- Add {sourceTypeLabels[addMode]} Repository
- </span>
- {repoSuggestions.length > 0 && (
- <button
- onClick={() => setShowSuggestions(!showSuggestions)}
- className="font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- {showSuggestions ? "Hide suggestions" : `${repoSuggestions.length} suggestions`}
- </button>
- )}
- </div>
-
- {/* Suggestions dropdown */}
- {showSuggestions && repoSuggestions.length > 0 && (
- <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
- {repoSuggestions.map((suggestion) => (
- <button
- key={suggestion.id}
- onClick={() => applySuggestion(suggestion)}
- className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] transition-colors border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
- >
- <div className="flex items-center justify-between">
- <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
- <span className="text-[10px] text-[#556677]">
- {suggestion.useCount}×
- </span>
- </div>
- <div className="text-[10px] text-[#556677] truncate">
- {addMode === "local" ? suggestion.localPath : suggestion.repositoryUrl}
- </div>
- </button>
- ))}
- </div>
- )}
-
- <input
- type="text"
- value={name}
- onChange={(e) => setName(e.target.value)}
- placeholder="Repository name"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
- />
-
- {addMode === "remote" && (
- <input
- type="text"
- value={url}
- onChange={(e) => setUrl(e.target.value)}
- placeholder="https://github.com/owner/repo"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc]"
- />
- )}
-
- {addMode === "local" && (
- <DirectoryInput
- value={path}
- onChange={setPath}
- suggestions={suggestedDirectories}
- placeholder="/path/to/repository"
- />
- )}
-
- {addMode === "managed" && (
- <p className="font-mono text-xs text-[#555]">
- Makima will create this repository via the daemon.
- </p>
- )}
-
- <label className="flex items-center gap-2 cursor-pointer">
- <input
- type="checkbox"
- checked={isPrimary}
- onChange={(e) => setIsPrimary(e.target.checked)}
- className="w-3 h-3"
- />
- <span className="font-mono text-xs text-[#9bc3ff]">
- Set as primary repository
- </span>
- </label>
-
- <div className="flex gap-2">
- <button
- onClick={handleCancel}
- className="px-3 py-1.5 font-mono text-xs text-[#555] hover:text-[#9bc3ff] transition-colors"
- >
- Cancel
- </button>
- <button
- onClick={handleAdd}
- disabled={
- !name.trim() ||
- (addMode === "remote" && !url.trim()) ||
- (addMode === "local" && !path.trim())
- }
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Add Repository
- </button>
- </div>
- </div>
- ) : (
- <div className="flex gap-2">
- <button
- onClick={() => setAddMode("remote")}
- 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"
- >
- + Remote
- </button>
- <button
- onClick={() => setAddMode("local")}
- 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"
- >
- + Local
- </button>
- <button
- onClick={() => setAddMode("managed")}
- 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"
- >
- + Managed
- </button>
- </div>
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx b/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
deleted file mode 100644
index 07421ef..0000000
--- a/makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import { useState, useCallback } from "react";
-
-export interface ParsedTask {
- name: string;
- description?: string;
- group?: string;
- order: number;
- completed: boolean;
- dependencies: string[];
-}
-
-interface TaskDerivationPreviewProps {
- tasks: ParsedTask[];
- groups: string[];
- fileName: string;
- onCreateTasks: (selectedTasks: ParsedTask[]) => void;
- onCancel: () => void;
- loading?: boolean;
-}
-
-export function TaskDerivationPreview({
- tasks,
- groups,
- fileName,
- onCreateTasks,
- onCancel,
- loading = false,
-}: TaskDerivationPreviewProps) {
- const [selectedIndices, setSelectedIndices] = useState<Set<number>>(
- new Set(tasks.map((_, i) => i)) // Select all by default
- );
-
- const toggleTask = useCallback((index: number) => {
- setSelectedIndices((prev) => {
- const newSet = new Set(prev);
- if (newSet.has(index)) {
- newSet.delete(index);
- } else {
- newSet.add(index);
- }
- return newSet;
- });
- }, []);
-
- const selectAll = useCallback(() => {
- setSelectedIndices(new Set(tasks.map((_, i) => i)));
- }, [tasks]);
-
- const selectNone = useCallback(() => {
- setSelectedIndices(new Set());
- }, []);
-
- const handleCreate = useCallback(() => {
- const selectedTasks = tasks.filter((_, i) => selectedIndices.has(i));
- onCreateTasks(selectedTasks);
- }, [tasks, selectedIndices, onCreateTasks]);
-
- // Group tasks by their group property
- const tasksByGroup = tasks.reduce((acc, task, index) => {
- const groupKey = task.group || "Ungrouped";
- if (!acc[groupKey]) {
- acc[groupKey] = [];
- }
- acc[groupKey].push({ task, index });
- return acc;
- }, {} as Record<string, { task: ParsedTask; index: number }[]>);
-
- const selectedCount = selectedIndices.size;
- const totalCount = tasks.length;
-
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
- <div className="w-full max-w-2xl p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[80vh] flex flex-col">
- {/* Header */}
- <div className="flex items-center justify-between mb-4">
- <div>
- <h3 className="font-mono text-sm text-[#75aafc] uppercase">
- Create Tasks from Document
- </h3>
- <p className="font-mono text-xs text-[#555] mt-1">
- Source: {fileName}
- </p>
- </div>
- <div className="flex items-center gap-2">
- <button
- onClick={selectAll}
- className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
- >
- Select All
- </button>
- <span className="text-[#555]">|</span>
- <button
- onClick={selectNone}
- className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
- >
- Select None
- </button>
- </div>
- </div>
-
- {/* Task List */}
- <div className="flex-1 overflow-y-auto space-y-4 mb-4">
- {groups.length > 0 ? (
- // Grouped view
- Object.entries(tasksByGroup).map(([groupName, groupTasks]) => (
- <div key={groupName} className="space-y-2">
- <h4 className="font-mono text-xs text-[#9bc3ff] uppercase border-b border-[rgba(117,170,252,0.2)] pb-1">
- {groupName}
- </h4>
- {groupTasks.map(({ task, index }) => (
- <TaskItem
- key={index}
- task={task}
- index={index}
- selected={selectedIndices.has(index)}
- onToggle={() => toggleTask(index)}
- />
- ))}
- </div>
- ))
- ) : (
- // Flat view
- tasks.map((task, index) => (
- <TaskItem
- key={index}
- task={task}
- index={index}
- selected={selectedIndices.has(index)}
- onToggle={() => toggleTask(index)}
- />
- ))
- )}
- </div>
-
- {/* Footer */}
- <div className="flex items-center justify-between pt-4 border-t border-[rgba(117,170,252,0.2)]">
- <span className="font-mono text-xs text-[#555]">
- {selectedCount} of {totalCount} tasks selected
- </span>
- <div className="flex gap-2">
- <button
- onClick={onCancel}
- disabled={loading}
- className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors disabled:opacity-50"
- >
- Cancel
- </button>
- <button
- onClick={handleCreate}
- disabled={loading || selectedCount === 0}
- 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"
- >
- {loading ? "Creating..." : `Create ${selectedCount} Task${selectedCount !== 1 ? "s" : ""}`}
- </button>
- </div>
- </div>
-
- {/* Chaining info */}
- {selectedCount > 1 && (
- <p className="font-mono text-[10px] text-[#555] mt-2 text-center">
- Tasks will be chained: each task continues from the previous one's work
- </p>
- )}
- </div>
- </div>
- );
-}
-
-function TaskItem({
- task,
- index,
- selected,
- onToggle,
-}: {
- task: ParsedTask;
- index: number;
- selected: boolean;
- onToggle: () => void;
-}) {
- return (
- <button
- onClick={onToggle}
- className={`w-full text-left p-3 border transition-colors ${
- selected
- ? "border-[#75aafc] bg-[rgba(117,170,252,0.1)]"
- : "border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.3)]"
- }`}
- >
- <div className="flex items-start gap-2">
- <span
- className={`font-mono text-xs mt-0.5 ${
- selected ? "text-[#75aafc]" : "text-[#555]"
- }`}
- >
- {selected ? "[x]" : "[ ]"}
- </span>
- <div className="flex-1 min-w-0">
- <div className="flex items-center gap-2">
- <span className="font-mono text-[10px] text-[#555]">#{index + 1}</span>
- <span className="font-mono text-sm text-[#dbe7ff]">{task.name}</span>
- {task.completed && (
- <span className="font-mono text-[9px] text-green-400 uppercase">
- done in source
- </span>
- )}
- </div>
- {task.description && (
- <p className="font-mono text-xs text-[#555] mt-1 truncate">
- {task.description}
- </p>
- )}
- {task.dependencies.length > 0 && (
- <p className="font-mono text-[10px] text-[#75aafc] mt-1">
- Depends on: {task.dependencies.join(", ")}
- </p>
- )}
- </div>
- </div>
- </button>
- );
-}
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index bbb72f3..c1c6c35 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -12,14 +12,12 @@ import { ProtectedRoute } from "./components/ProtectedRoute";
import HomePage from "./routes/_index";
import ListenPage from "./routes/listen";
import FilesPage from "./routes/files";
-import ContractsPage from "./routes/contracts";
import OrdersPage from "./routes/orders";
import MeshPage from "./routes/mesh";
import DaemonsPage from "./routes/daemons";
import HistoryPage from "./routes/history";
import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
-import ContractFilePage from "./routes/contract-file";
import SpeakPage from "./routes/speak";
import DirectivesPage from "./routes/directives";
import ExecRedirect from "./routes/exec-redirect";
@@ -62,30 +60,6 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/contracts"
- element={
- <ProtectedRoute>
- <ContractsPage />
- </ProtectedRoute>
- }
- />
- <Route
- path="/contracts/:id"
- element={
- <ProtectedRoute>
- <ContractsPage />
- </ProtectedRoute>
- }
- />
- <Route
- path="/contracts/:id/files/:fileId"
- element={
- <ProtectedRoute>
- <ContractFilePage />
- </ProtectedRoute>
- }
- />
- <Route
path="/orders"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/contract-file.tsx b/makima/frontend/src/routes/contract-file.tsx
deleted file mode 100644
index 9ed25ed..0000000
--- a/makima/frontend/src/routes/contract-file.tsx
+++ /dev/null
@@ -1,659 +0,0 @@
-import { useEffect, useState, useCallback, useRef } from "react";
-import { useParams, useNavigate } from "react-router";
-import { useAuth } from "../contexts/AuthContext";
-import { Masthead } from "../components/Masthead";
-import { FileDetail, type FocusedElement } from "../components/files/FileDetail";
-import { CliInput } from "../components/files/CliInput";
-import { ConflictNotification } from "../components/files/ConflictNotification";
-import { UpdateNotification } from "../components/files/UpdateNotification";
-import { useFiles } from "../hooks/useFiles";
-import { useVersionHistory } from "../hooks/useVersionHistory";
-import {
- useFileSubscription,
- type FileUpdateEvent,
-} from "../hooks/useFileSubscription";
-import type { FileDetail as FileDetailType, BodyElement } from "../lib/api";
-
-/**
- * ContractFilePage - Wrapper for viewing files within a contract context
- *
- * This component handles the /contracts/:contractId/files/:fileId route,
- * providing navigation back to the contract and rendering the file detail view.
- */
-export default function ContractFilePage() {
- const { id: contractId, fileId } = useParams<{ id: string; fileId: string }>();
- const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
- const navigate = useNavigate();
-
- // Redirect to login if not authenticated (when auth is configured)
- useEffect(() => {
- if (!authLoading && isAuthConfigured && !isAuthenticated) {
- navigate("/login");
- }
- }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
-
- // Show loading while checking auth
- if (authLoading) {
- return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
- <Masthead showNav />
- <main className="flex-1 flex items-center justify-center">
- <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
- </main>
- </div>
- );
- }
-
- // Don't render if not authenticated (will redirect)
- if (isAuthConfigured && !isAuthenticated) {
- return null;
- }
-
- // Render the file page with contract context
- return <ContractAwareFilesPage contractId={contractId} fileId={fileId} />;
-}
-
-// A version of the files page aware of contract context
-function ContractAwareFilesPage({
- contractId,
- fileId,
-}: {
- contractId?: string;
- fileId?: string;
-}) {
- const navigate = useNavigate();
- const { error, conflict, clearConflict, fetchFile, editFile, removeFile } = useFiles();
- const [fileDetail, setFileDetail] = useState<FileDetailType | null>(null);
- const [detailLoading, setDetailLoading] = useState(false);
- const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null);
- const [remoteFileData, setRemoteFileData] = useState<FileDetailType | null>(null);
- const [focusedElement, setFocusedElement] = useState<FocusedElement | null>(null);
- const [suggestedPrompt, setSuggestedPrompt] = useState<string | null>(null);
- const pendingUpdateRef = useRef(false);
- const lastSentVersionRef = useRef<number | null>(null);
- const lastSavedVersionRef = useRef<number | null>(null);
- const hasLocalChangesRef = useRef(false);
- const isActivelyEditingRef = useRef(false);
- const currentVersionRef = useRef<number | null>(null);
-
- // Handle back navigation - go to contract detail instead of /files
- const handleBack = useCallback(() => {
- if (contractId) {
- navigate(`/contracts/${contractId}`);
- } else {
- navigate("/contracts");
- }
- }, [contractId, navigate]);
-
- const updateHasLocalChanges = useCallback((value: boolean) => {
- hasLocalChangesRef.current = value;
- }, []);
-
- const updateIsActivelyEditing = useCallback((value: boolean) => {
- isActivelyEditingRef.current = value;
- }, []);
-
- // Version history
- const {
- versions,
- loading: versionsLoading,
- selectedVersion,
- loadingVersion,
- restoring,
- fetchVersion,
- restoreToVersion,
- clearSelectedVersion,
- fetchVersions,
- } = useVersionHistory({
- fileId: fileId || null,
- currentVersion: fileDetail?.version || 0,
- });
-
- const handleRestoreVersion = useCallback(
- async (targetVersion: number) => {
- const result = await restoreToVersion(targetVersion);
- if (result) {
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- fetchVersions();
- }
- },
- [restoreToVersion, fetchVersions, updateHasLocalChanges]
- );
-
- // Load file detail when fileId is provided
- useEffect(() => {
- if (fileId) {
- setDetailLoading(true);
- updateHasLocalChanges(false);
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- lastSavedVersionRef.current = null;
- currentVersionRef.current = null;
- setRemoteUpdate(null);
- setRemoteFileData(null);
- setFocusedElement(null);
- fetchFile(fileId).then((detail) => {
- if (detail) {
- currentVersionRef.current = detail.version;
- }
- setFileDetail(detail);
- setDetailLoading(false);
- });
- } else {
- setFileDetail(null);
- currentVersionRef.current = null;
- updateHasLocalChanges(false);
- }
- }, [fileId, fetchFile, updateHasLocalChanges]);
-
- // Handle file update events from WebSocket
- const handleFileUpdate = useCallback(
- async (event: FileUpdateEvent) => {
- if (lastSavedVersionRef.current !== null && event.version === lastSavedVersionRef.current) {
- lastSavedVersionRef.current = null;
- return;
- }
-
- if (pendingUpdateRef.current) {
- if (lastSentVersionRef.current !== null) {
- const expectedNewVersion = lastSentVersionRef.current + 1;
- if (event.version === expectedNewVersion) {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- return;
- }
- }
- return;
- }
-
- if (currentVersionRef.current !== null && event.version === currentVersionRef.current) {
- return;
- }
-
- if (!hasLocalChangesRef.current && !isActivelyEditingRef.current) {
- const detail = await fetchFile(event.fileId);
- if (detail) {
- currentVersionRef.current = detail.version;
- }
- setFileDetail(detail);
- } else {
- const remoteData = await fetchFile(event.fileId);
- setRemoteFileData(remoteData);
- setRemoteUpdate(event);
- }
- },
- [fetchFile]
- );
-
- useFileSubscription({
- fileId: fileId || null,
- onUpdate: handleFileUpdate,
- });
-
- const handleDelete = useCallback(
- async (id: string) => {
- if (confirm("Are you sure you want to delete this file?")) {
- const success = await removeFile(id);
- if (success && fileId === id) {
- handleBack();
- }
- }
- },
- [removeFile, fileId, handleBack]
- );
-
- const handleSave = useCallback(
- async (id: string, name: string, description: string) => {
- if (!fileDetail) return;
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(id, { name, description, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- },
- [editFile, fileDetail, updateHasLocalChanges]
- );
-
- const handleBodyUpdate = useCallback(
- (body: BodyElement[], summary: string | null) => {
- if (fileDetail) {
- setFileDetail({
- ...fileDetail,
- body,
- summary,
- });
- }
- },
- [fileDetail]
- );
-
- const handleBodyElementUpdate = useCallback(
- async (index: number, element: BodyElement) => {
- if (fileDetail && fileId) {
- const newBody = [...fileDetail.body];
- newBody[index] = element;
-
- setFileDetail({
- ...fileDetail,
- body: newBody,
- });
- updateHasLocalChanges(true);
-
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(fileId, { body: newBody, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- }
- },
- [fileDetail, fileId, editFile, updateHasLocalChanges]
- );
-
- const handleBodyReorder = useCallback(
- async (fromIndex: number, toIndex: number) => {
- if (fileDetail && fileId) {
- const newBody = [...fileDetail.body];
- const [movedElement] = newBody.splice(fromIndex, 1);
- newBody.splice(toIndex, 0, movedElement);
-
- setFileDetail({
- ...fileDetail,
- body: newBody,
- });
- updateHasLocalChanges(true);
-
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(fileId, { body: newBody, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- }
- },
- [fileDetail, fileId, editFile, updateHasLocalChanges]
- );
-
- const handleBodyElementDelete = useCallback(
- async (index: number) => {
- if (fileDetail && fileId) {
- const newBody = fileDetail.body.filter((_, i) => i !== index);
-
- setFileDetail({
- ...fileDetail,
- body: newBody,
- });
- updateHasLocalChanges(true);
-
- if (focusedElement?.index === index) {
- setFocusedElement(null);
- } else if (focusedElement && focusedElement.index > index) {
- setFocusedElement({
- ...focusedElement,
- index: focusedElement.index - 1,
- });
- }
-
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(fileId, { body: newBody, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- }
- },
- [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement]
- );
-
- const handleBodyElementDuplicate = useCallback(
- async (index: number) => {
- if (fileDetail && fileId) {
- const elementToDuplicate = fileDetail.body[index];
- if (!elementToDuplicate) return;
-
- const newBody = [...fileDetail.body];
- newBody.splice(index + 1, 0, { ...elementToDuplicate });
-
- setFileDetail({
- ...fileDetail,
- body: newBody,
- });
- updateHasLocalChanges(true);
-
- if (focusedElement && focusedElement.index > index) {
- setFocusedElement({
- ...focusedElement,
- index: focusedElement.index + 1,
- });
- }
-
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(fileId, { body: newBody, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- }
- },
- [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement]
- );
-
- const handleFocusElement = useCallback((element: FocusedElement | null) => {
- setFocusedElement(element);
- }, []);
-
- const handleClearFocus = useCallback(() => {
- setFocusedElement(null);
- }, []);
-
- const handleConvertElement = useCallback(
- async (index: number, toType: string) => {
- if (!fileDetail || !fileId) return;
-
- const element = fileDetail.body[index];
- if (!element) return;
-
- let textContent = "";
- switch (element.type) {
- case "heading":
- case "paragraph":
- textContent = element.text;
- break;
- case "code":
- textContent = element.content;
- break;
- case "list":
- textContent = element.items.join("\n");
- break;
- default:
- return;
- }
-
- let newElement: BodyElement;
- if (toType === "paragraph") {
- newElement = { type: "paragraph", text: textContent };
- } else if (toType === "list_unordered") {
- const items = textContent.split("\n").filter(line => line.trim());
- newElement = { type: "list", ordered: false, items };
- } else if (toType === "list_ordered") {
- const items = textContent.split("\n").filter(line => line.trim());
- newElement = { type: "list", ordered: true, items };
- } else if (toType === "code") {
- newElement = { type: "code", content: textContent };
- } else if (toType.startsWith("heading_")) {
- const level = parseInt(toType.replace("heading_", ""), 10) as 1 | 2 | 3 | 4 | 5 | 6;
- newElement = { type: "heading", level, text: textContent };
- } else {
- return;
- }
-
- const newBody = [...fileDetail.body];
- newBody[index] = newElement;
-
- setFileDetail({ ...fileDetail, body: newBody });
- updateHasLocalChanges(true);
-
- if (focusedElement?.index === index) {
- setFocusedElement({
- index,
- type: newElement.type,
- preview: textContent.slice(0, 50) + (textContent.length > 50 ? "..." : ""),
- });
- }
-
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = fileDetail.version;
- try {
- const result = await editFile(fileId, { body: newBody, version: fileDetail.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- },
- [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement]
- );
-
- const handleGenerateFromElement = useCallback(
- (index: number, action: string) => {
- if (!fileDetail) return;
-
- const element = fileDetail.body[index];
- if (!element) return;
-
- let preview = "";
- switch (element.type) {
- case "heading":
- case "paragraph":
- preview = element.text.slice(0, 50);
- break;
- case "code":
- preview = element.content.slice(0, 50);
- break;
- case "list":
- preview = element.items[0]?.slice(0, 40) || "";
- break;
- default:
- preview = "Element";
- }
-
- setFocusedElement({
- index,
- type: element.type,
- preview: preview + (preview.length >= 50 ? "..." : ""),
- });
-
- let prompt = "";
- switch (action) {
- case "elaborate":
- prompt = "Elaborate and expand on this content";
- break;
- case "summarize":
- prompt = "Summarize this content";
- break;
- case "extract_actions":
- prompt = "Extract action items from this content";
- break;
- }
- setSuggestedPrompt(prompt);
- },
- [fileDetail]
- );
-
- // Conflict resolution handlers
- const handleConflictReload = useCallback(async () => {
- if (fileId) {
- clearConflict();
- const detail = await fetchFile(fileId);
- if (detail) {
- currentVersionRef.current = detail.version;
- }
- setFileDetail(detail);
- updateHasLocalChanges(false);
- }
- }, [fileId, clearConflict, fetchFile, updateHasLocalChanges]);
-
- const handleConflictForceOverwrite = useCallback(async () => {
- if (fileId && fileDetail) {
- clearConflict();
- const latest = await fetchFile(fileId);
- if (latest) {
- pendingUpdateRef.current = true;
- lastSentVersionRef.current = latest.version;
- try {
- const result = await editFile(fileId, { body: fileDetail.body, version: latest.version });
- if (result) {
- lastSavedVersionRef.current = result.version;
- currentVersionRef.current = result.version;
- setFileDetail(result);
- updateHasLocalChanges(false);
- }
- } finally {
- pendingUpdateRef.current = false;
- lastSentVersionRef.current = null;
- }
- }
- }
- }, [fileId, fileDetail, clearConflict, fetchFile, editFile, updateHasLocalChanges]);
-
- const handleRemoteUpdateRefresh = useCallback(async () => {
- if (fileId) {
- const detail = await fetchFile(fileId);
- if (detail) {
- currentVersionRef.current = detail.version;
- }
- setFileDetail(detail);
- setRemoteUpdate(null);
- setRemoteFileData(null);
- updateHasLocalChanges(false);
- }
- }, [fileId, fetchFile, updateHasLocalChanges]);
-
- const handleRemoteUpdateDismiss = useCallback(() => {
- setRemoteUpdate(null);
- setRemoteFileData(null);
- }, []);
-
- return (
- <div className="relative z-10 h-screen flex flex-col overflow-hidden">
- <Masthead showTicker={false} showNav />
-
- <main className="flex-1 p-4 md:p-6 min-h-0 overflow-hidden flex flex-col">
- {error && (
- <div className="mb-4 p-3 border border-red-400/50 bg-red-400/10 text-red-400 font-mono text-sm shrink-0">
- {error}
- </div>
- )}
-
- {fileId && fileDetail ? (
- <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
- <div className="flex-1 min-h-0 overflow-hidden">
- <FileDetail
- file={fileDetail}
- loading={detailLoading}
- onBack={handleBack}
- onSave={handleSave}
- onDelete={handleDelete}
- onBodyElementUpdate={handleBodyElementUpdate}
- onBodyReorder={handleBodyReorder}
- onBodyElementDelete={handleBodyElementDelete}
- onBodyElementDuplicate={handleBodyElementDuplicate}
- onConvertElement={handleConvertElement}
- onGenerateFromElement={handleGenerateFromElement}
- onEditingChange={updateIsActivelyEditing}
- hasPendingRemoteUpdate={!!remoteUpdate}
- onOverwrite={handleRemoteUpdateDismiss}
- focusedElement={focusedElement}
- onFocusElement={handleFocusElement}
- versions={versions}
- versionsLoading={versionsLoading}
- selectedVersion={selectedVersion}
- loadingVersion={loadingVersion}
- restoring={restoring}
- onSelectVersion={fetchVersion}
- onRestoreVersion={handleRestoreVersion}
- onClearVersionSelection={clearSelectedVersion}
- />
- </div>
- <div className="shrink-0">
- <CliInput
- fileId={fileId}
- onUpdate={handleBodyUpdate}
- focusedElement={focusedElement}
- onClearFocus={handleClearFocus}
- suggestedPrompt={suggestedPrompt}
- onClearSuggestedPrompt={() => setSuggestedPrompt(null)}
- />
- </div>
- </div>
- ) : fileId && detailLoading ? (
- <div className="panel h-full flex items-center justify-center">
- <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
- </div>
- ) : (
- <div className="panel h-full flex items-center justify-center">
- <div className="text-center">
- <p className="font-mono text-sm text-[#555] mb-4">
- File not found
- </p>
- <button
- onClick={handleBack}
- className="px-4 py-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
- >
- &larr; Back to Contract
- </button>
- </div>
- </div>
- )}
- </main>
-
- {/* Conflict notification */}
- {conflict?.hasConflict && (
- <ConflictNotification
- onReload={handleConflictReload}
- onForceOverwrite={handleConflictForceOverwrite}
- onDismiss={clearConflict}
- />
- )}
-
- {/* Remote update notification */}
- {remoteUpdate && (
- <UpdateNotification
- updatedBy={remoteUpdate.updatedBy}
- localBody={fileDetail?.body || []}
- remoteBody={remoteFileData?.body || []}
- onRefresh={handleRemoteUpdateRefresh}
- onDismiss={handleRemoteUpdateDismiss}
- />
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
deleted file mode 100644
index ce9ceca..0000000
--- a/makima/frontend/src/routes/contracts.tsx
+++ /dev/null
@@ -1,885 +0,0 @@
-import { useState, useCallback, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { Masthead } from "../components/Masthead";
-import { ContractList } from "../components/contracts/ContractList";
-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,
- listContractTypes,
-} from "../lib/api";
-import type {
- ContractWithRelations,
- ContractSummary,
- ContractPhase,
- ContractStatus,
- ContractType,
- CreateContractRequest,
- RepositorySourceType,
- DaemonDirectory,
- RepositoryHistoryEntry,
- ContractTypeTemplate,
-} from "../lib/api";
-
-export default function ContractsPage() {
- const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
- const navigate = useNavigate();
-
- // Redirect to login if not authenticated (when auth is configured)
- useEffect(() => {
- if (!authLoading && isAuthConfigured && !isAuthenticated) {
- navigate("/login");
- }
- }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
-
- // Show loading while checking auth
- if (authLoading) {
- return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
- <Masthead showNav />
- <main className="flex-1 flex items-center justify-center">
- <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
- </main>
- </div>
- );
- }
-
- // Don't render if not authenticated (will redirect)
- if (isAuthConfigured && !isAuthenticated) {
- return null;
- }
-
- return <ContractsPageContent />;
-}
-
-function ContractsPageContent() {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
- const {
- contracts,
- loading,
- error,
- fetchContract,
- saveContract,
- editContract,
- removeContract,
- changePhase,
- addRemoteRepo,
- addLocalRepo,
- createManagedRepo,
- removeRepo,
- setRepoPrimary,
- } = useContracts();
-
- const [contractDetail, setContractDetail] = useState<ContractWithRelations | null>(null);
- const [detailLoading, setDetailLoading] = useState(false);
- const [isCreating, setIsCreating] = useState(false);
- const [newContractName, setNewContractName] = useState("");
- const [newContractDescription, setNewContractDescription] = useState("");
- const [contractType, setContractType] = useState<ContractType>("simple");
- const [initialPhase, setInitialPhase] = useState<ContractPhase>("plan");
- const [repoType, setRepoType] = useState<RepositorySourceType>("remote");
- const [repoName, setRepoName] = useState("");
- const [repoUrl, setRepoUrl] = useState("");
- const [repoPath, setRepoPath] = useState("");
- const [createError, setCreateError] = useState<string | null>(null);
- 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);
- const [localOnly, setLocalOnly] = useState(false);
-
- // Fetch contract types when modal opens - API returns both built-in and custom templates
- useEffect(() => {
- if (isCreating) {
- setContractTypesLoading(true);
-
- listContractTypes()
- .then((res) => {
- setContractTypes(res.contractTypes);
- setContractTypesLoading(false);
- })
- .catch((err) => {
- console.error("Failed to fetch contract types:", err);
- // Fall back to built-in types
- const builtinTypes: ContractTypeTemplate[] = [
- {
- 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,
- },
- {
- id: "execute",
- name: "Execute",
- description: "Execute only: Minimal workflow for immediate task execution",
- phases: ["execute"],
- defaultPhase: "execute",
- isBuiltin: true,
- },
- ];
- setContractTypes(builtinTypes);
- setContractTypesLoading(false);
- });
- }
- }, [isCreating]);
-
- // Fetch repository suggestions when modal opens and repo type changes
- useEffect(() => {
- if (isCreating && (repoType === "remote" || repoType === "local")) {
- getRepositorySuggestions(repoType, undefined, 10)
- .then((res) => {
- setRepoSuggestions(res.entries);
- setShowRepoSuggestions(res.entries.length > 0);
- })
- .catch(() => {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- });
- } else {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- }
- }, [isCreating, repoType]);
-
- // Apply a repository suggestion
- const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => {
- setRepoName(suggestion.name);
- if (suggestion.repositoryUrl) {
- setRepoUrl(suggestion.repositoryUrl);
- }
- if (suggestion.localPath) {
- setRepoPath(suggestion.localPath);
- }
- setShowRepoSuggestions(false);
- }, []);
-
- // Fetch daemon directories when "local" repo type is selected
- useEffect(() => {
- if (repoType === "local" && isCreating) {
- getDaemonDirectories()
- .then((res) => setSuggestedDirectories(res.directories))
- .catch(() => setSuggestedDirectories([]));
- }
- }, [repoType, isCreating]);
-
- // Load contract detail when ID changes
- useEffect(() => {
- if (id) {
- setDetailLoading(true);
- fetchContract(id).then((contract) => {
- setContractDetail(contract);
- setDetailLoading(false);
- });
- } else {
- setContractDetail(null);
- }
- }, [id, fetchContract]);
-
- const handleSelect = useCallback(
- (contractId: string) => {
- navigate(`/contracts/${contractId}`);
- },
- [navigate]
- );
-
- const handleBack = useCallback(() => {
- navigate("/contracts");
- }, [navigate]);
-
- const handleCreate = useCallback(() => {
- setIsCreating(true);
- }, []);
-
- // Validate repository configuration
- const isRepoValid = useCallback(() => {
- if (!repoName.trim()) return false;
- if (repoType === "remote" && !repoUrl.trim()) return false;
- if (repoType === "local" && !repoPath.trim()) return false;
- return true;
- }, [repoType, repoName, repoUrl, repoPath]);
-
- const handleCreateSubmit = useCallback(async () => {
- if (!newContractName.trim()) return;
- if (!isRepoValid()) {
- setCreateError("Repository configuration is required");
- return;
- }
-
- 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 isCustomTemplate = selectedType && !selectedType.isBuiltin;
-
- const data: CreateContractRequest = {
- name: newContractName.trim(),
- description: newContractDescription.trim() || undefined,
- // For custom templates, send templateId instead of contractType
- contractType: isCustomTemplate ? undefined : contractType,
- templateId: isCustomTemplate ? contractType : undefined,
- initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined,
- localOnly: localOnly || undefined,
- };
-
- try {
- const contract = await saveContract(data);
- if (contract) {
- // Add the repository after contract creation
- try {
- if (repoType === "remote") {
- await addRemoteRepo(contract.id, {
- name: repoName.trim(),
- repositoryUrl: repoUrl.trim(),
- isPrimary: true,
- });
- } else if (repoType === "local") {
- await addLocalRepo(contract.id, {
- name: repoName.trim(),
- localPath: repoPath.trim(),
- isPrimary: true,
- });
- } else if (repoType === "managed") {
- await createManagedRepo(contract.id, {
- name: repoName.trim(),
- isPrimary: true,
- });
- }
- } catch (repoError) {
- console.error("Failed to add repository:", repoError);
- // Still navigate to the contract - repo can be added later
- }
-
- // Clear form state
- setIsCreating(false);
- setNewContractName("");
- setNewContractDescription("");
- setContractType("simple");
- setInitialPhase("plan");
- setRepoType("remote");
- setRepoName("");
- setRepoUrl("");
- setRepoPath("");
- setLocalOnly(false);
- navigate(`/contracts/${contract.id}`);
- }
- } catch (err) {
- setCreateError(err instanceof Error ? err.message : "Failed to create contract");
- }
- }, [
- newContractName,
- newContractDescription,
- contractType,
- contractTypes,
- initialPhase,
- repoType,
- repoName,
- repoUrl,
- repoPath,
- isRepoValid,
- saveContract,
- addRemoteRepo,
- addLocalRepo,
- createManagedRepo,
- navigate,
- ]);
-
- const handleCreateCancel = useCallback(() => {
- setIsCreating(false);
- setNewContractName("");
- setNewContractDescription("");
- setContractType("simple");
- setInitialPhase("plan");
- setRepoType("remote");
- setRepoName("");
- setRepoUrl("");
- setRepoPath("");
- setLocalOnly(false);
- setCreateError(null);
- }, []);
-
- const handleUpdate = useCallback(
- async (name: string, description: string) => {
- if (contractDetail) {
- const updated = await editContract(contractDetail.id, {
- name,
- description: description || undefined,
- version: contractDetail.version,
- });
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, editContract, fetchContract]
- );
-
- const handleDelete = useCallback(async () => {
- if (contractDetail && confirm("Are you sure you want to delete this contract?")) {
- const success = await removeContract(contractDetail.id);
- if (success) {
- navigate("/contracts");
- }
- }
- }, [contractDetail, removeContract, navigate]);
-
- const handlePhaseChange = useCallback(
- async (phase: ContractPhase) => {
- if (contractDetail) {
- const updated = await changePhase(contractDetail.id, phase);
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, changePhase, fetchContract]
- );
-
- const handleStatusChange = useCallback(
- async (status: ContractStatus) => {
- if (contractDetail) {
- const updated = await editContract(contractDetail.id, {
- status,
- version: contractDetail.version,
- });
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, editContract, fetchContract]
- );
-
- // Repository handlers
- const handleAddRemoteRepo = useCallback(
- async (name: string, url: string, isPrimary: boolean) => {
- if (contractDetail) {
- await addRemoteRepo(contractDetail.id, { name, repositoryUrl: url, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, addRemoteRepo, fetchContract]
- );
-
- const handleAddLocalRepo = useCallback(
- async (name: string, path: string, isPrimary: boolean) => {
- if (contractDetail) {
- await addLocalRepo(contractDetail.id, { name, localPath: path, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, addLocalRepo, fetchContract]
- );
-
- const handleCreateManagedRepo = useCallback(
- async (name: string, isPrimary: boolean) => {
- if (contractDetail) {
- await createManagedRepo(contractDetail.id, { name, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, createManagedRepo, fetchContract]
- );
-
- const handleDeleteRepo = useCallback(
- async (repoId: string) => {
- if (contractDetail) {
- await removeRepo(contractDetail.id, repoId);
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, removeRepo, fetchContract]
- );
-
- const handleSetRepoPrimary = useCallback(
- async (repoId: string) => {
- if (contractDetail) {
- await setRepoPrimary(contractDetail.id, repoId);
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, setRepoPrimary, fetchContract]
- );
-
- // Refresh contract detail (used after file/task operations)
- const handleRefresh = useCallback(async () => {
- if (contractDetail) {
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }, [contractDetail, fetchContract]);
-
- // File/task navigation handlers
- const handleFileSelect = useCallback(
- (fileId: string) => {
- if (contractDetail) {
- navigate(`/contracts/${contractDetail.id}/files/${fileId}`);
- }
- },
- [navigate, contractDetail]
- );
-
- const handleTaskSelect = useCallback(
- (taskId: string) => {
- navigate(`/exec/${taskId}`);
- },
- [navigate]
- );
-
- // Create task within contract context
- const handleTaskCreate = useCallback(
- async (name: string, plan: string, repositoryUrl?: string) => {
- if (!contractDetail) return;
- try {
- // Create the task with contract_id (task is automatically associated)
- const task = await createTask({
- contractId: contractDetail.id,
- name,
- plan,
- repositoryUrl,
- });
- // Refresh contract detail to show new task
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- // Navigate to the new task
- navigate(`/exec/${task.id}`);
- } catch (e) {
- console.error("Failed to create task:", e);
- alert(e instanceof Error ? e.message : "Failed to create task");
- }
- },
- [contractDetail, fetchContract, navigate]
- );
-
- // Context menu handlers for ContractList
- const handleContextMarkComplete = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "completed", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextMarkActive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "active", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextArchive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "archived", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextDelete = useCallback(
- async (contract: ContractSummary) => {
- if (confirm(`Are you sure you want to delete "${contract.name}"?`)) {
- const success = await removeContract(contract.id);
- if (success && contract.id === id) {
- navigate("/contracts");
- }
- }
- },
- [removeContract, id, navigate]
- );
-
- const handleContextGoToSupervisor = useCallback(
- (contract: ContractSummary) => {
- if (contract.supervisorTaskId) {
- navigate(`/exec/${contract.supervisorTaskId}`);
- }
- },
- [navigate]
- );
-
- return (
- <div className="relative z-10 h-screen flex flex-col overflow-hidden bg-[#0a1628]">
- <Masthead showNav />
- <main className="flex-1 flex overflow-hidden" style={{ height: "calc(100vh - 80px)" }}>
- {/* Left: Contract list */}
- <div className="w-[350px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col">
- <ContractList
- contracts={contracts}
- loading={loading}
- onSelect={handleSelect}
- onCreate={handleCreate}
- selectedId={id}
- onMarkComplete={handleContextMarkComplete}
- onMarkActive={handleContextMarkActive}
- onArchive={handleContextArchive}
- onDelete={handleContextDelete}
- onGoToSupervisor={handleContextGoToSupervisor}
- />
- </div>
-
- {/* Right: Detail or Create */}
- <div className="flex-1 overflow-hidden flex flex-col min-h-0">
- {error && (
- <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
- {error}
- </div>
- )}
-
- {/* Contract detail, creation form, or empty state */}
- <div className="flex-1 min-h-0 overflow-hidden">
- {isCreating ? (
- <div className="p-4 max-w-lg overflow-y-auto h-full bg-[#0a1628]">
- <h3 className="font-mono text-[10px] text-[#9bc3ff] uppercase tracking-wide mb-4">
- Create Contract
- </h3>
-
- {createError && (
- <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
- {createError}
- </div>
- )}
-
- <div className="space-y-4">
- {/* Contract name */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Contract Name
- </label>
- <input
- type="text"
- value={newContractName}
- onChange={(e) => setNewContractName(e.target.value)}
- placeholder="Contract name"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- autoFocus
- />
- </div>
-
- {/* Description */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Description (optional)
- </label>
- <textarea
- value={newContractDescription}
- onChange={(e) => setNewContractDescription(e.target.value)}
- placeholder="Describe what this contract is for..."
- rows={2}
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc] resize-none"
- />
- </div>
-
- {/* Contract Type */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Contract Type
- </label>
- {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-[rgba(117,170,252,0.2)] 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 */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Starting Phase
- </label>
- <select
- value={initialPhase}
- onChange={(e) => setInitialPhase(e.target.value as ContractPhase)}
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- >
- {(() => {
- const template = contractTypes.find((t) => t.id === contractType);
- return (template?.phases || []).map((phase) => {
- const displayName = template?.phaseNames?.[phase] || (phase.charAt(0).toUpperCase() + phase.slice(1));
- return (
- <option key={phase} value={phase}>
- {displayName}
- </option>
- );
- });
- })()}
- </select>
- <p className="mt-1 font-mono text-xs text-[#8b949e]">
- {contractType === "simple"
- ? "Start in Plan to define what to build, or Execute if already planned"
- : "Skip earlier phases if you already have requirements defined"}
- </p>
- </div>
-
- {/* Local-Only Mode */}
- <div className="space-y-2">
- <div className="flex items-center space-x-3">
- <button
- type="button"
- onClick={() => setLocalOnly(!localOnly)}
- className={`w-5 h-5 flex items-center justify-center border transition-colors ${
- localOnly
- ? "bg-[#0f3c78] border-[#75aafc] text-[#dbe7ff]"
- : "bg-[#0d1b2d] border-[rgba(117,170,252,0.2)] text-transparent"
- }`}
- >
- {localOnly && (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="3"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="w-3 h-3"
- >
- <polyline points="20 6 9 17 4 12" />
- </svg>
- )}
- </button>
- <label
- className="font-mono text-sm text-[#dbe7ff] cursor-pointer select-none"
- onClick={() => setLocalOnly(!localOnly)}
- >
- Local-Only Mode
- </label>
- </div>
- <p className="font-mono text-xs text-[#8b949e] pl-8">
- When enabled, tasks won't automatically push to remote or create PRs.
- Use patch files to export changes.
- </p>
- </div>
-
- {/* Repository Configuration */}
- <div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-3">
- Repository Configuration (Required)
- </label>
-
- {/* Repository type selector */}
- <div className="flex gap-2 mb-3">
- <button
- type="button"
- onClick={() => setRepoType("remote")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "remote"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Remote
- </button>
- <button
- type="button"
- onClick={() => setRepoType("local")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "local"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Local
- </button>
- <button
- type="button"
- onClick={() => setRepoType("managed")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "managed"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Managed
- </button>
- </div>
-
- {/* Repository suggestions */}
- {showRepoSuggestions && repoSuggestions.length > 0 && (
- <div className="mb-3">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Recent Repositories
- </label>
- <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
- {repoSuggestions.map((suggestion) => (
- <button
- key={suggestion.id}
- type="button"
- onClick={() => applyRepoSuggestion(suggestion)}
- className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
- >
- <div className="flex items-center justify-between">
- <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
- <span className="text-[10px] text-[#556677] ml-2">
- {suggestion.useCount}×
- </span>
- </div>
- <div className="text-[10px] text-[#556677] truncate">
- {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl}
- </div>
- </button>
- ))}
- </div>
- </div>
- )}
-
- {/* Repository name */}
- <div className="mb-3">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Repository Name
- </label>
- <input
- type="text"
- value={repoName}
- onChange={(e) => setRepoName(e.target.value)}
- placeholder="e.g., my-project"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- />
- </div>
-
- {/* Repository URL (for remote) */}
- {repoType === "remote" && (
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Repository URL
- </label>
- <input
- type="text"
- value={repoUrl}
- onChange={(e) => setRepoUrl(e.target.value)}
- placeholder="https://github.com/user/repo.git"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- />
- </div>
- )}
-
- {/* Repository path (for local) */}
- {repoType === "local" && (
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Local Path
- </label>
- <DirectoryInput
- value={repoPath}
- onChange={setRepoPath}
- suggestions={suggestedDirectories}
- placeholder="/path/to/repository"
- />
- </div>
- )}
-
- {/* Managed description */}
- {repoType === "managed" && (
- <p className="font-mono text-xs text-[#8b949e]">
- A managed repository will be created automatically by the daemon.
- </p>
- )}
- </div>
-
- {/* Actions */}
- <div className="flex gap-2 justify-end pt-2">
- <button
- onClick={handleCreateCancel}
- className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
- >
- Cancel
- </button>
- <button
- onClick={handleCreateSubmit}
- disabled={!newContractName.trim() || !isRepoValid()}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[rgba(117,170,252,0.2)] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Create
- </button>
- </div>
- </div>
- </div>
- ) : contractDetail ? (
- <ContractDetail
- contract={contractDetail}
- loading={detailLoading}
- onBack={handleBack}
- onUpdate={handleUpdate}
- onDelete={handleDelete}
- onPhaseChange={handlePhaseChange}
- onStatusChange={handleStatusChange}
- onFileSelect={handleFileSelect}
- onTaskSelect={handleTaskSelect}
- onTaskCreate={handleTaskCreate}
- onRefresh={handleRefresh}
- onAddRemoteRepo={handleAddRemoteRepo}
- onAddLocalRepo={handleAddLocalRepo}
- onCreateManagedRepo={handleCreateManagedRepo}
- onDeleteRepo={handleDeleteRepo}
- onSetRepoPrimary={handleSetRepoPrimary}
- />
- ) : (
- <div className="panel h-full flex items-center justify-center">
- <div className="text-center">
- <p className="font-mono text-sm text-[#555] mb-4">
- Select a contract or create a new one
- </p>
- <button
- onClick={handleCreate}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- + New Contract
- </button>
- </div>
- </div>
- )}
- </div>
- </div>
- </main>
- </div>
- );
-}
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 7b0a89b..a3ea969 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -1530,13 +1530,17 @@ export default function DocumentDirectivesPage() {
: null;
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ // h-screen + overflow-hidden so the page itself never scrolls; the
+ // sidebar and editor pane each manage their own scroll via flex-1
+ // children with overflow-y-auto. Previously we set
+ // height: calc(100vh - 80px) on <main>, which assumed an 80px masthead
+ // and quietly clipped content when the masthead was taller (or pushed
+ // the page below the viewport on shorter screens, which made the
+ // whole page scroll instead of the sidebar/editor independently).
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
- <main
- className="flex-1 flex overflow-hidden"
- style={{ height: "calc(100vh - 80px)" }}
- >
- {/* Left: file-tree sidebar */}
+ <main className="flex-1 flex min-h-0 overflow-hidden">
+ {/* Left: file-tree sidebar — independent scroll. */}
<div className="w-[260px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
<DocumentSidebar
directives={directives}
diff --git a/makima/frontend/src/routes/tmp.tsx b/makima/frontend/src/routes/tmp.tsx
index 69f13a2..c0c7365 100644
--- a/makima/frontend/src/routes/tmp.tsx
+++ b/makima/frontend/src/routes/tmp.tsx
@@ -53,7 +53,7 @@ export default function TmpTaskPage() {
if (authLoading) {
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
<main className="flex-1 flex items-center justify-center">
<p className="text-[#7788aa] font-mono text-sm">Loading...</p>
@@ -63,12 +63,9 @@ export default function TmpTaskPage() {
}
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
- <main
- className="flex-1 flex flex-col overflow-hidden"
- style={{ height: "calc(100vh - 80px)" }}
- >
+ <main className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Breadcrumb echoing the document-mode header style. */}
<div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
<div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">