summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-21 19:33:44 +0000
committerGitHub <noreply@github.com>2026-02-21 19:33:44 +0000
commitd670dcb72984cfa483063d161bb468704038895c (patch)
tree885ea969d2c5ea5c026d1caf25cd0a15f6753ca1 /makima/frontend/src/components
parent61442ea1cb92ce8c28fe0622aa19d4e2947a8fd0 (diff)
downloadsoryu-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')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDAG.tsx114
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx91
-rw-r--r--makima/frontend/src/components/directives/OrchestratorStepNode.tsx161
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>
+ );
+}