diff options
| author | soryu <soryu@soryu.co> | 2026-05-01 23:56:51 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-01 23:56:51 +0100 |
| commit | e11759447b1ac00becfb1e979e488f7f9c9cf478 (patch) | |
| tree | f8a58368de3f6dda3f2f5c1af34e869a0e714205 /makima/frontend/src/components/contracts/ContractCliInput.tsx | |
| parent | 80085c7cfa9d679ed3e3fd54a7d55fa8ab1addef (diff) | |
| download | soryu-e11759447b1ac00becfb1e979e488f7f9c9cf478.tar.gz soryu-e11759447b1ac00becfb1e979e488f7f9c9cf478.zip | |
chore(cleanup): Phase 5 contracts removal + tmp directive + 30-day expiry + scroll fix (#118)
Sweeping cleanup across the surface and the wire. Net: -14k LOC of legacy
contracts code, plus the tmp/scroll/UX fixes the user asked for.
## Sidebar/editor independent scroll
Replace `height: calc(100vh - 80px)` (which assumed an 80px masthead and
quietly clipped or pushed the whole page below the fold when the masthead
was taller) with `h-screen + overflow-hidden` on the page root and proper
`flex-1 min-h-0` sizing on `<main>`. Sidebar and editor pane now manage
their own scroll independently; the page itself never scrolls.
Same fix in /tmp/:taskId.
## tmp directive — real backing for orphans/ephemerals
New migration `20260501100000_tmp_directive_and_clear_orphans.sql`:
* Adds `directives.is_tmp` BOOLEAN NOT NULL DEFAULT false.
* Partial unique index `(owner_id) WHERE is_tmp` — at most ONE tmp
directive per owner.
* Hard-deletes every existing orphan task (`directive_id IS NULL`).
Per the user spec: "ALSO there are TOO MANY old tasks in tmp, we
need to remove all of them as well."
New repository helpers:
* `get_or_create_tmp_directive(pool, owner_id) -> Directive`
INSERT ON CONFLICT DO NOTHING + fallback SELECT, race-safe.
* `list_all_tmp_directives` — drives the expiry sweep.
* `delete_expired_tmp_tasks(tmp_directive_id) -> u64`.
* `list_tmp_tasks_for_owner` (replaces `list_orphan_tasks_for_owner`).
`mesh::create_task`: every top-level task must have a directive. If a
caller doesn't supply `directive_id` and isn't a subtask, attach to the
caller's tmp directive (auto-creating it on first use).
`list_directives_for_owner` filters out `is_tmp=true` so the scratchpad
directive doesn't pollute the contract list — surfaced via the sidebar's
`tmp/` folder instead.
## 30-day expiry on tmp tasks
New `phase_tmp_expiry` in the directive reconciler. Throttled to once per
hour: enumerates every tmp directive, calls `delete_expired_tmp_tasks`,
logs the count. The actual delete is `WHERE created_at < NOW() - INTERVAL
'30 days'` and is fast on the existing index. Subtasks die via FK cascade.
## Phase 5 — contracts removed
### Frontend
Deleted entire `/contracts` surface:
* routes: `contracts.tsx`, `contract-file.tsx`
* components/contracts: ContractList, ContractDetail, ContractCliInput,
ContractContextMenu, CommandModePanel, PhaseBadge, PhaseHint,
PhaseDeliverablesPanel, PhaseProgressBar, QuickActionButtons,
RepositoryPanel, TaskDerivationPreview
* (Kept `PhaseConfirmationModal` — used outside the contracts surface
by `TaskOutput` and `PhaseConfirmationNotification`.)
* Routes deregistered from `main.tsx`; nav entry removed from
`NavStrip`.
### Backend handlers
Deleted: `contracts.rs` (2.4k LOC), `contract_chat.rs` (3.2k LOC),
`contract_daemon.rs` (~940 LOC), `contract_discuss.rs` (~590 LOC),
`transcript_analysis.rs` (~690 LOC). All `/api/v1/contracts/*` routes
deregistered. OpenAPI entries dropped. Module declarations removed from
`server/handlers/mod.rs`.
### CLI
Removed `makima contract` and `makima supervisor` subcommands. Deleted
`daemon/cli/contract.rs` and `daemon/cli/supervisor.rs`. Bin dispatch
trimmed (~377 LOC).
### Orchestrator
Removed the contract-spawn path from `phase_execution`
(`spawn_step_contract` and its caller). `directive_steps.contract_type`
now logs a warning and falls through to standalone-task spawn. Column
itself stays — old data still reads, just no longer triggers a
contract+supervisor spawn.
### TUI
`Action::PerformCreateContract` is now a no-op that surfaces a status
message: "Contracts have been removed. Use directives instead." The TUI
form is dead code pending a wider refresh.
## Out of scope (deliberately left)
* Contracts DB tables (`contracts`, `contract_repositories`,
`contract_chat_history`, `contract_events`, `contract_templates`) are
retained for historical data + because some peripheral code still
joins to them in TaskSummary queries.
* `mesh_supervisor` handlers are retained — they aren't only used by
contracts (some mesh-level supervisor behaviour persists), and the
cross-cutting cleanup is bigger than this PR.
* `directive_steps.contract_type` column itself isn't dropped; just no
longer functional.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/components/contracts/ContractCliInput.tsx')
| -rw-r--r-- | makima/frontend/src/components/contracts/ContractCliInput.tsx | 974 |
1 files changed, 0 insertions, 974 deletions
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]">></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">></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> - ); -} |
