summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx603
-rw-r--r--makima/frontend/src/components/directives/DirectiveList.tsx140
2 files changed, 0 insertions, 743 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
deleted file mode 100644
index 4931afa..0000000
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ /dev/null
@@ -1,603 +0,0 @@
-import { useState, useMemo, useEffect, useRef } from "react";
-import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest, DirectiveOrderGroup, CreateDOGRequest, UpdateDOGRequest } from "../../lib/api";
-import { DirectiveDAG } from "./DirectiveDAG";
-import type { SpecializedStep } from "./DirectiveDAG";
-import { DirectiveLogStream } from "./DirectiveLogStream";
-import { TaskSlideOutPanel } from "./TaskSlideOutPanel";
-import { DOGList } from "./DOGList";
-import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription";
-import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext";
-
-const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
- draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
- active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
- idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
- paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
- inactive: { color: "text-[#9bc3ff] border-[#3f6fb3]", label: "INACTIVE" },
- archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
-};
-
-interface DirectiveDetailProps {
- directive: DirectiveWithSteps;
- onStart: () => void;
- onPause: () => void;
- onAdvance: () => void;
- onCompleteStep: (stepId: string) => void;
- onFailStep: (stepId: string) => void;
- onSkipStep: (stepId: string) => void;
- onUpdateGoal: (goal: string) => void;
- onUpdate: (req: UpdateDirectiveRequest) => void;
- onDelete: () => void;
- onRefresh: () => void;
- onCleanup: () => void;
- onPickUpOrders: () => Promise<{ message: string; orderCount: number; taskId: string | null } | null>;
- onCreatePR: () => Promise<void>;
- dogs: DirectiveOrderGroup[];
- dogsLoading: boolean;
- onCreateDog: (req: CreateDOGRequest) => Promise<DirectiveOrderGroup | null>;
- onUpdateDog: (dogId: string, req: UpdateDOGRequest) => Promise<void>;
- onDeleteDog: (dogId: string) => Promise<void>;
- onPickUpDogOrders: (dogId: string) => Promise<any>;
-}
-
-export function DirectiveDetail({
- directive,
- onStart,
- onPause,
- onAdvance,
- onCompleteStep,
- onFailStep,
- onSkipStep,
- onUpdateGoal,
- onUpdate,
- onDelete,
- onRefresh,
- onCleanup,
- onPickUpOrders,
- onCreatePR,
- dogs,
- dogsLoading,
- onCreateDog,
- onUpdateDog,
- onDeleteDog,
- onPickUpDogOrders,
-}: DirectiveDetailProps) {
- const [activeTab, setActiveTab] = useState<"steps" | "dogs">("steps");
- const [editingGoal, setEditingGoal] = useState(false);
- const [goalText, setGoalText] = useState(directive.goal);
- const [visibleTaskIds, setVisibleTaskIds] = useState<Set<string> | null>(null);
- const [pickingUpOrders, setPickingUpOrders] = useState(false);
- const [pickUpResult, setPickUpResult] = useState<string | null>(null);
- const [creatingPR, setCreatingPR] = useState(false);
- const [slideOutTaskId, setSlideOutTaskId] = useState<string | null>(null);
-
- const handleViewTask = (taskId: string) => {
- setSlideOutTaskId(taskId);
- };
-
- // Sync goalText and reset editing state when directive changes
- useEffect(() => {
- setGoalText(directive.goal);
- setEditingGoal(false);
- }, [directive.id, directive.goal]);
- const [searchQuery, setSearchQuery] = useState("");
- const [isLogCollapsed, setIsLogCollapsed] = useState(true);
- const prevHadRunningRef = useRef(false);
- const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft;
-
- const completedSteps = directive.steps.filter((s) => s.status === "completed").length;
- const totalSteps = directive.steps.length;
- const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
- // Get pending questions for this directive's tasks
- const { pendingQuestions, submitAnswer } = useSupervisorQuestions();
- const directiveTaskIds = useMemo(() => {
- const ids = new Set<string>();
- if (directive.orchestratorTaskId) ids.add(directive.orchestratorTaskId);
- for (const step of directive.steps) {
- if (step.taskId) ids.add(step.taskId);
- }
- return ids;
- }, [directive.orchestratorTaskId, directive.steps]);
-
- const directiveQuestions = useMemo(
- () => pendingQuestions.filter((q) =>
- q.directiveId === directive.id || directiveTaskIds.has(q.taskId)
- ),
- [pendingQuestions, directive.id, directiveTaskIds]
- );
-
- // Build task map from directive steps and orchestrator
- // Derive a stable key from the actual task IDs to avoid recreating the map on every poll
- const taskMapKey = useMemo(() => {
- const parts: string[] = [];
- if (directive.orchestratorTaskId) parts.push(`o:${directive.orchestratorTaskId}`);
- for (const step of directive.steps) {
- if (step.taskId) parts.push(`${step.id}:${step.taskId}`);
- }
- return parts.join(",");
- }, [directive.orchestratorTaskId, directive.steps]);
-
- const taskMap = useMemo(() => {
- const map = new Map<string, string>();
- if (directive.orchestratorTaskId) {
- map.set(directive.orchestratorTaskId, "Orchestrator");
- }
- for (const step of directive.steps) {
- if (step.taskId) {
- map.set(step.taskId, step.name);
- }
- }
- return map;
- }, [taskMapKey]); // eslint-disable-line react-hooks/exhaustive-deps
-
- // Build specialized steps for DAG visualization
- const specializedSteps = useMemo(() => {
- const steps: SpecializedStep[] = [];
-
- if (directive.orchestratorTaskId) {
- steps.push({
- id: `orchestrator-${directive.orchestratorTaskId}`,
- name: taskMap.get(directive.orchestratorTaskId) || "Planning",
- type: "orchestrator",
- taskId: directive.orchestratorTaskId,
- status: "running",
- });
- }
-
- if (directive.completionTaskId) {
- steps.push({
- id: `completion-${directive.completionTaskId}`,
- name: directive.prUrl ? "Updating PR" : "Creating PR",
- type: "completion",
- taskId: directive.completionTaskId,
- status: "running",
- });
- }
-
- return steps;
- }, [directive.orchestratorTaskId, directive.completionTaskId, directive.prUrl, taskMap]);
-
- // Subscribe to all task outputs
- const { connected, entries, clearEntries } = useMultiTaskSubscription({
- taskMap,
- enabled: taskMap.size > 0,
- });
-
- // Auto-expand log panel when tasks start running
- const hasRunningTasks = directive.steps.some((s) => s.status === "running") ||
- !!directive.orchestratorTaskId;
-
- useEffect(() => {
- if (hasRunningTasks && !prevHadRunningRef.current) {
- setIsLogCollapsed(false);
- }
- prevHadRunningRef.current = hasRunningTasks;
- }, [hasRunningTasks]);
-
- const handlePickUpOrders = async () => {
- setPickingUpOrders(true);
- setPickUpResult(null);
- try {
- const result = await onPickUpOrders();
- if (result) {
- setPickUpResult(result.message);
- setTimeout(() => setPickUpResult(null), 5000);
- }
- } catch (e) {
- setPickUpResult(e instanceof Error ? e.message : "Failed to pick up orders");
- setTimeout(() => setPickUpResult(null), 5000);
- } finally {
- setPickingUpOrders(false);
- }
- };
-
- const handleGoalSave = () => {
- if (goalText.trim() && goalText !== directive.goal) {
- onUpdateGoal(goalText.trim());
- }
- setEditingGoal(false);
- };
-
-
- // Find the task name for the slide-out panel
- const slideOutTaskName = slideOutTaskId
- ? (directive.steps.find((s) => s.taskId === slideOutTaskId)?.name ??
- taskMap.get(slideOutTaskId) ??
- undefined)
- : undefined;
-
- return (
- <>
- <div className="flex flex-col h-full overflow-y-auto">
- {/* Header */}
- <div className="px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
- <div className="flex items-center justify-between mb-2">
- <h2 className="text-[14px] font-mono text-white font-medium truncate pr-2">
- {directive.title}
- </h2>
- <div className="flex items-center gap-2 shrink-0">
- <span
- className={`text-[10px] font-mono ${badge.color} border rounded px-2 py-0.5`}
- >
- {badge.label}
- </span>
- <button
- type="button"
- onClick={onRefresh}
- className="text-[10px] font-mono text-[#7788aa] hover:text-white"
- title="Refresh"
- >
- [refresh]
- </button>
- </div>
- </div>
-
- {/* Progress bar */}
- {totalSteps > 0 && (
- <div className="flex items-center gap-2 mb-2">
- <div className="flex-1 h-1.5 bg-[#1a2540] rounded overflow-hidden">
- <div
- className="h-full bg-emerald-600 rounded transition-all"
- style={{ width: `${progress}%` }}
- />
- </div>
- <span className="text-[10px] font-mono text-[#7788aa] shrink-0">
- {completedSteps}/{totalSteps} steps
- </span>
- </div>
- )}
-
- {/* Repo info */}
- {(directive.repositoryUrl || directive.localPath) && (
- <div className="text-[10px] font-mono text-[#556677] mb-2 truncate">
- {directive.repositoryUrl || directive.localPath}
- {directive.baseBranch && ` @ ${directive.baseBranch}`}
- </div>
- )}
-
- {/* Reconcile mode toggle */}
- <div className="flex items-center gap-2 mb-2">
- <div className="flex items-center border border-[#2a3a5a] rounded overflow-hidden">
- {(["auto", "semi-auto", "manual"] as const).map((mode) => {
- const isActive = directive.reconcileMode === mode;
- const modeStyles: Record<string, string> = {
- auto: isActive ? "text-[#9bc3ff] bg-[#1a2540]" : "text-[#445566] hover:text-[#7788aa]",
- "semi-auto": isActive ? "text-amber-400 bg-amber-900/20" : "text-[#445566] hover:text-[#7788aa]",
- manual: isActive ? "text-orange-400 bg-orange-900/20" : "text-[#445566] hover:text-[#7788aa]",
- };
- const labels: Record<string, string> = { auto: "Auto", "semi-auto": "Semi", manual: "Manual" };
- return (
- <button
- key={mode}
- type="button"
- onClick={() => onUpdate({ reconcileMode: mode })}
- className={`text-[10px] font-mono px-2 py-0.5 transition-colors border-r border-[#2a3a5a] last:border-r-0 ${modeStyles[mode]}`}
- >
- {labels[mode]}
- </button>
- );
- })}
- </div>
- <span className="text-[9px] font-mono text-[#445566]">
- {directive.reconcileMode === "auto" && "Questions timeout after 30s"}
- {directive.reconcileMode === "semi-auto" && "Questions pause execution"}
- {directive.reconcileMode === "manual" && "Tasks ask clarifying questions"}
- </span>
- </div>
-
- {/* PR link */}
- {directive.prUrl && (
- <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#0a1a10] border border-emerald-900 rounded">
- <span className="inline-block w-2 h-2 rounded-full bg-emerald-400" />
- <span className="text-[10px] font-mono text-emerald-400">
- PR created
- </span>
- <a
- href={directive.prUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 underline ml-auto truncate max-w-[200px]"
- >
- {directive.prUrl}
- </a>
- </div>
- )}
-
- {/* Pending Questions */}
- {directiveQuestions.length > 0 && (
- <div className="mb-2 space-y-2">
- {directiveQuestions.map((q) => (
- <DirectiveQuestionCard
- key={q.questionId}
- question={q}
- taskName={taskMap.get(q.taskId) || "Task"}
- onAnswer={(response) => submitAnswer(q.questionId, response)}
- />
- ))}
- </div>
- )}
-
- {/* Controls */}
- <div className="flex flex-wrap gap-2">
- {(directive.status === "draft" || directive.status === "paused") && (
- <button
- type="button"
- onClick={onStart}
- className="text-[10px] font-mono text-green-400 hover:text-green-300 border border-green-800 rounded px-2 py-1"
- >
- Start
- </button>
- )}
- {directive.status === "active" && (
- <>
- <button
- type="button"
- onClick={onPause}
- className="text-[10px] font-mono text-orange-400 hover:text-orange-300 border border-orange-800 rounded px-2 py-1"
- >
- Pause
- </button>
- <button
- type="button"
- onClick={onAdvance}
- className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
- >
- Advance
- </button>
- </>
- )}
- {directive.status === "idle" && (
- <div className="flex items-center gap-2">
- <span className="text-[10px] font-mono text-yellow-400">
- All steps done. Update goal to add new work.
- </span>
- <button
- type="button"
- onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
- className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1"
- >
- Update Goal
- </button>
- <button
- type="button"
- onClick={onCleanup}
- className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-1"
- >
- Clean up
- </button>
- </div>
- )}
- {completedSteps > 0 && !directive.completionTaskId && (
- <button
- type="button"
- onClick={async () => {
- setCreatingPR(true);
- try { await onCreatePR(); } catch (e) { console.error("Failed to create PR:", e); } finally { setCreatingPR(false); }
- }}
- disabled={creatingPR}
- className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-1 disabled:opacity-50"
- >
- {creatingPR ? "Creating..." : directive.prUrl ? "Update PR" : "Create PR"}
- </button>
- )}
- <button
- type="button"
- onClick={handlePickUpOrders}
- disabled={pickingUpOrders}
- className="text-[10px] font-mono text-[#c084fc] hover:text-[#d8b4fe] border border-[rgba(192,132,252,0.3)] rounded px-2 py-1 disabled:opacity-50"
- >
- {pickingUpOrders ? "Planning..." : "Plan Orders"}
- </button>
- <button
- type="button"
- onClick={onDelete}
- className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1 ml-auto"
- >
- Delete
- </button>
- </div>
-
- {pickUpResult && (
- <div className="mt-2 px-2 py-1.5 bg-[#1a1030] border border-[rgba(192,132,252,0.2)] rounded">
- <span className="text-[10px] font-mono text-[#c084fc]">{pickUpResult}</span>
- </div>
- )}
- </div>
-
- {/* Goal */}
- <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]">
- <div className="flex items-center justify-between mb-1">
- <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
- Goal
- </span>
- {!editingGoal && (
- <button
- type="button"
- onClick={() => { setGoalText(directive.goal); setEditingGoal(true); }}
- className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]"
- >
- [edit]
- </button>
- )}
- </div>
- {editingGoal ? (
- <div className="flex flex-col gap-1.5">
- <textarea
- value={goalText}
- onChange={(e) => setGoalText(e.target.value)}
- className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[11px] font-mono text-white resize-y min-h-[60px]"
- rows={3}
- />
- <div className="flex gap-1.5">
- <button
- type="button"
- onClick={handleGoalSave}
- className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5"
- >
- Save
- </button>
- <button
- type="button"
- onClick={() => setEditingGoal(false)}
- className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
- >
- Cancel
- </button>
- </div>
- </div>
- ) : (
- <p className="text-[11px] font-mono text-[#c0d0e0] whitespace-pre-wrap">
- {directive.goal}
- </p>
- )}
- </div>
-
- {/* Tab bar */}
- <div className="flex items-center gap-0 border-b border-[rgba(117,170,252,0.1)] px-4">
- <button
- type="button"
- onClick={() => setActiveTab("steps")}
- className={`px-3 py-2 text-[10px] font-mono uppercase tracking-wide transition-colors
- ${activeTab === "steps" ? "text-[#dbe7ff] border-b-2 border-[#75aafc]" : "text-[#556677] hover:text-[#9bc3ff]"}
- `}
- >
- Steps ({totalSteps})
- </button>
- <button
- type="button"
- onClick={() => setActiveTab("dogs")}
- className={`px-3 py-2 text-[10px] font-mono uppercase tracking-wide transition-colors
- ${activeTab === "dogs" ? "text-[#dbe7ff] border-b-2 border-[#75aafc]" : "text-[#556677] hover:text-[#9bc3ff]"}
- `}
- >
- DOGs ({dogs.length})
- </button>
- </div>
-
- {/* Tab content */}
- {activeTab === "steps" ? (
- <div className="px-4 py-3 flex-1">
- <DirectiveDAG
- steps={directive.steps}
- specializedSteps={specializedSteps}
- onComplete={onCompleteStep}
- onFail={onFailStep}
- onSkip={onSkipStep}
- onViewTask={handleViewTask}
- />
- </div>
- ) : (
- <div className="px-4 py-3 flex-1">
- <DOGList
- dogs={dogs}
- loading={dogsLoading}
- onCreateDog={onCreateDog}
- onUpdateDog={onUpdateDog}
- onDeleteDog={onDeleteDog}
- onPickUpOrders={onPickUpDogOrders}
- />
- </div>
- )}
-
- {/* Log Stream */}
- {taskMap.size > 0 && (
- <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.1)]">
- <DirectiveLogStream
- entries={entries}
- taskMap={taskMap}
- connected={connected}
- visibleTaskIds={visibleTaskIds}
- searchQuery={searchQuery}
- isCollapsed={isLogCollapsed}
- onToggleCollapse={() => setIsLogCollapsed((prev) => !prev)}
- onSetVisibleTaskIds={setVisibleTaskIds}
- onSetSearchQuery={setSearchQuery}
- onClear={clearEntries}
- />
- </div>
- )}
- </div>
-
- <TaskSlideOutPanel
- taskId={slideOutTaskId || ""}
- taskName={slideOutTaskName}
- isOpen={!!slideOutTaskId}
- onClose={() => setSlideOutTaskId(null)}
- />
- </>
- );
-}
-
-/** Inline question card for directive pending questions */
-function DirectiveQuestionCard({
- question,
- taskName,
- onAnswer,
-}: {
- question: { questionId: string; question: string; choices: string[]; context: string | null };
- taskName: string;
- onAnswer: (response: string) => void;
-}) {
- const [customResponse, setCustomResponse] = useState("");
- const [submitting, setSubmitting] = useState(false);
-
- const handleSubmit = async (response: string) => {
- setSubmitting(true);
- await onAnswer(response);
- setSubmitting(false);
- };
-
- return (
- <div className="px-2 py-2 bg-[#1a1020] border border-purple-900/50 rounded">
- <div className="flex items-center gap-1.5 mb-1">
- <span className="inline-block w-2 h-2 rounded-full bg-purple-400 animate-pulse" />
- <span className="text-[9px] font-mono text-purple-400 uppercase">
- Question from {taskName}
- </span>
- </div>
- <p className="text-[11px] font-mono text-white mb-1.5">{question.question}</p>
- {question.context && (
- <p className="text-[9px] font-mono text-[#556677] mb-1.5">{question.context}</p>
- )}
- {question.choices.length > 0 ? (
- <div className="flex flex-wrap gap-1">
- {question.choices.map((choice) => (
- <button
- key={choice}
- type="button"
- disabled={submitting}
- onClick={() => handleSubmit(choice)}
- className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 hover:border-purple-600 rounded px-2 py-0.5 disabled:opacity-50"
- >
- {choice}
- </button>
- ))}
- </div>
- ) : (
- <div className="flex gap-1">
- <input
- type="text"
- value={customResponse}
- onChange={(e) => setCustomResponse(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" && customResponse.trim()) {
- handleSubmit(customResponse.trim());
- }
- }}
- placeholder="Type your answer..."
- className="flex-1 bg-[#0a0618] border border-purple-900/50 rounded px-2 py-0.5 text-[10px] font-mono text-white placeholder:text-[#445566]"
- disabled={submitting}
- />
- <button
- type="button"
- disabled={submitting || !customResponse.trim()}
- onClick={() => handleSubmit(customResponse.trim())}
- className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 rounded px-2 py-0.5 disabled:opacity-50"
- >
- Send
- </button>
- </div>
- )}
- </div>
- );
-}
diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx
deleted file mode 100644
index a35c8b1..0000000
--- a/makima/frontend/src/components/directives/DirectiveList.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { useState, useMemo } from "react";
-import type { DirectiveSummary, DirectiveStatus } from "../../lib/api";
-import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext";
-import { DirectiveContextMenu } from "./DirectiveContextMenu";
-
-const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = {
- draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" },
- active: { color: "text-green-400 border-green-800", label: "ACTIVE" },
- idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" },
- paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" },
- inactive: { color: "text-[#9bc3ff] border-[#3f6fb3]", label: "INACTIVE" },
- archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
-};
-
-interface DirectiveListProps {
- directives: DirectiveSummary[];
- selectedId: string | null;
- onSelect: (id: string) => void;
- onCreate: () => void;
- onStart?: (directive: DirectiveSummary) => void;
- onPause?: (directive: DirectiveSummary) => void;
- onArchive?: (directive: DirectiveSummary) => void;
- onDelete?: (directive: DirectiveSummary) => void;
- onGoToPR?: (directive: DirectiveSummary) => void;
-}
-
-export function DirectiveList({ directives, selectedId, onSelect, onCreate, onStart, onPause, onArchive, onDelete, onGoToPR }: DirectiveListProps) {
- const { pendingQuestions } = useSupervisorQuestions();
- const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null);
- const [contextMenuDirective, setContextMenuDirective] = useState<DirectiveSummary | null>(null);
-
- const handleContextMenu = (e: React.MouseEvent, directive: DirectiveSummary) => {
- e.preventDefault();
- setContextMenuPosition({ x: e.clientX, y: e.clientY });
- setContextMenuDirective(directive);
- };
-
- const closeContextMenu = () => {
- setContextMenuPosition(null);
- setContextMenuDirective(null);
- };
-
- const questionsPerDirective = useMemo(() => {
- const counts = new Map<string, number>();
- for (const q of pendingQuestions) {
- if (q.directiveId) {
- counts.set(q.directiveId, (counts.get(q.directiveId) || 0) + 1);
- }
- }
- return counts;
- }, [pendingQuestions]);
-
- return (
- <div className="flex flex-col h-full">
- <div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.2)]">
- <span className="text-[11px] font-mono text-[#9bc3ff] uppercase tracking-wide">
- Directives
- </span>
- <button
- type="button"
- onClick={onCreate}
- className="text-[11px] font-mono text-[#75aafc] hover:text-white bg-transparent border border-[rgba(117,170,252,0.3)] rounded px-2 py-0.5 hover:border-[rgba(117,170,252,0.6)] transition-colors"
- >
- + New
- </button>
- </div>
- <div className="flex-1 overflow-y-auto">
- {directives.length === 0 ? (
- <div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
- No directives yet
- </div>
- ) : (
- directives.map((d) => {
- const badge = STATUS_BADGE[d.status] || STATUS_BADGE.draft;
- const progress = d.totalSteps > 0
- ? Math.round((d.completedSteps / d.totalSteps) * 100)
- : 0;
-
- return (
- <button
- key={d.id}
- type="button"
- onClick={() => onSelect(d.id)}
- onContextMenu={(e) => handleContextMenu(e, d)}
- className={`w-full text-left px-3 py-2.5 border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] transition-colors ${
- selectedId === d.id ? "bg-[rgba(117,170,252,0.1)]" : ""
- }`}
- >
- <div className="flex items-center justify-between mb-1">
- <span className="text-[12px] font-mono text-white truncate pr-2">
- {d.title}
- </span>
- {questionsPerDirective.has(d.id) && (
- <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse shrink-0" title={`${questionsPerDirective.get(d.id)} pending question(s)`} />
- )}
- <span
- className={`text-[9px] font-mono ${badge.color} border rounded px-1.5 py-0.5 shrink-0`}
- >
- {badge.label}
- </span>
- </div>
- <p className="text-[10px] text-[#7788aa] font-mono truncate mb-1.5">
- {d.goal}
- </p>
- {d.totalSteps > 0 && (
- <div className="flex items-center gap-2">
- <div className="flex-1 h-1 bg-[#1a2540] rounded overflow-hidden">
- <div
- className="h-full bg-emerald-600 rounded transition-all"
- style={{ width: `${progress}%` }}
- />
- </div>
- <span className="text-[9px] font-mono text-[#556677] shrink-0">
- {d.completedSteps}/{d.totalSteps}
- </span>
- </div>
- )}
- </button>
- );
- })
- )}
- </div>
-
- {/* Context Menu */}
- {contextMenuPosition && contextMenuDirective && (
- <DirectiveContextMenu
- x={contextMenuPosition.x}
- y={contextMenuPosition.y}
- directive={contextMenuDirective}
- onClose={closeContextMenu}
- onStart={() => onStart?.(contextMenuDirective)}
- onPause={() => onPause?.(contextMenuDirective)}
- onArchive={() => onArchive?.(contextMenuDirective)}
- onDelete={() => onDelete?.(contextMenuDirective)}
- onGoToPR={() => onGoToPR?.(contextMenuDirective)}
- />
- )}
- </div>
- );
-}