diff options
| author | soryu <soryu@soryu.co> | 2026-01-17 19:47:35 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-17 19:47:35 +0000 |
| commit | 900472091e4d9b4000508b0d266d786ef41107bd (patch) | |
| tree | 55fb5ec80b8df6693a8a2960071148dfd88e928a /makima/frontend | |
| parent | 2f62df1cc89a23a5bd30e1a3f68a39bcfce9665c (diff) | |
| download | soryu-900472091e4d9b4000508b0d266d786ef41107bd.tar.gz soryu-900472091e4d9b4000508b0d266d786ef41107bd.zip | |
Add phase guard toggle for contract phase confirmation (#2)
* Add phase_guard field to Contract model and database
This adds a new boolean field to control whether the supervisor should
wait for user confirmation before progressing to the next phase. When
enabled, users can review and potentially amend phase outputs (like
plans, requirements docs) before the contract continues.
Changes:
- Add migration for phase_guard column (defaults to false)
- Add phase_guard to Contract, CreateContractRequest, and
UpdateContractRequest structs
- Update create_contract_for_owner and update_contract_for_owner
repository functions to handle phase_guard
- Update all CreateContractRequest instantiations with phase_guard field
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: Add phase_guard for contract phase transitions
Implement phase_guard logic in the advance_phase tool. When a contract
has phase_guard enabled, the phase transition now:
1. Asks for user confirmation before advancing
2. Allows users to request changes to phase deliverables
3. Passes feedback to the task without advancing if changes requested
Changes:
- Add phase_guard field to Contract model and CreateContractRequest
- Add PhaseTransitionRequest, PhaseFileInfo, PhaseTaskInfo structs
- Update ChangePhaseRequest with confirmed and feedback fields
- Update ContractToolRequest::AdvancePhase with confirmed/feedback
- Modify advance_phase handling in contract_chat.rs with phase_guard logic
- Update change_phase endpoint in contracts.rs with phase_guard support
- Add database migration for phase_guard column
When phase_guard=false: Phase advances immediately (current behavior)
When phase_guard=true: Returns pending_confirmation status with deliverables
If user provides feedback: Returns feedback to task, doesn't advance
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(frontend): Add UI for phase transition confirmation requests
When phase_guard is enabled and a supervisor tries to advance the contract
phase, users now see a confirmation modal with:
- Current and proposed next phase visualization
- Phase deliverables checklist (if available)
- Summary of the phase work
- Options to "Approve & Advance" or "Request Changes" with feedback
Components added:
- PhaseConfirmationModal: Full modal dialog for phase confirmations
- PhaseConfirmationInline: Inline variant for task output view
- PhaseConfirmationNotification: Global notification wrapper
- PhaseConfirmationToast: Alternative toast-style notification
Integration:
- Added phase_confirmation message type to TaskOutput renderer
- Extended PendingQuestion API type with phase confirmation data
- Integrated notification into main app layout
The UI uses the existing supervisor question infrastructure (polling via
/api/v1/mesh/questions) and responds with APPROVE or CHANGES_REQUESTED
prefixed feedback.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(frontend): Add Phase Guard toggle to AutopilotPanel
Added the phase_guard toggle to the AutopilotPanel component, which allows
users to enable/disable requiring confirmation before phase transitions.
Changes:
- Added phaseGuard and autonomousLoop fields to Contract interface in api.ts
- Added phaseGuard field to UpdateContractRequest in api.ts
- Added Phase Guard toggle UI in AutopilotPanel with similar styling to master
- Toggle shows an 'active' badge when enabled
- Connected toggle to contract update API
The toggle appears below the autopilot control buttons and allows users to
require confirmation before the supervisor advances to the next phase.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/PhaseConfirmationNotification.tsx | 141 | ||||
| -rw-r--r-- | makima/frontend/src/components/contracts/AutopilotPanel.tsx | 50 | ||||
| -rw-r--r-- | makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx | 411 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskOutput.tsx | 66 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 21 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 2 |
6 files changed, 691 insertions, 0 deletions
diff --git a/makima/frontend/src/components/PhaseConfirmationNotification.tsx b/makima/frontend/src/components/PhaseConfirmationNotification.tsx new file mode 100644 index 0000000..516211f --- /dev/null +++ b/makima/frontend/src/components/PhaseConfirmationNotification.tsx @@ -0,0 +1,141 @@ +import { useNavigate } from "react-router"; +import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; +import { PhaseConfirmationModal, type PhaseConfirmationData } from "./contracts/PhaseConfirmationModal"; +import type { PendingQuestion } from "../lib/api"; + +/** + * Notification component for phase confirmation requests. + * Shows a modal when there are pending phase_confirmation type questions. + * Uses the same question infrastructure as supervisor questions. + */ +export function PhaseConfirmationNotification() { + const { notificationQuestions, submitAnswer, dismissNotification } = + useSupervisorQuestions(); + + // Filter for phase_confirmation type questions + const phaseConfirmationQuestions = notificationQuestions.filter( + (q) => q.questionType === "phase_confirmation" + ); + + if (phaseConfirmationQuestions.length === 0) { + return null; + } + + // Show the first phase confirmation question as a modal + const question = phaseConfirmationQuestions[0]; + + // Build phase confirmation data from the question + const data: PhaseConfirmationData = { + questionId: question.questionId, + contractId: question.contractId, + contractName: question.phaseConfirmation?.contractName, + currentPhase: question.phaseConfirmation?.currentPhase || "research", + nextPhase: question.phaseConfirmation?.nextPhase || "specify", + summary: question.phaseConfirmation?.summary, + deliverables: question.phaseConfirmation?.deliverables, + }; + + const handleApprove = async (questionId: string) => { + const success = await submitAnswer(questionId, "APPROVE"); + if (success) { + dismissNotification(questionId); + } + }; + + const handleRequestChanges = async (questionId: string, feedback: string) => { + const success = await submitAnswer( + questionId, + `CHANGES_REQUESTED: ${feedback}` + ); + if (success) { + dismissNotification(questionId); + } + }; + + const handleDismiss = () => { + // Dismiss to notification (user can still respond via task output) + dismissNotification(question.questionId); + }; + + return ( + <PhaseConfirmationModal + data={data} + onApprove={handleApprove} + onRequestChanges={handleRequestChanges} + onDismiss={handleDismiss} + /> + ); +} + +/** + * Alternative: Notification toast-style for phase confirmations + * Shows as a small notification in the corner (like regular supervisor questions) + */ +export function PhaseConfirmationToast() { + const navigate = useNavigate(); + const { notificationQuestions, dismissNotification } = useSupervisorQuestions(); + + // Filter for phase_confirmation type questions + const phaseConfirmationQuestions = notificationQuestions.filter( + (q) => q.questionType === "phase_confirmation" + ); + + if (phaseConfirmationQuestions.length === 0) { + return null; + } + + const handleGoToTask = (question: PendingQuestion) => { + dismissNotification(question.questionId); + navigate(`/mesh/${question.taskId}`); + }; + + return ( + <div className="fixed bottom-4 right-4 z-50 max-w-md space-y-2"> + {phaseConfirmationQuestions.map((question) => ( + <div + key={question.questionId} + className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.5)] rounded-lg shadow-lg overflow-hidden" + > + {/* Header */} + <div className="flex items-center justify-between px-4 py-3 bg-[rgba(117,170,252,0.1)]"> + <div className="flex items-center gap-2"> + <span className="text-[#75aafc] text-lg">?</span> + <span className="font-mono text-sm text-[#9bc3ff] uppercase"> + Phase Transition + </span> + </div> + <button + onClick={() => handleGoToTask(question)} + className="px-3 py-1 font-mono text-xs text-[#75aafc] border border-[rgba(117,170,252,0.3)] hover:border-[rgba(117,170,252,0.5)] hover:bg-[rgba(117,170,252,0.1)] transition-colors uppercase" + > + Review + </button> + </div> + + {/* Content preview */} + <div className="px-4 py-3"> + {question.phaseConfirmation && ( + <div className="flex items-center gap-2 mb-2"> + <span className="font-mono text-xs text-purple-400"> + {question.phaseConfirmation.currentPhase} + </span> + <span className="text-[#555] font-mono text-xs">→</span> + <span className="font-mono text-xs text-green-400"> + {question.phaseConfirmation.nextPhase} + </span> + </div> + )} + <p className="text-sm text-[#dbe7ff] font-mono line-clamp-2"> + {question.question} + </p> + {question.phaseConfirmation?.contractName && ( + <p className="text-xs text-[#555] font-mono mt-1"> + Contract: {question.phaseConfirmation.contractName} + </p> + )} + </div> + </div> + ))} + </div> + ); +} diff --git a/makima/frontend/src/components/contracts/AutopilotPanel.tsx b/makima/frontend/src/components/contracts/AutopilotPanel.tsx index a8a8e2e..cf42e44 100644 --- a/makima/frontend/src/components/contracts/AutopilotPanel.tsx +++ b/makima/frontend/src/components/contracts/AutopilotPanel.tsx @@ -5,6 +5,7 @@ import { startSupervisor, stopSupervisor, resumeSupervisor, + updateContract, type SupervisorStatus, } from "../../lib/api"; @@ -111,6 +112,23 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) { } }, [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]); + // Don't show panel for task-type contracts (they don't have supervisors) if (contract.contractType === "task") { return null; @@ -185,6 +203,38 @@ export function AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) { )} </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)]"> diff --git a/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx new file mode 100644 index 0000000..96cc1d4 --- /dev/null +++ b/makima/frontend/src/components/contracts/PhaseConfirmationModal.tsx @@ -0,0 +1,411 @@ +import { useState } from "react"; +import type { ContractPhase } from "../../lib/api"; + +/** Phase configuration for styling */ +const phaseConfig: Record< + ContractPhase, + { label: string; color: string; borderColor: string } +> = { + research: { + label: "Research", + color: "text-purple-400", + borderColor: "border-purple-400/50", + }, + specify: { + label: "Specify", + color: "text-blue-400", + borderColor: "border-blue-400/50", + }, + plan: { + label: "Plan", + color: "text-cyan-400", + borderColor: "border-cyan-400/50", + }, + execute: { + label: "Execute", + color: "text-green-400", + borderColor: "border-green-400/50", + }, + review: { + label: "Review", + color: "text-yellow-400", + borderColor: "border-yellow-400/50", + }, +}; + +export interface PhaseConfirmationData { + questionId: string; + contractId: string; + contractName?: string; + currentPhase: ContractPhase; + nextPhase: ContractPhase; + summary?: string; + deliverables?: Array<{ + name: string; + completed: boolean; + }>; +} + +interface PhaseConfirmationModalProps { + data: PhaseConfirmationData; + onApprove: (questionId: string) => Promise<void>; + onRequestChanges: (questionId: string, feedback: string) => Promise<void>; + onDismiss?: () => void; +} + +export function PhaseConfirmationModal({ + data, + onApprove, + onRequestChanges, + onDismiss, +}: PhaseConfirmationModalProps) { + const [mode, setMode] = useState<"choice" | "feedback">("choice"); + const [feedback, setFeedback] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState<string | null>(null); + + const currentConfig = phaseConfig[data.currentPhase]; + const nextConfig = phaseConfig[data.nextPhase]; + + const handleApprove = async () => { + setSubmitting(true); + setError(null); + try { + await onApprove(data.questionId); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to approve"); + } finally { + setSubmitting(false); + } + }; + + const handleRequestChanges = () => { + setMode("feedback"); + }; + + const handleSubmitFeedback = async () => { + if (!feedback.trim()) return; + setSubmitting(true); + setError(null); + try { + await onRequestChanges(data.questionId, feedback.trim()); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to submit feedback"); + } finally { + setSubmitting(false); + } + }; + + const handleBack = () => { + setMode("choice"); + setFeedback(""); + setError(null); + }; + + return ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"> + <div className="w-full max-w-lg mx-4 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-2xl"> + {/* Header */} + <div className="flex items-center justify-between p-4 border-b border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center gap-2"> + <span className="text-amber-400 text-lg">?</span> + <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wide"> + Phase Transition Confirmation + </h2> + </div> + {onDismiss && ( + <button + onClick={onDismiss} + className="text-[#555] hover:text-[#9bc3ff] transition-colors" + aria-label="Dismiss" + > + <span className="text-xl">×</span> + </button> + )} + </div> + + {/* Content */} + <div className="p-4 space-y-4"> + {/* Contract name */} + {data.contractName && ( + <p className="font-mono text-sm text-[#9bc3ff]"> + Contract: <span className="text-[#dbe7ff]">{data.contractName}</span> + </p> + )} + + {/* Phase transition indicator */} + <div className="flex items-center justify-center gap-3 py-4"> + <div + className={`px-4 py-2 border ${currentConfig.borderColor} ${currentConfig.color} font-mono text-sm uppercase`} + > + {currentConfig.label} + </div> + <div className="text-[#555] font-mono">→</div> + <div + className={`px-4 py-2 border ${nextConfig.borderColor} ${nextConfig.color} font-mono text-sm uppercase`} + > + {nextConfig.label} + </div> + </div> + + {/* Summary */} + {data.summary && ( + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3"> + <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase"> + Phase Summary + </p> + <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap"> + {data.summary} + </p> + </div> + )} + + {/* Deliverables checklist */} + {data.deliverables && data.deliverables.length > 0 && ( + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.15)] p-3"> + <p className="font-mono text-xs text-[#9bc3ff] mb-2 uppercase"> + Phase Deliverables + </p> + <ul className="space-y-1"> + {data.deliverables.map((d, idx) => ( + <li key={idx} className="flex items-center gap-2"> + <span + className={ + d.completed ? "text-green-400" : "text-[#555]" + } + > + {d.completed ? "+" : "-"} + </span> + <span + className={`font-mono text-sm ${ + d.completed ? "text-[#9bc3ff]" : "text-[#555]" + }`} + > + {d.name} + </span> + </li> + ))} + </ul> + </div> + )} + + {/* Error message */} + {error && ( + <div className="bg-red-900/20 border border-red-500/30 p-3 text-red-400 font-mono text-sm"> + {error} + </div> + )} + + {/* Choice mode */} + {mode === "choice" && ( + <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]"> + <p className="font-mono text-sm text-[#dbe7ff]"> + Ready to advance to the {nextConfig.label.toLowerCase()} phase? + </p> + <div className="flex gap-3"> + <button + onClick={handleApprove} + disabled={submitting} + className="flex-1 px-4 py-2.5 font-mono text-sm text-[#dbe7ff] bg-green-700/30 border border-green-400/50 hover:bg-green-700/40 hover:border-green-400/70 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase" + > + {submitting ? "Advancing..." : "Approve & Advance"} + </button> + <button + onClick={handleRequestChanges} + disabled={submitting} + className="flex-1 px-4 py-2.5 font-mono text-sm text-amber-300 border border-amber-500/50 hover:border-amber-400/70 hover:bg-amber-900/20 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase" + > + Request Changes + </button> + </div> + </div> + )} + + {/* Feedback mode */} + {mode === "feedback" && ( + <div className="space-y-3 pt-4 border-t border-[rgba(117,170,252,0.2)]"> + <div> + <label className="block font-mono text-xs text-[#9bc3ff] uppercase mb-2"> + What changes are needed before advancing? + </label> + <textarea + value={feedback} + onChange={(e) => setFeedback(e.target.value)} + placeholder="Describe what needs to be changed or completed..." + rows={4} + 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" + autoFocus + /> + </div> + <div className="flex gap-3"> + <button + onClick={handleBack} + disabled={submitting} + className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors uppercase" + > + ← Back + </button> + <button + onClick={handleSubmitFeedback} + disabled={submitting || !feedback.trim()} + className="flex-1 px-4 py-2 font-mono text-sm text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase" + > + {submitting ? "Submitting..." : "Submit Feedback"} + </button> + </div> + </div> + )} + </div> + </div> + </div> + ); +} + +/** Inline variant for task output view */ +interface PhaseConfirmationInlineProps { + data: PhaseConfirmationData; + isPending: boolean; + onApprove: (questionId: string) => Promise<void>; + onRequestChanges: (questionId: string, feedback: string) => Promise<void>; +} + +export function PhaseConfirmationInline({ + data, + isPending, + onApprove, + onRequestChanges, +}: PhaseConfirmationInlineProps) { + const [mode, setMode] = useState<"choice" | "feedback">("choice"); + const [feedback, setFeedback] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const currentConfig = phaseConfig[data.currentPhase]; + const nextConfig = phaseConfig[data.nextPhase]; + + const handleApprove = async () => { + setSubmitting(true); + try { + await onApprove(data.questionId); + } finally { + setSubmitting(false); + } + }; + + const handleSubmitFeedback = async () => { + if (!feedback.trim()) return; + setSubmitting(true); + try { + await onRequestChanges(data.questionId, feedback.trim()); + setFeedback(""); + } finally { + setSubmitting(false); + } + }; + + return ( + <div className="bg-[#0f1825] border border-[rgba(117,170,252,0.4)] rounded p-3 my-2"> + <div className="flex items-center gap-2 mb-3"> + <span className="text-[#75aafc] text-lg">?</span> + <span className="font-mono text-sm text-[#75aafc] uppercase"> + Phase Transition + </span> + {!isPending && ( + <span className="text-green-400 text-xs font-mono">(Responded)</span> + )} + </div> + + {/* Phase transition indicator */} + <div className="flex items-center gap-2 mb-3"> + <span className={`font-mono text-sm ${currentConfig.color}`}> + {currentConfig.label} + </span> + <span className="text-[#555] font-mono">→</span> + <span className={`font-mono text-sm ${nextConfig.color}`}> + {nextConfig.label} + </span> + </div> + + {/* Summary */} + {data.summary && ( + <p className="font-mono text-xs text-[#9bc3ff] mb-3 whitespace-pre-wrap"> + {data.summary} + </p> + )} + + {/* Deliverables */} + {data.deliverables && data.deliverables.length > 0 && ( + <div className="mb-3"> + <p className="font-mono text-[10px] text-[#555] uppercase mb-1"> + Deliverables + </p> + <div className="flex flex-wrap gap-2"> + {data.deliverables.map((d, idx) => ( + <span + key={idx} + className={`px-2 py-0.5 font-mono text-xs border ${ + d.completed + ? "border-green-400/30 text-green-400" + : "border-[#555] text-[#555]" + }`} + > + {d.completed ? "+" : "-"} {d.name} + </span> + ))} + </div> + </div> + )} + + {isPending && ( + <> + {mode === "choice" && ( + <div className="flex flex-wrap gap-2"> + <button + onClick={handleApprove} + disabled={submitting} + className="px-3 py-1.5 font-mono text-xs bg-green-700/30 border border-green-400/50 text-green-300 hover:bg-green-700/40 disabled:opacity-50 transition-colors" + > + {submitting ? "..." : "Approve & Advance"} + </button> + <button + onClick={() => setMode("feedback")} + disabled={submitting} + className="px-3 py-1.5 font-mono text-xs border border-amber-500/50 text-amber-300 hover:bg-amber-900/20 disabled:opacity-50 transition-colors" + > + Request Changes + </button> + </div> + )} + + {mode === "feedback" && ( + <div className="space-y-2"> + <textarea + value={feedback} + onChange={(e) => setFeedback(e.target.value)} + placeholder="Describe what changes are needed..." + rows={2} + className="w-full px-2 py-1.5 bg-[#0a1525] border border-[rgba(117,170,252,0.3)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#75aafc] resize-none" + autoFocus + /> + <div className="flex gap-2"> + <button + onClick={() => setMode("choice")} + disabled={submitting} + className="px-2 py-1 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleSubmitFeedback} + disabled={submitting || !feedback.trim()} + className="px-3 py-1 font-mono text-xs bg-[#0f3c78] border border-[#3f6fb3] text-[#dbe7ff] hover:bg-[#153667] disabled:opacity-50 disabled:cursor-not-allowed transition-colors" + > + {submitting ? "..." : "Submit"} + </button> + </div> + </div> + )} + </> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx index d53429d..7140c8a 100644 --- a/makima/frontend/src/components/mesh/TaskOutput.tsx +++ b/makima/frontend/src/components/mesh/TaskOutput.tsx @@ -2,6 +2,11 @@ import { useRef, useEffect, useState, useCallback } from "react"; import { SimpleMarkdown } from "../SimpleMarkdown"; import type { TaskOutputEvent } from "../../hooks/useTaskSubscription"; import { sendTaskMessage } from "../../lib/api"; +import { + PhaseConfirmationInline, + type PhaseConfirmationData, +} from "../contracts/PhaseConfirmationModal"; +import type { ContractPhase } from "../../lib/api"; interface TaskOutputProps { /** Array of parsed output events from the backend */ @@ -312,6 +317,15 @@ function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: Ou /> ); + case "phase_confirmation": + return ( + <PhaseConfirmationEntry + entry={entry} + pendingQuestionIds={pendingQuestionIds} + onAnswerQuestion={onAnswerQuestion} + /> + ); + default: return null; } @@ -520,3 +534,55 @@ function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) { </div> ); } + +/** Entry for phase transition confirmations */ +function PhaseConfirmationEntry({ + entry, + pendingQuestionIds, + onAnswerQuestion, +}: { + entry: TaskOutputEvent; + pendingQuestionIds?: Set<string>; + onAnswerQuestion?: (questionId: string, response: string) => Promise<void>; +}) { + const questionId = entry.toolInput?.question_id as string; + const currentPhase = entry.toolInput?.current_phase as ContractPhase; + const nextPhase = entry.toolInput?.next_phase as ContractPhase; + const contractId = entry.toolInput?.contract_id as string; + const contractName = entry.toolInput?.contract_name as string | undefined; + const summary = entry.toolInput?.summary as string | undefined; + const deliverables = entry.toolInput?.deliverables as + | Array<{ name: string; completed: boolean }> + | undefined; + + const isPending = pendingQuestionIds?.has(questionId) ?? false; + + const data: PhaseConfirmationData = { + questionId, + contractId, + contractName, + currentPhase, + nextPhase, + summary, + deliverables, + }; + + const handleApprove = async (qId: string) => { + if (!onAnswerQuestion) return; + await onAnswerQuestion(qId, "APPROVE"); + }; + + const handleRequestChanges = async (qId: string, feedback: string) => { + if (!onAnswerQuestion) return; + await onAnswerQuestion(qId, `CHANGES_REQUESTED: ${feedback}`); + }; + + return ( + <PhaseConfirmationInline + data={data} + isPending={isPending} + onApprove={handleApprove} + onRequestChanges={handleRequestChanges} + /> + ); +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index ee04935..9b725bc 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -1478,6 +1478,10 @@ export interface Contract { status: ContractStatus; /** Supervisor task ID for contract orchestration */ supervisorTaskId: string | null; + /** Whether tasks for this contract should run in autonomous loop mode */ + autonomousLoop: boolean; + /** Whether to wait for user confirmation before progressing to the next phase */ + phaseGuard: boolean; version: number; createdAt: string; updatedAt: string; @@ -1518,6 +1522,10 @@ export interface UpdateContractRequest { description?: string; phase?: ContractPhase; status?: ContractStatus; + /** Enable or disable autonomous loop mode */ + autonomousLoop?: boolean; + /** Enable or disable phase guard mode */ + phaseGuard?: boolean; version?: number; } @@ -1938,6 +1946,19 @@ export interface PendingQuestion { choices: string[]; context: string | null; createdAt: string; + /** Question type - "general" for regular questions, "phase_confirmation" for phase transitions */ + questionType?: "general" | "phase_confirmation"; + /** Phase confirmation specific data (when questionType is "phase_confirmation") */ + phaseConfirmation?: { + currentPhase: ContractPhase; + nextPhase: ContractPhase; + contractName?: string; + summary?: string; + deliverables?: Array<{ + name: string; + completed: boolean; + }>; + }; } export interface AnswerQuestionRequest { diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index 5fd6a4e..19f02d1 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -6,6 +6,7 @@ import { AuthProvider } from "./contexts/AuthContext"; import { SupervisorQuestionsProvider } from "./contexts/SupervisorQuestionsContext"; import { GridOverlay } from "./components/GridOverlay"; import { SupervisorQuestionNotification } from "./components/SupervisorQuestionNotification"; +import { PhaseConfirmationNotification } from "./components/PhaseConfirmationNotification"; import { ProtectedRoute } from "./components/ProtectedRoute"; import HomePage from "./routes/_index"; import ListenPage from "./routes/listen"; @@ -24,6 +25,7 @@ createRoot(document.getElementById("root")!).render( <BrowserRouter> <GridOverlay /> <SupervisorQuestionNotification /> + <PhaseConfirmationNotification /> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<LoginPage />} /> |
