diff options
| author | soryu <soryu@soryu.co> | 2026-02-21 19:33:44 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-21 19:33:44 +0000 |
| commit | d670dcb72984cfa483063d161bb468704038895c (patch) | |
| tree | 885ea969d2c5ea5c026d1caf25cd0a15f6753ca1 /makima/frontend/src/components/directives | |
| parent | 61442ea1cb92ce8c28fe0622aa19d4e2947a8fd0 (diff) | |
| download | soryu-d670dcb72984cfa483063d161bb468704038895c.tar.gz soryu-d670dcb72984cfa483063d161bb468704038895c.zip | |
feat: add directive ask command, log backfill & specialized DAG steps (#75)
* feat: soryu-co/soryu - makima: Add makima directive ask CLI command
* feat: soryu-co/soryu - makima: Update directive skill docs and planning prompt to support asking questions
* feat: soryu-co/soryu - makima: Add log stream backfill for directive tasks
* feat: soryu-co/soryu - makima: Update planning prompts to inform tasks they can ask questions
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Add ask command to directive SKILL.md documentation
* feat: soryu-co/soryu - makima: Add log stream backfill for directive task output history
* feat: soryu-co/soryu - makima: Update planning prompt to tell planning tasks they can ask questions
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Show Planning, PR, and Cleanup tasks as specialized steps in DAG
Diffstat (limited to 'makima/frontend/src/components/directives')
3 files changed, 325 insertions, 41 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx index 27a80ac..8c7def9 100644 --- a/makima/frontend/src/components/directives/DirectiveDAG.tsx +++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx @@ -1,9 +1,31 @@ import { useMemo } from "react"; import type { DirectiveStep } from "../../lib/api"; import { StepNode } from "./StepNode"; +import { + OrchestratorStepNode, + type OrchestratorStepType, + type OrchestratorStepStatus, +} from "./OrchestratorStepNode"; + +export interface VirtualStep { + type: OrchestratorStepType; + taskId: string; + status: OrchestratorStepStatus; + label: string; + hasQuestions?: boolean; +} + +export interface SpecializedStep { + id: string; + name: string; + type: "orchestrator" | "completion"; + taskId: string; + status: "running" | "completed"; +} interface DirectiveDAGProps { steps: DirectiveStep[]; + specializedSteps?: SpecializedStep[]; onComplete?: (stepId: string) => void; onFail?: (stepId: string) => void; onSkip?: (stepId: string) => void; @@ -13,6 +35,13 @@ interface Layer { steps: DirectiveStep[]; } +/** Types that should appear before the regular DAG steps */ +const BEFORE_TYPES = new Set<OrchestratorStepType>([ + "planning", + "replanning", + "plan-orders", +]); + function topoSort(steps: DirectiveStep[]): Layer[] { if (steps.length === 0) return []; @@ -31,10 +60,13 @@ function topoSort(steps: DirectiveStep[]): Layer[] { })); } -export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAGProps) { +export function DirectiveDAG({ steps, specializedSteps, onComplete, onFail, onSkip }: DirectiveDAGProps) { const layers = useMemo(() => topoSort(steps), [steps]); - if (steps.length === 0) { + const orchestratorSteps = specializedSteps?.filter(s => s.type === "orchestrator") ?? []; + const completionSteps = specializedSteps?.filter(s => s.type === "completion") ?? []; + + if (steps.length === 0 && orchestratorSteps.length === 0 && completionSteps.length === 0) { return ( <div className="text-center py-8 text-[#7788aa] font-mono text-sm"> No steps yet. Add steps to build the DAG. @@ -44,6 +76,19 @@ export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAG return ( <div className="flex flex-col gap-4 items-center py-4"> + {/* Orchestrator steps (Planning/Cleanup/Orders) - rendered above regular steps */} + {orchestratorSteps.map(step => ( + <SpecializedStepNode key={step.id} step={step} /> + ))} + + {/* Connector line if both orchestrator step and regular steps exist */} + {orchestratorSteps.length > 0 && layers.length > 0 && ( + <div className="flex justify-center py-1"> + <div className="w-px h-4 bg-[rgba(117,170,252,0.2)]" /> + </div> + )} + + {/* Regular step layers */} {layers.map((layer, layerIdx) => ( <div key={layerIdx}> {layerIdx > 0 && ( @@ -52,18 +97,69 @@ export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAG </div> )} <div className="flex flex-wrap gap-3 justify-center"> - {layer.steps.map((step) => ( - <StepNode - key={step.id} - step={step} - onComplete={onComplete ? () => onComplete(step.id) : undefined} - onFail={onFail ? () => onFail(step.id) : undefined} - onSkip={onSkip ? () => onSkip(step.id) : undefined} + {afterSteps.map((vs) => ( + <OrchestratorStepNode + key={`${vs.type}-${vs.taskId}`} + type={vs.type} + taskId={vs.taskId} + status={vs.status} + label={vs.label} + hasQuestions={vs.hasQuestions} /> ))} </div> </div> ))} + + {/* Connector line if both regular steps and completion step exist */} + {completionSteps.length > 0 && layers.length > 0 && ( + <div className="flex justify-center py-1"> + <div className="w-px h-4 bg-[rgba(117,170,252,0.2)]" /> + </div> + )} + + {/* Completion steps (PR creation) - rendered below regular steps */} + {completionSteps.map(step => ( + <SpecializedStepNode key={step.id} step={step} /> + ))} + </div> + ); +} + +function SpecializedStepNode({ step }: { step: SpecializedStep }) { + const themeColors = step.type === "orchestrator" + ? { + bg: "bg-[#1a1a30]", + border: "border-[rgba(117,170,252,0.3)]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + label: step.name.startsWith("Cleanup") ? "CLEANUP" + : step.name.startsWith("Pick up") ? "ORDERS" + : "PLANNING", + } + : { + bg: "bg-[#1a1a10]", + border: "border-yellow-900/50", + text: "text-yellow-400", + dot: "bg-yellow-400", + label: "PR", + }; + + return ( + <div className={`flex items-center gap-2 px-3 py-2 ${themeColors.bg} border ${themeColors.border} rounded-lg mx-2`}> + <span className={`inline-block w-2 h-2 rounded-full ${themeColors.dot} animate-pulse`} /> + <span className={`text-[9px] font-mono uppercase tracking-wide ${themeColors.text} opacity-60`}> + {themeColors.label} + </span> + <span className={`text-[11px] font-mono ${themeColors.text} flex-1 truncate`}> + {step.name} + </span> + <a + href={`/mesh/${step.taskId}`} + className="text-[9px] font-mono text-[#556677] hover:text-white underline" + > + View task + </a> </div> ); } diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index 98940d0..171654d 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useEffect, useRef } from "react"; import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; +import type { SpecializedStep } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; @@ -108,6 +109,33 @@ export function DirectiveDetail({ 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, @@ -149,6 +177,36 @@ export function DirectiveDetail({ setEditingGoal(false); }; + // Build virtual steps for orchestrator tasks to display in the DAG + const virtualSteps = useMemo(() => { + const steps: VirtualStep[] = []; + if (directive.orchestratorTaskId) { + const hasOrchestratorQuestions = directiveQuestions.some( + (q) => q.taskId === directive.orchestratorTaskId + ); + steps.push({ + type: "planning", + taskId: directive.orchestratorTaskId, + status: "running", + label: "Planning", + hasQuestions: hasOrchestratorQuestions, + }); + } + if (directive.completionTaskId) { + const hasCompletionQuestions = directiveQuestions.some( + (q) => q.taskId === directive.completionTaskId + ); + steps.push({ + type: directive.prUrl ? "pr-update" : "pr", + taskId: directive.completionTaskId, + status: "running", + label: directive.prUrl ? "Updating PR" : "Creating PR", + hasQuestions: hasCompletionQuestions, + }); + } + return steps; + }, [directive.orchestratorTaskId, directive.completionTaskId, directive.prUrl, directiveQuestions]); + return ( <div className="flex flex-col h-full overflow-y-auto"> {/* Header */} @@ -217,22 +275,6 @@ export function DirectiveDetail({ </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"> - <span className="inline-block w-2 h-2 rounded-full bg-[#75aafc] animate-pulse" /> - <span className="text-[10px] font-mono text-[#75aafc]"> - Planning in progress... - </span> - <a - href={`/mesh/${directive.orchestratorTaskId}`} - className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline ml-auto" - > - View task - </a> - </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"> @@ -251,22 +293,6 @@ export function DirectiveDetail({ </div> )} - {/* Completion task indicator */} - {directive.completionTaskId && ( - <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a10] border border-yellow-900 rounded"> - <span className="inline-block w-2 h-2 rounded-full bg-yellow-400 animate-pulse" /> - <span className="text-[10px] font-mono text-yellow-400"> - {directive.prUrl ? "Updating PR..." : "Creating PR..."} - </span> - <a - href={`/mesh/${directive.completionTaskId}`} - className="text-[9px] font-mono text-[#556677] hover:text-yellow-400 underline ml-auto" - > - View task - </a> - </div> - )} - {/* Pending Questions */} {directiveQuestions.length > 0 && ( <div className="mb-2 space-y-2"> @@ -423,6 +449,7 @@ export function DirectiveDetail({ </span> <DirectiveDAG steps={directive.steps} + specializedSteps={specializedSteps} onComplete={onCompleteStep} onFail={onFailStep} onSkip={onSkipStep} diff --git a/makima/frontend/src/components/directives/OrchestratorStepNode.tsx b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx new file mode 100644 index 0000000..9c8e95e --- /dev/null +++ b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx @@ -0,0 +1,161 @@ +export type OrchestratorStepType = + | "planning" + | "replanning" + | "plan-orders" + | "pr" + | "pr-update" + | "cleanup" + | "verification"; + +export type OrchestratorStepStatus = "running" | "completed" | "failed" | "pending"; + +export interface OrchestratorStepNodeProps { + type: OrchestratorStepType; + taskId: string; + status: OrchestratorStepStatus; + label: string; + hasQuestions?: boolean; +} + +const TYPE_COLORS: Record< + OrchestratorStepType, + { accent: string; bg: string; border: string; text: string; dot: string } +> = { + planning: { + accent: "#75aafc", + bg: "bg-[#0d1a30]", + border: "border-[#75aafc]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + }, + replanning: { + accent: "#75aafc", + bg: "bg-[#0d1a30]", + border: "border-[#75aafc]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + }, + "plan-orders": { + accent: "#c084fc", + bg: "bg-[#1a0d30]", + border: "border-[#c084fc]", + text: "text-[#c084fc]", + dot: "bg-[#c084fc]", + }, + pr: { + accent: "#34d399", + bg: "bg-[#0a1a14]", + border: "border-[#34d399]", + text: "text-[#34d399]", + dot: "bg-[#34d399]", + }, + "pr-update": { + accent: "#34d399", + bg: "bg-[#0a1a14]", + border: "border-[#34d399]", + text: "text-[#34d399]", + dot: "bg-[#34d399]", + }, + cleanup: { + accent: "#7788aa", + bg: "bg-[#141a24]", + border: "border-[#7788aa]", + text: "text-[#7788aa]", + dot: "bg-[#7788aa]", + }, + verification: { + accent: "#7788aa", + bg: "bg-[#141a24]", + border: "border-[#7788aa]", + text: "text-[#7788aa]", + dot: "bg-[#7788aa]", + }, +}; + +const TYPE_LABELS: Record<OrchestratorStepType, string> = { + planning: "PLANNING", + replanning: "REPLANNING", + "plan-orders": "PLAN ORDERS", + pr: "PR", + "pr-update": "PR UPDATE", + cleanup: "CLEANUP", + verification: "VERIFICATION", +}; + +const STATUS_LABELS: Record<OrchestratorStepStatus, string> = { + pending: "PENDING", + running: "RUNNING", + completed: "DONE", + failed: "FAILED", +}; + +export function OrchestratorStepNode({ + type, + taskId, + status, + label, + hasQuestions, +}: OrchestratorStepNodeProps) { + const colors = TYPE_COLORS[type]; + const typeLabel = TYPE_LABELS[type]; + const statusLabel = STATUS_LABELS[status]; + + return ( + <div + className={`${colors.bg} ${colors.border} border border-dashed rounded px-3 py-2 min-w-[160px] max-w-[220px] relative`} + > + {/* Type badge */} + <div className="flex items-center justify-between gap-2 mb-1"> + <div className="flex items-center gap-1.5 min-w-0"> + {/* Status dot */} + {status === "running" && ( + <span + className={`inline-block w-2 h-2 rounded-full ${colors.dot} animate-pulse shrink-0`} + /> + )} + {status === "completed" && ( + <span className="inline-block w-2 h-2 rounded-full bg-emerald-400 shrink-0" /> + )} + {status === "failed" && ( + <span className="inline-block w-2 h-2 rounded-full bg-red-400 shrink-0" /> + )} + {status === "pending" && ( + <span className="inline-block w-2 h-2 rounded-full bg-[#556677] shrink-0" /> + )} + <span className={`text-[9px] font-mono ${colors.text} uppercase shrink-0`}> + {typeLabel} + </span> + </div> + <div className="flex items-center gap-1 shrink-0"> + {hasQuestions && ( + <span className="inline-block w-2 h-2 rounded-full bg-purple-400 animate-pulse" title="Has pending questions" /> + )} + <span + className={`text-[9px] font-mono uppercase ${ + status === "completed" + ? "text-emerald-400" + : status === "failed" + ? "text-red-400" + : colors.text + }`} + > + {statusLabel} + </span> + </div> + </div> + + {/* Label */} + <span className="text-[11px] font-mono text-white truncate block font-medium mb-1"> + {label} + </span> + + {/* Task link */} + <a + href={`/mesh/${taskId}`} + className={`text-[9px] font-mono text-[#556677] hover:${colors.text} underline block`} + > + {status === "running" ? "View running task" : "View task"} + </a> + </div> + ); +} |
