summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/directives
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-14 21:29:26 +0000
committerGitHub <noreply@github.com>2026-02-14 21:29:26 +0000
commit9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch)
treeef8bed9718c39041191b58a284ee31f5d8d32521 /makima/frontend/src/components/directives
parentc1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff)
downloadsoryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.tar.gz
soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.zip
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
Diffstat (limited to 'makima/frontend/src/components/directives')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx139
1 files changed, 137 insertions, 2 deletions
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>
+ );
+}