From 9aadbc7958d39d181c0dd0600e2b7c30bb6c391a Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sat, 14 Feb 2026 21:29:26 +0000
Subject: Makima system improvements: Orders, directive questions, PR creation
 fix, bug fixes (#62)

* feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue

* WIP: heartbeat checkpoint

* WIP: heartbeat checkpoint

* feat: soryu-co/soryu - makima: Create Orders database schema and backend API

* feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box

* WIP: heartbeat checkpoint

* feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page

* WIP: heartbeat checkpoint

* WIP: heartbeat checkpoint

* feat: soryu-co/soryu - makima: Fix directive PR creation system
---
 .../src/components/directives/DirectiveDetail.tsx  | 139 ++++++++++++++++++++-
 1 file changed, 137 insertions(+), 2 deletions(-)

(limited to 'makima/frontend/src/components/directives/DirectiveDetail.tsx')

diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index b73463d..e278939 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -1,8 +1,9 @@
 import { useState, useMemo, useEffect, useRef } from "react";
-import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api";
+import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api";
 import { DirectiveDAG } from "./DirectiveDAG";
 import { DirectiveLogStream } from "./DirectiveLogStream";
 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" },
@@ -21,6 +22,7 @@ interface DirectiveDetailProps {
   onFailStep: (stepId: string) => void;
   onSkipStep: (stepId: string) => void;
   onUpdateGoal: (goal: string) => void;
+  onUpdate: (req: UpdateDirectiveRequest) => void;
   onDelete: () => void;
   onRefresh: () => void;
   onCleanupTasks: () => void;
@@ -35,6 +37,7 @@ export function DirectiveDetail({
   onFailStep,
   onSkipStep,
   onUpdateGoal,
+  onUpdate,
   onDelete,
   onRefresh,
   onCleanupTasks,
@@ -42,6 +45,12 @@ export function DirectiveDetail({
   const [editingGoal, setEditingGoal] = useState(false);
   const [goalText, setGoalText] = useState(directive.goal);
   const [visibleTaskIds, setVisibleTaskIds] = useState<Set<string> | null>(null);
+
+  // 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);
@@ -53,6 +62,24 @@ export function DirectiveDetail({
   const terminalStatuses = new Set(["completed", "failed", "skipped"]);
   const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status));
 
+  // 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(() => {
@@ -149,6 +176,26 @@ export function DirectiveDetail({
           </div>
         )}
 
+        {/* Reconcile mode toggle */}
+        <div className="flex items-center gap-2 mb-2">
+          <button
+            type="button"
+            onClick={() => onUpdate({ reconcileMode: !directive.reconcileMode })}
+            className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${
+              directive.reconcileMode
+                ? "text-amber-400 border-amber-800 bg-amber-900/20"
+                : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]"
+            }`}
+          >
+            {directive.reconcileMode ? "Reconcile: ON" : "Reconcile: OFF"}
+          </button>
+          <span className="text-[9px] font-mono text-[#445566]">
+            {directive.reconcileMode
+              ? "Questions pause execution"
+              : "Questions timeout after 30s"}
+          </span>
+        </div>
+
         {/* Orchestrator planning indicator */}
         {directive.orchestratorTaskId && (
           <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a30] border border-[rgba(117,170,252,0.2)] rounded">
@@ -199,6 +246,20 @@ export function DirectiveDetail({
           </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") && (
@@ -235,7 +296,7 @@ export function DirectiveDetail({
               </span>
               <button
                 type="button"
-                onClick={() => setEditingGoal(true)}
+                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
@@ -342,3 +403,77 @@ export function DirectiveDetail({
     </div>
   );
 }
+
+/** 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>
+  );
+}
-- 
cgit v1.2.3

