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 --- .../components/directives/DirectiveLogStream.tsx | 367 +++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 makima/frontend/src/components/directives/DirectiveLogStream.tsx (limited to 'makima/frontend/src/components/directives/DirectiveLogStream.tsx') diff --git a/makima/frontend/src/components/directives/DirectiveLogStream.tsx b/makima/frontend/src/components/directives/DirectiveLogStream.tsx new file mode 100644 index 0000000..d457fe3 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveLogStream.tsx @@ -0,0 +1,367 @@ +import { useRef, useEffect, useState, useCallback } from "react"; +import { SimpleMarkdown } from "../SimpleMarkdown"; +import type { MultiTaskOutputEntry } from "../../hooks/useMultiTaskSubscription"; + +interface DirectiveLogStreamProps { + entries: MultiTaskOutputEntry[]; + /** Map of taskId -> label for display */ + taskMap: Map; + /** Whether the WebSocket is connected */ + connected: boolean; + /** Filter: set of visible task IDs (null = show all) */ + visibleTaskIds: Set | null; + /** Current search query */ + searchQuery: string; + /** Whether the panel is collapsed */ + isCollapsed: boolean; + /** Toggle collapse state */ + onToggleCollapse: () => void; + /** Update visible task filter */ + onSetVisibleTaskIds: (ids: Set | null) => void; + /** Update search query */ + onSetSearchQuery: (query: string) => void; + /** Clear all entries */ + onClear: () => void; +} + +// Assign stable colors to tasks +const TASK_COLORS = [ + "#75aafc", // blue + "#4ade80", // green + "#f59e0b", // amber + "#a78bfa", // violet + "#f472b6", // pink + "#22d3ee", // cyan + "#fb923c", // orange + "#34d399", // emerald +]; + +function getTaskColor(index: number): string { + return TASK_COLORS[index % TASK_COLORS.length]; +} + +export function DirectiveLogStream({ + entries, + taskMap, + connected, + visibleTaskIds, + searchQuery, + isCollapsed, + onToggleCollapse, + onSetVisibleTaskIds, + onSetSearchQuery, + onClear, +}: DirectiveLogStreamProps) { + const containerRef = useRef(null); + const [autoScroll, setAutoScroll] = useState(true); + const [showFilters, setShowFilters] = useState(false); + + // Build task color map + const taskColorMap = new Map(); + let colorIdx = 0; + for (const [taskId] of taskMap) { + taskColorMap.set(taskId, getTaskColor(colorIdx++)); + } + + // Filter entries + const filteredEntries = entries.filter((entry) => { + // Filter by visible task IDs + if (visibleTaskIds && !visibleTaskIds.has(entry.taskId)) return false; + // Filter by search query + if (searchQuery) { + const q = searchQuery.toLowerCase(); + const matchesContent = entry.content?.toLowerCase().includes(q); + const matchesLabel = entry.taskLabel?.toLowerCase().includes(q); + const matchesTool = entry.toolName?.toLowerCase().includes(q); + if (!matchesContent && !matchesLabel && !matchesTool) return false; + } + return true; + }); + + // Handle scroll + const handleScroll = useCallback(() => { + if (!containerRef.current) return; + const { scrollTop, scrollHeight, clientHeight } = containerRef.current; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; + setAutoScroll(isAtBottom); + }, []); + + // Auto-scroll when entries change + useEffect(() => { + if (autoScroll && containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [filteredEntries.length, autoScroll]); + + // Count active (running) tasks + const activeTaskCount = Array.from(taskMap.keys()).length; + + if (isCollapsed) { + return ( + + ); + } + + return ( +
+ {/* Header */} +
+
+ + {connected && ( + + + Live + + )} + + {filteredEntries.length} entries + +
+
+ + {entries.length > 0 && ( + + )} + {!autoScroll && ( + + )} + +
+
+ + {/* Filters */} + {showFilters && ( +
+ {/* Search */} + onSetSearchQuery(e.target.value)} + placeholder="Search logs..." + className="bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5 text-[10px] font-mono text-white w-[160px] placeholder-[#556677]" + /> + {/* Task filter buttons */} + + {Array.from(taskMap.entries()).map(([taskId, label]) => { + const isVisible = visibleTaskIds === null || visibleTaskIds.has(taskId); + const color = taskColorMap.get(taskId) || "#75aafc"; + return ( + + ); + })} +
+ )} + + {/* Log output */} +
+ {filteredEntries.length === 0 ? ( +
+ {entries.length === 0 + ? connected + ? "Waiting for output..." + : "No tasks subscribed" + : "No entries match filter"} +
+ ) : ( +
+ {filteredEntries.map((entry, idx) => ( + + ))} +
+ )} +
+
+ ); +} + +function LogEntry({ + entry, + color, +}: { + entry: MultiTaskOutputEntry; + color: string; +}) { + const [expanded, setExpanded] = useState(false); + + // Skip empty content for tool results + if (entry.messageType === "tool_result" && !entry.content) return null; + + return ( +
+ {/* Task label */} + + {entry.taskLabel} + + | + + {/* Content */} +
+ {entry.messageType === "assistant" && ( + + )} + {entry.messageType === "tool_use" && ( +
+ * + {entry.toolName || "unknown"} + {entry.toolInput && Object.keys(entry.toolInput).length > 0 && ( + + )} + {expanded && entry.toolInput && ( +
+                {JSON.stringify(entry.toolInput, null, 2)}
+              
+ )} +
+ )} + {entry.messageType === "tool_result" && ( +
+ + {entry.isError ? "x" : "+"} + {" "} + + {entry.content.split("\n")[0]} + {entry.content.includes("\n") && "..."} + +
+ )} + {entry.messageType === "result" && ( +
+ Done + {entry.costUsd !== undefined && ( + ${entry.costUsd.toFixed(4)} + )} + {entry.durationMs !== undefined && ( + {(entry.durationMs / 1000).toFixed(1)}s + )} +
+ )} + {entry.messageType === "error" && ( + {entry.content} + )} + {entry.messageType === "system" && ( + {entry.content} + )} + {!["assistant", "tool_use", "tool_result", "result", "error", "system"].includes( + entry.messageType + ) && ( + {entry.content} + )} +
+
+ ); +} -- cgit v1.2.3