diff options
| author | soryu <soryu@soryu.co> | 2026-02-12 02:29:45 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-12 02:29:45 +0000 |
| commit | 355f10964c4dbec24a244a00caba5c17ed23fc65 (patch) | |
| tree | 6fdc998e6b95948e80a87a962acd58acf79d5b98 /makima/frontend/src/components | |
| parent | 9bd6eacaa9ebe860842b5d5cfbf2b7d2d0293ab1 (diff) | |
| download | soryu-355f10964c4dbec24a244a00caba5c17ed23fc65.tar.gz soryu-355f10964c4dbec24a244a00caba5c17ed23fc65.zip | |
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
Diffstat (limited to 'makima/frontend/src/components')
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 318 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveLogStream.tsx | 367 |
2 files changed, 683 insertions, 2 deletions
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<DirectiveStatus, { color: string; label: string }> = { draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, @@ -10,6 +13,16 @@ const STATUS_BADGE: Record<DirectiveStatus, { color: string; label: string }> = archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, }; +const CATEGORY_COLORS: Record<MemoryCategory, { text: string; border: string; bg: string; label: string }> = { + 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<Set<string> | 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<MemoryCategory>("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<string, string>(); + 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 ( <div className="flex flex-col h-full overflow-y-auto"> {/* Header */} @@ -249,6 +337,214 @@ export function DirectiveDetail({ )} </div> + {/* Memory Panel */} + <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.1)]"> + {/* Memory header — always visible */} + <div className="flex items-center justify-between"> + <button + type="button" + onClick={() => setMemoryOpen((v) => !v)} + className="flex items-center gap-1.5 group" + > + <span className="text-[10px] font-mono text-[#556677] group-hover:text-[#9bc3ff] transition-colors"> + {memoryOpen ? "\u25BC" : "\u25B6"} + </span> + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Memory + </span> + {totalMemories > 0 && ( + <span className="text-[9px] font-mono text-[#556677] ml-1"> + ({totalMemories}) + </span> + )} + </button> + <div className="flex items-center gap-2"> + {/* Enable/disable toggle */} + <button + type="button" + onClick={() => toggleEnabled(!memoryEnabled)} + className={`text-[9px] font-mono border rounded px-1.5 py-0.5 transition-colors ${ + memoryEnabled + ? "text-emerald-400 border-emerald-800 hover:text-emerald-300" + : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]" + }`} + title={memoryEnabled ? "Disable memory" : "Enable memory"} + > + {memoryEnabled ? "ON" : "OFF"} + </button> + </div> + </div> + + {/* Collapsible content */} + {memoryOpen && ( + <div className="mt-2"> + {memoryError && ( + <div className="text-[10px] font-mono text-red-400 mb-2 px-2 py-1 bg-red-900/10 border border-red-800/30 rounded"> + {memoryError} + </div> + )} + + {memoryLoading ? ( + <div className="text-[10px] font-mono text-[#556677] py-2">Loading...</div> + ) : totalMemories === 0 ? ( + <div className="text-[10px] font-mono text-[#556677] py-2"> + No memory entries yet. + {!memoryEnabled && " Enable memory to start capturing entries."} + </div> + ) : ( + /* Grouped entries */ + <div className="flex flex-col gap-2"> + {ALL_CATEGORIES.map((cat) => { + const entries = grouped[cat]; + if (entries.length === 0) return null; + const style = CATEGORY_COLORS[cat]; + return ( + <div key={cat}> + <div className="flex items-center gap-1.5 mb-1"> + <span className={`text-[9px] font-mono ${style.text} uppercase tracking-wider`}> + {style.label} + </span> + <span className="text-[9px] font-mono text-[#556677]"> + ({entries.length}) + </span> + </div> + <div className="flex flex-col gap-1"> + {entries.map((entry) => ( + <div + key={entry.id} + className={`flex items-start gap-2 px-2 py-1.5 rounded border ${style.border} ${style.bg}`} + > + <div className="flex-1 min-w-0"> + <p className="text-[10px] font-mono text-[#c0d0e0] whitespace-pre-wrap break-words"> + {entry.content} + </p> + {entry.source && ( + <span className="text-[9px] font-mono text-[#556677] mt-0.5 block"> + src: {entry.source} + </span> + )} + </div> + <button + type="button" + onClick={() => removeMemory(entry.id)} + className="text-[9px] font-mono text-[#556677] hover:text-red-400 shrink-0 mt-0.5" + title="Delete entry" + > + x + </button> + </div> + ))} + </div> + </div> + ); + })} + </div> + )} + + {/* Action bar: Add + Clear */} + <div className="flex items-center gap-2 mt-2 pt-2 border-t border-[rgba(117,170,252,0.1)]"> + <button + type="button" + onClick={() => setAddingMemory((v) => !v)} + className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.2)] rounded px-2 py-0.5" + > + {addingMemory ? "Cancel" : "+ Add"} + </button> + {totalMemories > 0 && ( + <> + {confirmClear ? ( + <div className="flex items-center gap-1.5 ml-auto"> + <span className="text-[9px] font-mono text-red-400">Clear all?</span> + <button + type="button" + onClick={handleClearAll} + className="text-[9px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-1.5 py-0.5" + > + Yes + </button> + <button + type="button" + onClick={() => setConfirmClear(false)} + className="text-[9px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-1.5 py-0.5" + > + No + </button> + </div> + ) : ( + <button + type="button" + onClick={() => setConfirmClear(true)} + className="text-[10px] font-mono text-[#556677] hover:text-red-400 ml-auto" + > + Clear all + </button> + )} + </> + )} + <button + type="button" + onClick={refreshMemories} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + title="Refresh memories" + > + [refresh] + </button> + </div> + + {/* Add form */} + {addingMemory && ( + <div className="mt-2 p-2 bg-[#0a1628] border border-[rgba(117,170,252,0.15)] rounded flex flex-col gap-2"> + <div className="flex items-center gap-2"> + <label className="text-[9px] font-mono text-[#7788aa] shrink-0">Category</label> + <select + value={newCategory} + onChange={(e) => setNewCategory(e.target.value as MemoryCategory)} + className="bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-1.5 py-0.5 text-[10px] font-mono text-white flex-1" + > + {ALL_CATEGORIES.map((c) => ( + <option key={c} value={c}> + {CATEGORY_COLORS[c].label} + </option> + ))} + </select> + </div> + <textarea + value={newContent} + onChange={(e) => setNewContent(e.target.value)} + placeholder="Memory content..." + className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[10px] font-mono text-white resize-y min-h-[40px] placeholder:text-[#556677]" + rows={2} + /> + <input + type="text" + value={newSource} + onChange={(e) => setNewSource(e.target.value)} + placeholder="Source (optional)" + className="w-full bg-[#1a2540] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[10px] font-mono text-white placeholder:text-[#556677]" + /> + <div className="flex gap-1.5"> + <button + type="button" + onClick={handleAddMemory} + disabled={!newContent.trim()} + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-2 py-0.5 disabled:opacity-40 disabled:cursor-not-allowed" + > + Save + </button> + <button + type="button" + onClick={() => { setAddingMemory(false); setNewContent(""); setNewSource(""); }} + className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5" + > + Cancel + </button> + </div> + </div> + )} + </div> + )} + </div> + {/* DAG */} <div className="px-4 py-3 flex-1"> <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-2"> @@ -261,6 +557,24 @@ export function DirectiveDetail({ onSkip={onSkipStep} /> </div> + + {/* Log Stream */} + {taskMap.size > 0 && ( + <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.1)]"> + <DirectiveLogStream + entries={entries} + taskMap={taskMap} + connected={connected} + visibleTaskIds={visibleTaskIds} + searchQuery={searchQuery} + isCollapsed={isLogCollapsed} + onToggleCollapse={() => setIsLogCollapsed((prev) => !prev)} + onSetVisibleTaskIds={setVisibleTaskIds} + onSetSearchQuery={setSearchQuery} + onClear={clearEntries} + /> + </div> + )} </div> ); } 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<string, string>; + /** Whether the WebSocket is connected */ + connected: boolean; + /** Filter: set of visible task IDs (null = show all) */ + visibleTaskIds: Set<string> | 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<string> | 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<HTMLDivElement>(null); + const [autoScroll, setAutoScroll] = useState(true); + const [showFilters, setShowFilters] = useState(false); + + // Build task color map + const taskColorMap = new Map<string, string>(); + 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 ( + <button + type="button" + onClick={onToggleCollapse} + className="flex items-center gap-2 w-full text-left" + > + <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide"> + Log Stream + </span> + <span className="text-[10px] font-mono text-[#556677]"> + [{activeTaskCount} task{activeTaskCount !== 1 ? "s" : ""}] + </span> + {connected && entries.length > 0 && ( + <span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-400/10 border border-green-400/20 rounded"> + <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" /> + <span className="text-[9px] font-mono text-green-400">{entries.length}</span> + </span> + )} + <span className="text-[10px] font-mono text-[#556677] ml-auto">[expand]</span> + </button> + ); + } + + return ( + <div className="flex flex-col" style={{ maxHeight: "400px" }}> + {/* Header */} + <div className="flex items-center justify-between mb-2"> + <div className="flex items-center gap-2"> + <button + type="button" + onClick={onToggleCollapse} + className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide hover:text-white" + > + Log Stream + </button> + {connected && ( + <span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-400/10 border border-green-400/20 rounded"> + <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" /> + <span className="text-[9px] font-mono text-green-400">Live</span> + </span> + )} + <span className="text-[10px] font-mono text-[#556677]"> + {filteredEntries.length} entries + </span> + </div> + <div className="flex items-center gap-2"> + <button + type="button" + onClick={() => setShowFilters(!showFilters)} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [filter] + </button> + {entries.length > 0 && ( + <button + type="button" + onClick={onClear} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [clear] + </button> + )} + {!autoScroll && ( + <button + type="button" + onClick={() => { + setAutoScroll(true); + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [scroll to bottom] + </button> + )} + <button + type="button" + onClick={onToggleCollapse} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc]" + > + [collapse] + </button> + </div> + </div> + + {/* Filters */} + {showFilters && ( + <div className="flex flex-wrap items-center gap-2 mb-2 pb-2 border-b border-[rgba(117,170,252,0.1)]"> + {/* Search */} + <input + type="text" + value={searchQuery} + onChange={(e) => 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 */} + <button + type="button" + onClick={() => onSetVisibleTaskIds(null)} + className={`text-[9px] font-mono px-1.5 py-0.5 rounded border ${ + visibleTaskIds === null + ? "text-white border-[#75aafc] bg-[rgba(117,170,252,0.15)]" + : "text-[#556677] border-[#2a3a5a] hover:text-white" + }`} + > + All + </button> + {Array.from(taskMap.entries()).map(([taskId, label]) => { + const isVisible = visibleTaskIds === null || visibleTaskIds.has(taskId); + const color = taskColorMap.get(taskId) || "#75aafc"; + return ( + <button + key={taskId} + type="button" + onClick={() => { + if (visibleTaskIds === null) { + // Switch from "all" to just this task + onSetVisibleTaskIds(new Set([taskId])); + } else if (visibleTaskIds.has(taskId)) { + const next = new Set(visibleTaskIds); + next.delete(taskId); + if (next.size === 0) { + onSetVisibleTaskIds(null); // back to all + } else { + onSetVisibleTaskIds(next); + } + } else { + const next = new Set(visibleTaskIds); + next.add(taskId); + if (next.size === taskMap.size) { + onSetVisibleTaskIds(null); // all selected = show all + } else { + onSetVisibleTaskIds(next); + } + } + }} + className={`text-[9px] font-mono px-1.5 py-0.5 rounded border transition-colors ${ + isVisible + ? "border-current bg-[rgba(117,170,252,0.1)]" + : "border-[#2a3a5a] opacity-50 hover:opacity-75" + }`} + style={{ color: isVisible ? color : "#556677" }} + > + {label} + </button> + ); + })} + </div> + )} + + {/* Log output */} + <div + ref={containerRef} + onScroll={handleScroll} + className="flex-1 overflow-auto bg-[#0a0f18] rounded p-2 font-mono text-xs min-h-0" + style={{ minHeight: "120px" }} + > + {filteredEntries.length === 0 ? ( + <div className="text-[#555] italic text-[10px]"> + {entries.length === 0 + ? connected + ? "Waiting for output..." + : "No tasks subscribed" + : "No entries match filter"} + </div> + ) : ( + <div className="space-y-1"> + {filteredEntries.map((entry, idx) => ( + <LogEntry + key={idx} + entry={entry} + color={taskColorMap.get(entry.taskId) || "#75aafc"} + /> + ))} + </div> + )} + </div> + </div> + ); +} + +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 ( + <div className="flex gap-2 items-start py-0.5"> + {/* Task label */} + <span + className="text-[9px] shrink-0 font-semibold uppercase tracking-wide w-[80px] truncate text-right" + style={{ color }} + title={entry.taskLabel} + > + {entry.taskLabel} + </span> + <span className="text-[#2a3a5a] shrink-0">|</span> + + {/* Content */} + <div className="flex-1 min-w-0"> + {entry.messageType === "assistant" && ( + <SimpleMarkdown content={entry.content} className="text-[#9bc3ff] text-[10px]" /> + )} + {entry.messageType === "tool_use" && ( + <div className="flex items-center gap-1"> + <span className="text-yellow-500 text-[10px]">*</span> + <span className="text-[#75aafc] text-[10px]">{entry.toolName || "unknown"}</span> + {entry.toolInput && Object.keys(entry.toolInput).length > 0 && ( + <button + onClick={() => setExpanded(!expanded)} + className="text-[#555] hover:text-[#9bc3ff] text-[9px]" + > + {expanded ? "[-]" : "[+]"} + </button> + )} + {expanded && entry.toolInput && ( + <pre className="text-[9px] text-[#555] bg-[#0a1525] p-1 overflow-x-auto mt-0.5 block w-full"> + {JSON.stringify(entry.toolInput, null, 2)} + </pre> + )} + </div> + )} + {entry.messageType === "tool_result" && ( + <div className="text-[10px]"> + <span className={entry.isError ? "text-red-400" : "text-green-500"}> + {entry.isError ? "x" : "+"} + </span>{" "} + <span className="text-[#555]"> + {entry.content.split("\n")[0]} + {entry.content.includes("\n") && "..."} + </span> + </div> + )} + {entry.messageType === "result" && ( + <div className="text-[10px]"> + <span className="text-green-500 font-semibold">Done</span> + {entry.costUsd !== undefined && ( + <span className="text-[#555] ml-2">${entry.costUsd.toFixed(4)}</span> + )} + {entry.durationMs !== undefined && ( + <span className="text-[#555] ml-2">{(entry.durationMs / 1000).toFixed(1)}s</span> + )} + </div> + )} + {entry.messageType === "error" && ( + <span className="text-red-400 text-[10px]">{entry.content}</span> + )} + {entry.messageType === "system" && ( + <span className="text-[#555] text-[9px] uppercase">{entry.content}</span> + )} + {!["assistant", "tool_use", "tool_result", "result", "error", "system"].includes( + entry.messageType + ) && ( + <span className="text-[#555] text-[10px]">{entry.content}</span> + )} + </div> + </div> + ); +} |
