summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/contracts/AutopilotPanel.tsx208
-rw-r--r--makima/frontend/src/components/contracts/ContractDetail.tsx7
-rw-r--r--makima/frontend/src/lib/api.ts114
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
4 files changed, 330 insertions, 1 deletions
diff --git a/makima/frontend/src/components/contracts/AutopilotPanel.tsx b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
new file mode 100644
index 0000000..a8a8e2e
--- /dev/null
+++ b/makima/frontend/src/components/contracts/AutopilotPanel.tsx
@@ -0,0 +1,208 @@
+import { useState, useCallback } from "react";
+import type { ContractWithRelations } from "../../lib/api";
+import {
+ getSupervisorStatus,
+ startSupervisor,
+ stopSupervisor,
+ resumeSupervisor,
+ type SupervisorStatus,
+} from "../../lib/api";
+
+interface AutopilotPanelProps {
+ 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 AutopilotPanel({ contract, onUpdate }: AutopilotPanelProps) {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const supervisorStatus = getSupervisorStatus(contract);
+ 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 autopilot");
+ } 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 autopilot");
+ } 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 autopilot");
+ } finally {
+ setLoading(false);
+ }
+ }, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);
+
+ // Don't show panel for task-type contracts (they don't have supervisors)
+ if (contract.contractType === "task") {
+ return null;
+ }
+
+ return (
+ <div className="space-y-3">
+ <div className="flex items-center justify-between">
+ <h3 className="font-mono text-xs text-[#75aafc] uppercase">
+ Autopilot Mode
+ </h3>
+ <div
+ className={`px-2 py-1 rounded font-mono text-xs ${config.color} ${config.bgColor}`}
+ >
+ {config.label}
+ </div>
+ </div>
+
+ <p className="font-mono text-xs text-[#555]">
+ {supervisorStatus.status === "not_configured" ? (
+ "This contract does not have an autopilot supervisor configured."
+ ) : supervisorStatus.status === "running" ? (
+ "Autopilot is actively working on this contract, spawning tasks and managing progress."
+ ) : supervisorStatus.status === "pending" ? (
+ "Autopilot is ready to start. Click 'Enable' to begin autonomous work."
+ ) : supervisorStatus.status === "paused" ? (
+ "Autopilot is paused. Click 'Resume' to continue work."
+ ) : supervisorStatus.status === "failed" ? (
+ "Autopilot encountered an error. You can resume to retry."
+ ) : supervisorStatus.status === "done" ? (
+ "Autopilot has completed its work on this contract."
+ ) : (
+ "Autopilot 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 Autopilot"}
+ </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 Autopilot"}
+ </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 Autopilot"}
+ </button>
+ )}
+ </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">
+ Autopilot 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 autopilot...
+ </span>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx
index cf5f8f2..f93097a 100644
--- a/makima/frontend/src/components/contracts/ContractDetail.tsx
+++ b/makima/frontend/src/components/contracts/ContractDetail.tsx
@@ -18,6 +18,7 @@ import { PhaseHint } from "./PhaseHint";
import { RepositoryPanel } from "./RepositoryPanel";
import { ContractCliInput } from "./ContractCliInput";
import { PhaseDeliverablesPanel } from "./PhaseDeliverablesPanel";
+import { AutopilotPanel } from "./AutopilotPanel";
import { TaskTree } from "../mesh/TaskTree";
type Tab = "overview" | "repos" | "files" | "tasks";
@@ -225,6 +226,7 @@ export function ContractDetail({
onStatusChange={onStatusChange}
onPhaseChange={onPhaseChange}
onCreateFile={onCreateFileFromTemplate}
+ onRefresh={onRefresh}
/>
)}
@@ -276,14 +278,19 @@ function OverviewTab({
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">
+ {/* Autopilot controls */}
+ <AutopilotPanel contract={contract} onUpdate={onRefresh} />
+
{/* Phase deliverables checklist */}
<PhaseDeliverablesPanel
contract={contract}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 9c56f6b..ee04935 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -2492,3 +2492,117 @@ export async function resumeFromCheckpoint(
}
return res.json();
}
+
+// =============================================================================
+// Supervisor/Autopilot Control Functions
+// =============================================================================
+
+/**
+ * Start a contract's supervisor task (enable autopilot mode).
+ * This is a convenience wrapper around startTask.
+ */
+export async function startSupervisor(supervisorTaskId: string): Promise<Task> {
+ return startTask(supervisorTaskId);
+}
+
+/**
+ * Stop a contract's supervisor task (pause autopilot mode).
+ * This is a convenience wrapper around stopTask.
+ */
+export async function stopSupervisor(supervisorTaskId: string): Promise<Task> {
+ return stopTask(supervisorTaskId);
+}
+
+/** Status of the supervisor/autopilot for a contract */
+export interface SupervisorStatus {
+ supervisorTaskId: string | null;
+ status: "not_configured" | "pending" | "starting" | "running" | "paused" | "done" | "failed";
+ daemonId: string | null;
+ canStart: boolean;
+ canStop: boolean;
+ canResume: boolean;
+}
+
+/**
+ * Get the supervisor status for a contract.
+ */
+export function getSupervisorStatus(
+ contract: ContractWithRelations
+): SupervisorStatus {
+ const supervisorTaskId = contract.supervisorTaskId;
+
+ if (!supervisorTaskId) {
+ return {
+ supervisorTaskId: null,
+ status: "not_configured",
+ daemonId: null,
+ canStart: false,
+ canStop: false,
+ canResume: false,
+ };
+ }
+
+ // Find the supervisor task in the contract's tasks
+ const supervisorTask = contract.tasks.find(
+ (t) => t.id === supervisorTaskId && t.isSupervisor
+ );
+
+ if (!supervisorTask) {
+ return {
+ supervisorTaskId,
+ status: "pending",
+ daemonId: null,
+ canStart: true,
+ canStop: false,
+ canResume: false,
+ };
+ }
+
+ // Map task status to supervisor status
+ let status: SupervisorStatus["status"];
+ let canStart = false;
+ let canStop = false;
+ let canResume = false;
+
+ switch (supervisorTask.status) {
+ case "pending":
+ status = "pending";
+ canStart = true;
+ break;
+ case "initializing":
+ case "starting":
+ status = "starting";
+ canStop = true;
+ break;
+ case "running":
+ status = "running";
+ canStop = true;
+ break;
+ case "paused":
+ case "blocked":
+ status = "paused";
+ canResume = true;
+ canStop = true;
+ break;
+ case "done":
+ case "merged":
+ status = "done";
+ break;
+ case "failed":
+ status = "failed";
+ canResume = true;
+ break;
+ default:
+ status = "pending";
+ canStart = true;
+ }
+
+ return {
+ supervisorTaskId,
+ status,
+ daemonId: null, // Task summary doesn't have daemon_id, would need full task
+ canStart,
+ canStop,
+ canResume,
+ };
+}
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 33deafa..a3ef773 100644
--- a/makima/frontend/tsconfig.tsbuildinfo
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/autopilotpanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file