From 355f10964c4dbec24a244a00caba5c17ed23fc65 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 12 Feb 2026 02:29:45 +0000 Subject: makima: Add an optional memory system for directives (#59) * feat: makima: Add an optional memory system for directives: Add directive_memories database table and migration * feat: makima: Add an optional memory system for directives: Update directive skill documentation with memory commands * feat: makima: Add an optional memory system for directives: Add repository functions for directive memory CRUD * feat: makima: Add an optional memory system for directives: Add frontend API functions and types for directive memory * feat: makima: Add an optional memory system for directives: Add Rust models for directive memory * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: makima: Add an optional memory system for directives: Add memory panel to frontend DirectiveDetail component * Merge remote-tracking branch 'origin/makima/makima--add-an-optional-memory-system-for-directiv-5de1e06d' into combined branch * Merge remote-tracking branch 'origin/makima/makima--add-an-optional-memory-system-for-directiv-c8298c6c' into combined branch * feat: makima: Add an optional memory system for directives: Create useMultiTaskSubscription hook for multi-output WebSocket streaming * feat: makima: Add an optional memory system for directives: Create DirectiveLogStream component for stern-like multi-task output viewing * feat: makima: Add an optional memory system for directives: Integrate log stream panel into directive detail page --- .../src/components/directives/DirectiveDetail.tsx | 318 ++++++++++++++++++++- 1 file changed, 316 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 616c5d2..ab6ddbb 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,6 +1,9 @@ -import { useState } from "react"; -import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api"; +import { useState, useMemo, useEffect, useRef } from "react"; +import type { DirectiveWithSteps, DirectiveStatus, MemoryCategory } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; +import { DirectiveLogStream } from "./DirectiveLogStream"; +import { useDirectiveMemories } from "../../hooks/useDirectiveMemories"; +import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; const STATUS_BADGE: Record = { draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, @@ -10,6 +13,16 @@ const STATUS_BADGE: Record = archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, }; +const CATEGORY_COLORS: Record = { + decision: { text: "text-amber-400", border: "border-amber-800", bg: "bg-amber-900/20", label: "Decision" }, + context: { text: "text-cyan-400", border: "border-cyan-800", bg: "bg-cyan-900/20", label: "Context" }, + preference: { text: "text-violet-400", border: "border-violet-800", bg: "bg-violet-900/20", label: "Preference" }, + learning: { text: "text-emerald-400", border: "border-emerald-800", bg: "bg-emerald-900/20", label: "Learning" }, + other: { text: "text-[#7788aa]", border: "border-[#2a3a5a]", bg: "bg-[#1a2540]", label: "Other" }, +}; + +const ALL_CATEGORIES: MemoryCategory[] = ["decision", "context", "preference", "learning", "other"]; + interface DirectiveDetailProps { directive: DirectiveWithSteps; onStart: () => void; @@ -37,12 +50,70 @@ export function DirectiveDetail({ }: DirectiveDetailProps) { const [editingGoal, setEditingGoal] = useState(false); const [goalText, setGoalText] = useState(directive.goal); + const [visibleTaskIds, setVisibleTaskIds] = useState | null>(null); + 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; + // Memory panel state + const [memoryOpen, setMemoryOpen] = useState(false); + const [addingMemory, setAddingMemory] = useState(false); + const [newCategory, setNewCategory] = useState("context"); + const [newContent, setNewContent] = useState(""); + const [newSource, setNewSource] = useState(""); + const [confirmClear, setConfirmClear] = useState(false); + + const { + grouped, + config: memoryConfig, + loading: memoryLoading, + error: memoryError, + toggleEnabled, + add: addMemory, + remove: removeMemory, + clearAll: clearMemories, + refresh: refreshMemories, + } = useDirectiveMemories(directive.id); + + const memoryEnabled = memoryConfig?.enabled ?? false; + const totalMemories = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0); + + // Build task map from directive steps and orchestrator + const taskMap = useMemo(() => { + const map = new Map(); + 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; + }, [directive.orchestratorTaskId, directive.steps]); + + // 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 handleGoalSave = () => { if (goalText.trim() && goalText !== directive.goal) { onUpdateGoal(goalText.trim()); @@ -50,6 +121,23 @@ export function DirectiveDetail({ setEditingGoal(false); }; + const handleAddMemory = async () => { + if (!newContent.trim()) return; + await addMemory({ + category: newCategory, + content: newContent.trim(), + source: newSource.trim() || undefined, + }); + setNewContent(""); + setNewSource(""); + setAddingMemory(false); + }; + + const handleClearAll = async () => { + await clearMemories(); + setConfirmClear(false); + }; + return (
{/* Header */} @@ -249,6 +337,214 @@ export function DirectiveDetail({ )}
+ {/* Memory Panel */} +
+ {/* Memory header — always visible */} +
+ +
+ {/* Enable/disable toggle */} + +
+
+ + {/* Collapsible content */} + {memoryOpen && ( +
+ {memoryError && ( +
+ {memoryError} +
+ )} + + {memoryLoading ? ( +
Loading...
+ ) : totalMemories === 0 ? ( +
+ No memory entries yet. + {!memoryEnabled && " Enable memory to start capturing entries."} +
+ ) : ( + /* Grouped entries */ +
+ {ALL_CATEGORIES.map((cat) => { + const entries = grouped[cat]; + if (entries.length === 0) return null; + const style = CATEGORY_COLORS[cat]; + return ( +
+
+ + {style.label} + + + ({entries.length}) + +
+
+ {entries.map((entry) => ( +
+
+

+ {entry.content} +

+ {entry.source && ( + + src: {entry.source} + + )} +
+ +
+ ))} +
+
+ ); + })} +
+ )} + + {/* Action bar: Add + Clear */} +
+ + {totalMemories > 0 && ( + <> + {confirmClear ? ( +
+ Clear all? + + +
+ ) : ( + + )} + + )} + +
+ + {/* Add form */} + {addingMemory && ( +
+
+ + +
+