diff options
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/contracts/RepositoryPanel.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/CheckpointCard.tsx | 284 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/CheckpointList.tsx | 51 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/ConversationMessage.tsx | 147 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/ConversationView.tsx | 114 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/HistoryFilters.tsx | 84 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/ResumeControls.tsx | 306 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/TimelineEventCard.tsx | 139 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/TimelineList.tsx | 80 | ||||
| -rw-r--r-- | makima/frontend/src/components/history/index.ts | 8 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 399 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 17 | ||||
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/routes/history.tsx | 325 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
16 files changed, 1958 insertions, 3 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 48abe09..7e12c75 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -14,6 +14,7 @@ const NAV_LINKS: NavLink[] = [ { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, + { label: "History", href: "/history", requiresAuth: true }, ]; export function NavStrip() { diff --git a/makima/frontend/src/components/contracts/RepositoryPanel.tsx b/makima/frontend/src/components/contracts/RepositoryPanel.tsx index e314140..15741a8 100644 --- a/makima/frontend/src/components/contracts/RepositoryPanel.tsx +++ b/makima/frontend/src/components/contracts/RepositoryPanel.tsx @@ -226,7 +226,7 @@ export function RepositoryPanel({ </span> </div> <div className="text-[10px] text-[#556677] truncate"> - {suggestion.repositoryUrl || suggestion.localPath} + {addMode === "local" ? suggestion.localPath : suggestion.repositoryUrl} </div> </button> ))} diff --git a/makima/frontend/src/components/history/CheckpointCard.tsx b/makima/frontend/src/components/history/CheckpointCard.tsx new file mode 100644 index 0000000..fee5bdc --- /dev/null +++ b/makima/frontend/src/components/history/CheckpointCard.tsx @@ -0,0 +1,284 @@ +import { useState } from "react"; +import type { TaskCheckpoint } from "../../lib/api"; +import { forkTask, resumeFromCheckpoint } from "../../lib/api"; + +interface CheckpointCardProps { + checkpoint: TaskCheckpoint; + taskId: string; + onActionComplete: () => void; +} + +export function CheckpointCard({ checkpoint, taskId, onActionComplete }: CheckpointCardProps) { + const [showActions, setShowActions] = useState(false); + const [showForkDialog, setShowForkDialog] = useState(false); + const [showResumeDialog, setShowResumeDialog] = useState(false); + const [forkName, setForkName] = useState(`Fork from checkpoint ${checkpoint.checkpointNumber}`); + const [forkPlan, setForkPlan] = useState(""); + const [resumePlan, setResumePlan] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const handleFork = async () => { + if (!forkName.trim() || !forkPlan.trim()) { + setError("Name and plan are required"); + return; + } + + setIsLoading(true); + setError(null); + try { + await forkTask(taskId, { + forkFromType: "checkpoint", + forkFromValue: String(checkpoint.checkpointNumber), + newTaskName: forkName, + newTaskPlan: forkPlan, + includeConversation: true, + }); + setShowForkDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fork task"); + } finally { + setIsLoading(false); + } + }; + + const handleResume = async () => { + if (!resumePlan.trim()) { + setError("Plan is required"); + return; + } + + setIsLoading(true); + setError(null); + try { + await resumeFromCheckpoint(taskId, checkpoint.id, { + plan: resumePlan, + includeConversation: true, + }); + setShowResumeDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to resume from checkpoint"); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + <div className="p-3 hover:bg-[rgba(117,170,252,0.05)] transition-colors"> + <div className="flex items-start justify-between gap-4"> + {/* Checkpoint info */} + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2"> + <span className="font-mono text-sm text-purple-400">#{checkpoint.checkpointNumber}</span> + <span className="font-mono text-[10px] text-[#7788aa]"> + {checkpoint.commitSha.slice(0, 7)} + </span> + <span className="font-mono text-[10px] text-[#556677]"> + on {checkpoint.branchName} + </span> + </div> + + {checkpoint.message && ( + <div className="font-mono text-xs text-[#9bc3ff] mt-1">{checkpoint.message}</div> + )} + + {/* Files changed */} + {checkpoint.filesChanged.length > 0 && ( + <div className="mt-2 flex flex-wrap gap-2"> + {checkpoint.filesChanged.slice(0, 5).map((file, i) => ( + <span + key={i} + className={`font-mono text-[9px] px-1.5 py-0.5 border ${ + file.action === "A" + ? "text-green-400 border-green-400/30" + : file.action === "D" + ? "text-red-400 border-red-400/30" + : "text-yellow-400 border-yellow-400/30" + }`} + > + {file.action} {file.path.split("/").pop()} + </span> + ))} + {checkpoint.filesChanged.length > 5 && ( + <span className="font-mono text-[9px] text-[#556677]"> + +{checkpoint.filesChanged.length - 5} more + </span> + )} + </div> + )} + + {/* Stats */} + <div className="mt-2 flex items-center gap-4 font-mono text-[9px] text-[#556677]"> + <span className="text-green-400">+{checkpoint.linesAdded}</span> + <span className="text-red-400">-{checkpoint.linesRemoved}</span> + <span>{new Date(checkpoint.createdAt).toLocaleString()}</span> + </div> + </div> + + {/* Actions button */} + <button + onClick={() => setShowActions(!showActions)} + className="shrink-0 p-1 text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" + /> + </svg> + </button> + </div> + + {/* Actions dropdown */} + {showActions && ( + <div className="mt-3 flex gap-2"> + <button + onClick={() => { + setShowForkDialog(true); + setShowActions(false); + }} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-purple-400 border border-purple-400/30 hover:bg-purple-400/10 transition-colors" + > + Fork from here + </button> + <button + onClick={() => { + setShowResumeDialog(true); + setShowActions(false); + }} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-cyan-400 border border-cyan-400/30 hover:bg-cyan-400/10 transition-colors" + > + Resume from here + </button> + </div> + )} + </div> + + {/* Fork dialog */} + {showForkDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Fork from Checkpoint #{checkpoint.checkpointNumber} + </h2> + <button + onClick={() => setShowForkDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + New Task Name + </label> + <input + type="text" + value={forkName} + onChange={(e) => setForkName(e.target.value)} + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none" + /> + </div> + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Plan for New Task + </label> + <textarea + value={forkPlan} + onChange={(e) => setForkPlan(e.target.value)} + rows={4} + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none resize-none" + placeholder="Describe what this forked task should accomplish..." + /> + </div> + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowForkDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleFork} + disabled={isLoading} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-purple-600 border border-purple-500 hover:bg-purple-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Creating..." : "Create Fork"} + </button> + </div> + </div> + </div> + </div> + )} + + {/* Resume dialog */} + {showResumeDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Resume from Checkpoint #{checkpoint.checkpointNumber} + </h2> + <button + onClick={() => setShowResumeDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Plan for Resumed Task + </label> + <textarea + value={resumePlan} + onChange={(e) => setResumePlan(e.target.value)} + rows={4} + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none resize-none" + placeholder="Describe what the resumed task should do..." + /> + </div> + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowResumeDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleResume} + disabled={isLoading} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-cyan-600 border border-cyan-500 hover:bg-cyan-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Creating..." : "Resume Task"} + </button> + </div> + </div> + </div> + </div> + )} + </> + ); +} diff --git a/makima/frontend/src/components/history/CheckpointList.tsx b/makima/frontend/src/components/history/CheckpointList.tsx new file mode 100644 index 0000000..a12c155 --- /dev/null +++ b/makima/frontend/src/components/history/CheckpointList.tsx @@ -0,0 +1,51 @@ +import type { TaskCheckpoint } from "../../lib/api"; +import { CheckpointCard } from "./CheckpointCard"; + +interface CheckpointListProps { + checkpoints: TaskCheckpoint[]; + taskId: string; + onActionComplete: () => void; +} + +export function CheckpointList({ checkpoints, taskId, onActionComplete }: CheckpointListProps) { + // Sort checkpoints by number descending (most recent first) + const sortedCheckpoints = [...checkpoints].sort( + (a, b) => b.checkpointNumber - a.checkpointNumber + ); + + return ( + <div className="flex flex-col h-full"> + {/* Header */} + <div className="shrink-0 p-3 border-b border-[rgba(117,170,252,0.1)] bg-[rgba(0,0,0,0.2)]"> + <div className="flex items-center justify-between"> + <div className="font-mono text-xs text-[#9bc3ff]"> + {checkpoints.length} checkpoint{checkpoints.length !== 1 ? "s" : ""} + </div> + <div className="font-mono text-[10px] text-[#556677]"> + Fork or resume from any checkpoint + </div> + </div> + </div> + + {/* Checkpoint list */} + <div className="flex-1 overflow-y-auto"> + {sortedCheckpoints.length === 0 ? ( + <div className="flex items-center justify-center h-32"> + <div className="font-mono text-[#7788aa] text-xs">No checkpoints</div> + </div> + ) : ( + <div className="divide-y divide-[rgba(117,170,252,0.1)]"> + {sortedCheckpoints.map((checkpoint) => ( + <CheckpointCard + key={checkpoint.id} + checkpoint={checkpoint} + taskId={taskId} + onActionComplete={onActionComplete} + /> + ))} + </div> + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/history/ConversationMessage.tsx b/makima/frontend/src/components/history/ConversationMessage.tsx new file mode 100644 index 0000000..43c0ed0 --- /dev/null +++ b/makima/frontend/src/components/history/ConversationMessage.tsx @@ -0,0 +1,147 @@ +import { useState } from "react"; +import type { ConversationMessage as ConversationMessageType } from "../../lib/api"; + +interface ConversationMessageProps { + message: ConversationMessageType; +} + +// Get role styling +function getRoleStyle(role: string) { + switch (role.toLowerCase()) { + case "user": + return { label: "User", color: "text-[#9bc3ff]", bg: "bg-[rgba(155,195,255,0.1)]" }; + case "assistant": + return { label: "Assistant", color: "text-emerald-400", bg: "bg-[rgba(52,211,153,0.1)]" }; + case "system": + return { label: "System", color: "text-yellow-400", bg: "bg-[rgba(250,204,21,0.1)]" }; + case "tool": + return { label: "Tool", color: "text-purple-400", bg: "bg-[rgba(192,132,252,0.1)]" }; + default: + return { label: role, color: "text-[#7788aa]", bg: "bg-[rgba(119,136,170,0.1)]" }; + } +} + +// Format JSON for display +function formatJson(data: unknown): string { + try { + return JSON.stringify(data, null, 2); + } catch { + return String(data); + } +} + +export function ConversationMessage({ message }: ConversationMessageProps) { + const [showToolDetails, setShowToolDetails] = useState(false); + const { label, color, bg } = getRoleStyle(message.role); + + const hasToolInfo = message.toolName || message.toolCalls?.length; + + return ( + <div className={`p-3 ${bg} border-l-2 border-transparent hover:border-[rgba(117,170,252,0.3)]`}> + {/* Header */} + <div className="flex items-center justify-between mb-2"> + <div className="flex items-center gap-2"> + <span className={`font-mono text-[10px] uppercase ${color}`}>{label}</span> + {message.toolName && ( + <span className="font-mono text-[9px] text-purple-400 px-1.5 py-0.5 border border-[rgba(192,132,252,0.3)]"> + {message.toolName} + </span> + )} + </div> + <div className="flex items-center gap-3"> + {message.tokenCount && ( + <span className="font-mono text-[9px] text-[#556677]"> + {message.tokenCount.toLocaleString()} tokens + </span> + )} + {message.costUsd !== undefined && message.costUsd > 0 && ( + <span className="font-mono text-[9px] text-[#556677]"> + ${message.costUsd.toFixed(4)} + </span> + )} + <span className="font-mono text-[9px] text-[#556677]"> + {new Date(message.timestamp).toLocaleTimeString()} + </span> + </div> + </div> + + {/* Content */} + <div className="font-mono text-xs text-[#dbe7ff] whitespace-pre-wrap break-words"> + {message.content} + </div> + + {/* Tool calls */} + {hasToolInfo && ( + <div className="mt-2"> + <button + onClick={() => setShowToolDetails(!showToolDetails)} + className="font-mono text-[9px] text-purple-400 hover:text-purple-300 uppercase flex items-center gap-1" + > + <svg + className={`w-3 h-3 transition-transform ${showToolDetails ? "rotate-90" : ""}`} + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + {message.toolCalls?.length + ? `${message.toolCalls.length} tool call${message.toolCalls.length > 1 ? "s" : ""}` + : "Tool details"} + </button> + + {showToolDetails && ( + <div className="mt-2 space-y-2"> + {/* Tool input */} + {message.toolInput && ( + <div className="p-2 bg-[rgba(0,0,0,0.3)] border border-[rgba(192,132,252,0.2)]"> + <div className="font-mono text-[9px] text-purple-400 uppercase mb-1">Input</div> + <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto"> + {formatJson(message.toolInput)} + </pre> + </div> + )} + + {/* Tool result */} + {message.toolResult && ( + <div + className={`p-2 border ${ + message.isError + ? "bg-[rgba(239,68,68,0.1)] border-[rgba(239,68,68,0.3)]" + : "bg-[rgba(0,0,0,0.3)] border-[rgba(192,132,252,0.2)]" + }`} + > + <div + className={`font-mono text-[9px] uppercase mb-1 ${ + message.isError ? "text-red-400" : "text-purple-400" + }`} + > + {message.isError ? "Error" : "Result"} + </div> + <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto max-h-48 overflow-y-auto"> + {message.toolResult} + </pre> + </div> + )} + + {/* Multiple tool calls */} + {message.toolCalls?.map((call, i) => ( + <div + key={call.id || i} + className="p-2 bg-[rgba(0,0,0,0.3)] border border-[rgba(192,132,252,0.2)]" + > + <div className="font-mono text-[9px] text-purple-400 uppercase mb-1"> + {call.name} + </div> + <pre className="font-mono text-[10px] text-[#9bc3ff] overflow-x-auto"> + {formatJson(call.input)} + </pre> + </div> + ))} + </div> + )} + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/components/history/ConversationView.tsx b/makima/frontend/src/components/history/ConversationView.tsx new file mode 100644 index 0000000..e3d1110 --- /dev/null +++ b/makima/frontend/src/components/history/ConversationView.tsx @@ -0,0 +1,114 @@ +import type { + TaskConversationResponse, + SupervisorConversationResponse, +} from "../../lib/api"; +import { ConversationMessage } from "./ConversationMessage"; + +interface ConversationViewProps { + conversation: TaskConversationResponse | SupervisorConversationResponse; +} + +// Type guard for task conversation +function isTaskConversation( + conv: TaskConversationResponse | SupervisorConversationResponse +): conv is TaskConversationResponse { + return "taskId" in conv; +} + +// Type guard for supervisor conversation +function isSupervisorConversation( + conv: TaskConversationResponse | SupervisorConversationResponse +): conv is SupervisorConversationResponse { + return "supervisorTaskId" in conv; +} + +export function ConversationView({ conversation }: ConversationViewProps) { + const messages = conversation.messages; + + return ( + <div className="flex flex-col h-full"> + {/* Header info */} + <div className="shrink-0 p-3 border-b border-[rgba(117,170,252,0.1)] bg-[rgba(0,0,0,0.2)]"> + <div className="flex items-center justify-between"> + <div> + {isTaskConversation(conversation) ? ( + <div className="font-mono text-xs text-[#9bc3ff]"> + {conversation.taskName} + <span + className={`ml-2 text-[9px] uppercase px-1.5 py-0.5 border ${ + conversation.status === "done" + ? "text-emerald-400 border-emerald-400/30" + : conversation.status === "running" + ? "text-green-400 border-green-400/30" + : conversation.status === "failed" + ? "text-red-400 border-red-400/30" + : "text-[#7788aa] border-[rgba(117,170,252,0.25)]" + }`} + > + {conversation.status} + </span> + </div> + ) : isSupervisorConversation(conversation) ? ( + <div className="font-mono text-xs text-[#9bc3ff]"> + Supervisor + <span className="ml-2 text-[9px] uppercase text-cyan-400 px-1.5 py-0.5 border border-cyan-400/30"> + {conversation.phase} + </span> + </div> + ) : null} + </div> + + <div className="flex items-center gap-4 font-mono text-[10px] text-[#556677]"> + <span>{messages.length} messages</span> + {isTaskConversation(conversation) && conversation.totalTokens && ( + <span>{conversation.totalTokens.toLocaleString()} tokens</span> + )} + {isTaskConversation(conversation) && + conversation.totalCost !== null && + conversation.totalCost > 0 && ( + <span>${conversation.totalCost.toFixed(4)}</span> + )} + </div> + </div> + + {/* Spawned tasks (supervisor only) */} + {isSupervisorConversation(conversation) && conversation.spawnedTasks.length > 0 && ( + <div className="mt-2 flex flex-wrap gap-2"> + <span className="font-mono text-[9px] text-[#7788aa] uppercase">Spawned:</span> + {conversation.spawnedTasks.map((task) => ( + <span + key={task.taskId} + className={`font-mono text-[9px] px-1.5 py-0.5 border ${ + task.status === "done" + ? "text-emerald-400 border-emerald-400/30" + : task.status === "running" + ? "text-green-400 border-green-400/30" + : task.status === "failed" + ? "text-red-400 border-red-400/30" + : "text-[#7788aa] border-[rgba(117,170,252,0.25)]" + }`} + > + {task.taskName} + </span> + ))} + </div> + )} + </div> + + {/* Messages */} + <div className="flex-1 overflow-y-auto"> + {messages.length === 0 ? ( + <div className="flex items-center justify-center h-32"> + <div className="font-mono text-[#7788aa] text-xs">No messages</div> + </div> + ) : ( + <div className="divide-y divide-[rgba(117,170,252,0.05)]"> + {messages.map((message, index) => ( + <ConversationMessage key={message.id || index} message={message} /> + ))} + </div> + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/history/HistoryFilters.tsx b/makima/frontend/src/components/history/HistoryFilters.tsx new file mode 100644 index 0000000..a1a4945 --- /dev/null +++ b/makima/frontend/src/components/history/HistoryFilters.tsx @@ -0,0 +1,84 @@ +import type { ContractSummary } from "../../lib/api"; + +interface HistoryFiltersProps { + contracts: ContractSummary[]; + selectedContractId: string | null; + onContractChange: (contractId: string | null) => void; + dateFrom: string; + dateTo: string; + onDateFromChange: (date: string) => void; + onDateToChange: (date: string) => void; + totalCount: number; +} + +export function HistoryFilters({ + contracts, + selectedContractId, + onContractChange, + dateFrom, + dateTo, + onDateFromChange, + onDateToChange, + totalCount, +}: HistoryFiltersProps) { + return ( + <div className="shrink-0 flex items-center gap-4 p-3 border border-[rgba(117,170,252,0.15)] bg-[rgba(0,0,0,0.2)]"> + {/* Contract filter */} + <div className="flex items-center gap-2"> + <label className="font-mono text-[10px] text-[#7788aa] uppercase">Contract</label> + <select + value={selectedContractId || ""} + onChange={(e) => onContractChange(e.target.value || null)} + className="font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-2 py-1 focus:border-[#3f6fb3] focus:outline-none min-w-[150px]" + > + <option value="">All Contracts</option> + {contracts.map((contract) => ( + <option key={contract.id} value={contract.id}> + {contract.name} + </option> + ))} + </select> + </div> + + {/* Date range */} + <div className="flex items-center gap-2"> + <label className="font-mono text-[10px] text-[#7788aa] uppercase">From</label> + <input + type="date" + value={dateFrom} + onChange={(e) => onDateFromChange(e.target.value)} + className="font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-2 py-1 focus:border-[#3f6fb3] focus:outline-none" + /> + </div> + + <div className="flex items-center gap-2"> + <label className="font-mono text-[10px] text-[#7788aa] uppercase">To</label> + <input + type="date" + value={dateTo} + onChange={(e) => onDateToChange(e.target.value)} + className="font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-2 py-1 focus:border-[#3f6fb3] focus:outline-none" + /> + </div> + + {/* Clear filters */} + {(selectedContractId || dateFrom || dateTo) && ( + <button + onClick={() => { + onContractChange(null); + onDateFromChange(""); + onDateToChange(""); + }} + className="font-mono text-[10px] text-[#7788aa] hover:text-[#9bc3ff] uppercase transition-colors" + > + Clear Filters + </button> + )} + + {/* Result count */} + <div className="ml-auto font-mono text-[10px] text-[#556677]"> + {totalCount} event{totalCount !== 1 ? "s" : ""} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/history/ResumeControls.tsx b/makima/frontend/src/components/history/ResumeControls.tsx new file mode 100644 index 0000000..23493f0 --- /dev/null +++ b/makima/frontend/src/components/history/ResumeControls.tsx @@ -0,0 +1,306 @@ +import { useState } from "react"; +import type { TaskCheckpoint } from "../../lib/api"; +import { rewindTask, resumeSupervisor, rewindSupervisorConversation } from "../../lib/api"; + +interface ResumeControlsProps { + taskId: string; + contractId: string | null; + checkpoints: TaskCheckpoint[]; + onActionComplete: () => void; +} + +export function ResumeControls({ + taskId, + contractId, + checkpoints, + onActionComplete, +}: ResumeControlsProps) { + const [showRewindDialog, setShowRewindDialog] = useState(false); + const [showSupervisorDialog, setShowSupervisorDialog] = useState(false); + const [selectedCheckpoint, setSelectedCheckpoint] = useState<string>(""); + const [preserveMode, setPreserveMode] = useState<"discard" | "create_branch">("create_branch"); + const [branchName, setBranchName] = useState(""); + const [resumeMode, setResumeMode] = useState<"continue" | "restart_phase">("continue"); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const handleRewindTask = async () => { + if (!selectedCheckpoint) { + setError("Select a checkpoint"); + return; + } + + setIsLoading(true); + setError(null); + try { + await rewindTask(taskId, { + checkpointId: selectedCheckpoint, + preserveMode, + branchName: preserveMode === "create_branch" ? branchName || undefined : undefined, + }); + setShowRewindDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to rewind task"); + } finally { + setIsLoading(false); + } + }; + + const handleResumeSupervisor = async () => { + if (!contractId) return; + + setIsLoading(true); + setError(null); + try { + await resumeSupervisor(contractId, { + resumeMode, + }); + setShowSupervisorDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to resume supervisor"); + } finally { + setIsLoading(false); + } + }; + + const handleRewindConversation = async () => { + if (!contractId) return; + + setIsLoading(true); + setError(null); + try { + await rewindSupervisorConversation(contractId, { + byMessageCount: 1, // Rewind by 1 message + }); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to rewind conversation"); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + <div className="shrink-0 p-3 border-t border-[rgba(117,170,252,0.15)] bg-[rgba(0,0,0,0.2)] flex items-center gap-2"> + {/* Task controls */} + {checkpoints.length > 0 && ( + <button + onClick={() => setShowRewindDialog(true)} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-yellow-400 border border-yellow-400/30 hover:bg-yellow-400/10 transition-colors" + > + Rewind Code + </button> + )} + + {/* Supervisor controls */} + {contractId && ( + <> + <button + onClick={() => setShowSupervisorDialog(true)} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-cyan-400 border border-cyan-400/30 hover:bg-cyan-400/10 transition-colors" + > + Resume Supervisor + </button> + <button + onClick={handleRewindConversation} + disabled={isLoading} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-orange-400 border border-orange-400/30 hover:bg-orange-400/10 transition-colors disabled:opacity-50" + > + Undo Last Message + </button> + </> + )} + </div> + + {/* Rewind Task Dialog */} + {showRewindDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Rewind Task Code + </h2> + <button + onClick={() => setShowRewindDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Checkpoint + </label> + <select + value={selectedCheckpoint} + onChange={(e) => setSelectedCheckpoint(e.target.value)} + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none" + > + <option value="">Select checkpoint...</option> + {checkpoints.map((cp) => ( + <option key={cp.id} value={cp.id}> + #{cp.checkpointNumber} - {cp.message || cp.commitSha.slice(0, 7)} + </option> + ))} + </select> + </div> + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Preserve Current Code + </label> + <div className="flex gap-4"> + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="radio" + name="preserveMode" + checked={preserveMode === "create_branch"} + onChange={() => setPreserveMode("create_branch")} + className="text-[#3f6fb3]" + /> + <span className="font-mono text-xs text-[#9bc3ff]">Create branch</span> + </label> + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="radio" + name="preserveMode" + checked={preserveMode === "discard"} + onChange={() => setPreserveMode("discard")} + className="text-[#3f6fb3]" + /> + <span className="font-mono text-xs text-[#9bc3ff]">Discard</span> + </label> + </div> + </div> + + {preserveMode === "create_branch" && ( + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Branch Name (optional) + </label> + <input + type="text" + value={branchName} + onChange={(e) => setBranchName(e.target.value)} + placeholder="Auto-generated if empty" + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none" + /> + </div> + )} + + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowRewindDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleRewindTask} + disabled={isLoading || !selectedCheckpoint} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-yellow-600 border border-yellow-500 hover:bg-yellow-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Rewinding..." : "Rewind"} + </button> + </div> + </div> + </div> + </div> + )} + + {/* Resume Supervisor Dialog */} + {showSupervisorDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Resume Supervisor + </h2> + <button + onClick={() => setShowSupervisorDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-2"> + Resume Mode + </label> + <div className="space-y-2"> + <label className="flex items-start gap-2 cursor-pointer p-2 border border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.25)] transition-colors"> + <input + type="radio" + name="resumeMode" + checked={resumeMode === "continue"} + onChange={() => setResumeMode("continue")} + className="mt-0.5 text-[#3f6fb3]" + /> + <div> + <span className="font-mono text-xs text-[#9bc3ff]">Continue</span> + <p className="font-mono text-[10px] text-[#7788aa] mt-0.5"> + Resume with existing conversation context + </p> + </div> + </label> + <label className="flex items-start gap-2 cursor-pointer p-2 border border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.25)] transition-colors"> + <input + type="radio" + name="resumeMode" + checked={resumeMode === "restart_phase"} + onChange={() => setResumeMode("restart_phase")} + className="mt-0.5 text-[#3f6fb3]" + /> + <div> + <span className="font-mono text-xs text-[#9bc3ff]">Restart Phase</span> + <p className="font-mono text-[10px] text-[#7788aa] mt-0.5"> + Clear conversation but keep phase progress + </p> + </div> + </label> + </div> + </div> + + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowSupervisorDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleResumeSupervisor} + disabled={isLoading} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-cyan-600 border border-cyan-500 hover:bg-cyan-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Resuming..." : "Resume"} + </button> + </div> + </div> + </div> + </div> + )} + </> + ); +} diff --git a/makima/frontend/src/components/history/TimelineEventCard.tsx b/makima/frontend/src/components/history/TimelineEventCard.tsx new file mode 100644 index 0000000..f48466f --- /dev/null +++ b/makima/frontend/src/components/history/TimelineEventCard.tsx @@ -0,0 +1,139 @@ +import type { HistoryEvent } from "../../lib/api"; + +interface TimelineEventCardProps { + event: HistoryEvent; + isSelected: boolean; + onClick: () => void; +} + +// Get icon and color based on event type +function getEventStyle(eventType: string, eventSubtype: string | null) { + const type = eventType.toLowerCase(); + const subtype = eventSubtype?.toLowerCase(); + + if (type === "task") { + if (subtype === "created") return { icon: "+", color: "text-[#9bc3ff]" }; + if (subtype === "started") return { icon: "\u25B6", color: "text-green-400" }; + if (subtype === "completed") return { icon: "\u2713", color: "text-emerald-400" }; + if (subtype === "failed") return { icon: "\u2717", color: "text-red-400" }; + if (subtype === "stopped") return { icon: "\u25A0", color: "text-yellow-400" }; + return { icon: "\u2022", color: "text-[#9bc3ff]" }; + } + + if (type === "checkpoint") { + return { icon: "\u2691", color: "text-purple-400" }; + } + + if (type === "phase") { + return { icon: "\u21B3", color: "text-cyan-400" }; + } + + if (type === "chat") { + return { icon: "\u2709", color: "text-[#75aafc]" }; + } + + if (type === "contract") { + return { icon: "\u2606", color: "text-[#9bc3ff]" }; + } + + if (type === "file") { + return { icon: "\u2630", color: "text-[#7788aa]" }; + } + + return { icon: "\u2022", color: "text-[#7788aa]" }; +} + +// Format relative time +function formatRelativeTime(dateStr: string): string { + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); + + if (diffSec < 60) return "just now"; + if (diffMin < 60) return `${diffMin}m ago`; + if (diffHour < 24) return `${diffHour}h ago`; + if (diffDay < 7) return `${diffDay}d ago`; + + return date.toLocaleDateString(); +} + +// Extract a preview from event data +function getEventPreview(event: HistoryEvent): string { + const data = event.eventData as Record<string, unknown>; + + // Task events + if (data.taskName) return String(data.taskName); + if (data.name) return String(data.name); + + // Chat events + if (data.message) { + const msg = String(data.message); + return msg.length > 50 ? msg.slice(0, 50) + "..." : msg; + } + + // Checkpoint events + if (data.checkpointMessage) return String(data.checkpointMessage); + if (data.commitSha) return `Commit ${String(data.commitSha).slice(0, 7)}`; + + // Phase events + if (data.phase) return `Phase: ${data.phase}`; + + // Contract events + if (data.contractName) return String(data.contractName); + + return ""; +} + +export function TimelineEventCard({ event, isSelected, onClick }: TimelineEventCardProps) { + const { icon, color } = getEventStyle(event.eventType, event.eventSubtype); + const preview = getEventPreview(event); + + return ( + <button + onClick={onClick} + className={`w-full text-left p-3 transition-colors ${ + isSelected + ? "bg-[rgba(63,111,179,0.2)] border-l-2 border-[#3f6fb3]" + : "hover:bg-[rgba(117,170,252,0.05)] border-l-2 border-transparent" + }`} + > + <div className="flex items-start gap-3"> + {/* Icon */} + <div className={`font-mono text-sm ${color}`}>{icon}</div> + + {/* Content */} + <div className="flex-1 min-w-0"> + <div className="flex items-center justify-between gap-2"> + <div className="font-mono text-xs text-[#9bc3ff] uppercase truncate"> + {event.eventType} + {event.eventSubtype && ( + <span className="text-[#7788aa]"> / {event.eventSubtype}</span> + )} + </div> + <div className="font-mono text-[10px] text-[#556677] shrink-0"> + {formatRelativeTime(event.createdAt)} + </div> + </div> + + {preview && ( + <div className="font-mono text-[10px] text-[#7788aa] mt-1 truncate"> + {preview} + </div> + )} + + {event.phase && ( + <div className="mt-1"> + <span className="font-mono text-[9px] text-[#75aafc] uppercase px-1.5 py-0.5 border border-[rgba(117,170,252,0.25)]"> + {event.phase} + </span> + </div> + )} + </div> + </div> + </button> + ); +} diff --git a/makima/frontend/src/components/history/TimelineList.tsx b/makima/frontend/src/components/history/TimelineList.tsx new file mode 100644 index 0000000..b0348c0 --- /dev/null +++ b/makima/frontend/src/components/history/TimelineList.tsx @@ -0,0 +1,80 @@ +import type { HistoryEvent } from "../../lib/api"; +import { TimelineEventCard } from "./TimelineEventCard"; + +interface TimelineListProps { + events: HistoryEvent[]; + loading: boolean; + error: string | null; + selectedEvent: HistoryEvent | null; + onSelectEvent: (event: HistoryEvent) => void; + onRefresh: () => void; +} + +export function TimelineList({ + events, + loading, + error, + selectedEvent, + onSelectEvent, + onRefresh, +}: TimelineListProps) { + return ( + <div className="panel flex flex-col h-full"> + {/* Header */} + <div className="shrink-0 p-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between"> + <h2 className="font-mono text-xs text-[#9bc3ff] uppercase tracking-wide"> + Timeline + </h2> + <button + onClick={onRefresh} + disabled={loading} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors disabled:opacity-50" + title="Refresh timeline" + > + <svg + className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" + /> + </svg> + </button> + </div> + + {/* Content */} + <div className="flex-1 overflow-y-auto"> + {loading && events.length === 0 ? ( + <div className="flex items-center justify-center h-32"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + ) : error ? ( + <div className="p-4"> + <div className="font-mono text-red-400 text-xs mb-2">Error loading timeline</div> + <div className="font-mono text-[#7788aa] text-[10px]">{error}</div> + </div> + ) : events.length === 0 ? ( + <div className="flex items-center justify-center h-32"> + <div className="font-mono text-[#7788aa] text-xs">No events found</div> + </div> + ) : ( + <div className="divide-y divide-[rgba(117,170,252,0.1)]"> + {events.map((event) => ( + <TimelineEventCard + key={event.id} + event={event} + isSelected={selectedEvent?.id === event.id} + onClick={() => onSelectEvent(event)} + /> + ))} + </div> + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/history/index.ts b/makima/frontend/src/components/history/index.ts new file mode 100644 index 0000000..5b66ea2 --- /dev/null +++ b/makima/frontend/src/components/history/index.ts @@ -0,0 +1,8 @@ +export { TimelineList } from "./TimelineList"; +export { TimelineEventCard } from "./TimelineEventCard"; +export { HistoryFilters } from "./HistoryFilters"; +export { ConversationView } from "./ConversationView"; +export { ConversationMessage } from "./ConversationMessage"; +export { CheckpointList } from "./CheckpointList"; +export { CheckpointCard } from "./CheckpointCard"; +export { ResumeControls } from "./ResumeControls"; diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 1e62732..9c56f6b 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -2093,3 +2093,402 @@ export async function createAdhocTask( } return res.json(); } + +// ============================================================================= +// History Types +// ============================================================================= + +/** History event from the timeline */ +export interface HistoryEvent { + id: string; + ownerId: string; + contractId: string | null; + taskId: string | null; + eventType: string; + eventSubtype: string | null; + phase: string | null; + eventData: Record<string, unknown>; + createdAt: string; +} + +/** Response for contract history endpoint */ +export interface ContractHistoryResponse { + contractId: string; + entries: HistoryEvent[]; + totalCount: number; + cursor: string | null; +} + +/** Tool call info in conversation messages */ +export interface ToolCallInfo { + id: string; + name: string; + input: Record<string, unknown>; +} + +/** Conversation message with optional tool calls */ +export interface ConversationMessage { + id: string; + role: string; + content: string; + timestamp: string; + toolCalls?: ToolCallInfo[]; + toolName?: string; + toolInput?: Record<string, unknown>; + toolResult?: string; + isError?: boolean; + tokenCount?: number; + costUsd?: number; +} + +/** Reference to a spawned task in supervisor conversation */ +export interface TaskReference { + taskId: string; + taskName: string; + status: string; + createdAt: string; + completedAt: string | null; +} + +/** Response for supervisor conversation endpoint */ +export interface SupervisorConversationResponse { + contractId: string; + supervisorTaskId: string; + phase: string; + lastActivity: string; + pendingTaskIds: string[]; + messages: ConversationMessage[]; + spawnedTasks: TaskReference[]; +} + +/** Response for task conversation endpoint */ +export interface TaskConversationResponse { + taskId: string; + taskName: string; + status: string; + messages: ConversationMessage[]; + totalTokens: number | null; + totalCost: number | null; +} + +/** Query filters for timeline endpoint */ +export interface TimelineQueryFilters { + contractId?: string; + taskId?: string; + includeSubtasks?: boolean; + from?: string; + to?: string; + limit?: number; +} + +/** Query filters for contract history endpoint */ +export interface HistoryQueryFilters { + phase?: string; + eventTypes?: string; + from?: string; + to?: string; + limit?: number; +} + +/** Task checkpoint */ +export interface TaskCheckpoint { + id: string; + taskId: string; + checkpointNumber: number; + commitSha: string; + branchName: string; + message: string; + filesChanged: Array<{ path: string; action: string }>; + linesAdded: number; + linesRemoved: number; + createdAt: string; +} + +// ============================================================================= +// History API Functions +// ============================================================================= + +/** + * Get contract history timeline. + */ +export async function getContractHistory( + contractId: string, + filters?: HistoryQueryFilters +): Promise<ContractHistoryResponse> { + const params = new URLSearchParams(); + if (filters?.phase) params.append("phase", filters.phase); + if (filters?.eventTypes) params.append("event_types", filters.eventTypes); + if (filters?.from) params.append("from", filters.from); + if (filters?.to) params.append("to", filters.to); + if (filters?.limit) params.append("limit", filters.limit.toString()); + + const query = params.toString(); + const url = `${API_BASE}/api/v1/contracts/${contractId}/history${query ? `?${query}` : ""}`; + + const res = await authFetch(url); + if (!res.ok) { + throw new Error(`Failed to get contract history: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get supervisor conversation history. + */ +export async function getSupervisorConversation( + contractId: string +): Promise<SupervisorConversationResponse> { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation` + ); + if (!res.ok) { + throw new Error(`Failed to get supervisor conversation: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get task conversation history. + */ +export async function getTaskConversation( + taskId: string, + options?: { includeToolCalls?: boolean; includeToolResults?: boolean; limit?: number } +): Promise<TaskConversationResponse> { + const params = new URLSearchParams(); + if (options?.includeToolCalls !== undefined) + params.append("include_tool_calls", options.includeToolCalls.toString()); + if (options?.includeToolResults !== undefined) + params.append("include_tool_results", options.includeToolResults.toString()); + if (options?.limit) params.append("limit", options.limit.toString()); + + const query = params.toString(); + const url = `${API_BASE}/api/v1/mesh/tasks/${taskId}/conversation${query ? `?${query}` : ""}`; + + const res = await authFetch(url); + if (!res.ok) { + throw new Error(`Failed to get task conversation: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get unified timeline for current user. + */ +export async function getTimeline( + filters?: TimelineQueryFilters +): Promise<ContractHistoryResponse> { + const params = new URLSearchParams(); + if (filters?.contractId) params.append("contract_id", filters.contractId); + if (filters?.taskId) params.append("task_id", filters.taskId); + if (filters?.includeSubtasks !== undefined) + params.append("include_subtasks", filters.includeSubtasks.toString()); + if (filters?.from) params.append("from", filters.from); + if (filters?.to) params.append("to", filters.to); + if (filters?.limit) params.append("limit", filters.limit.toString()); + + const query = params.toString(); + const url = `${API_BASE}/api/v1/timeline${query ? `?${query}` : ""}`; + + const res = await authFetch(url); + if (!res.ok) { + throw new Error(`Failed to get timeline: ${res.statusText}`); + } + return res.json(); +} + +/** + * Get task checkpoints. + */ +export async function getTaskCheckpoints(taskId: string): Promise<TaskCheckpoint[]> { + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints`); + if (!res.ok) { + throw new Error(`Failed to get task checkpoints: ${res.statusText}`); + } + return res.json(); +} + +// ============================================================================= +// Resume/Rewind/Fork Types +// ============================================================================= + +/** Request to rewind a task to a checkpoint */ +export interface RewindTaskRequest { + checkpointId?: string; + checkpointSha?: string; + preserveMode: "discard" | "create_branch" | "stash"; + branchName?: string; +} + +/** Response from task rewind */ +export interface RewindTaskResponse { + taskId: string; + rewindedTo: { + checkpointNumber: number; + sha: string; + message: string; + }; + preservedAs?: { + stateType: string; + reference: string; + }; +} + +/** Request to fork a task from a checkpoint */ +export interface ForkTaskRequest { + forkFromType: "checkpoint" | "timestamp" | "message_id"; + forkFromValue: string; + newTaskName: string; + newTaskPlan: string; + includeConversation?: boolean; + createBranch?: boolean; + branchName?: string; +} + +/** Response from task fork */ +export interface ForkTaskResponse { + newTaskId: string; + sourceTaskId: string; + forkPoint: { + forkType: string; + checkpoint?: TaskCheckpoint; + timestamp: string; + }; + branchName?: string; + conversationIncluded: boolean; + messageCount?: number; +} + +/** Request to resume supervisor */ +export interface ResumeSupervisorRequest { + resumeMode: "continue" | "restart_phase" | "from_checkpoint"; + checkpointId?: string; + additionalContext?: string; +} + +/** Response from supervisor resume */ +export interface ResumeSupervisorResponse { + supervisorTaskId: string; + daemonId: string | null; + contractId: string; + phase: string; + status: string; + conversationMessageCount: number; +} + +/** Request to rewind supervisor conversation */ +export interface RewindConversationRequest { + toMessageId?: string; + byMessageCount?: number; + rewindCode?: boolean; +} + +/** Response from conversation rewind */ +export interface RewindConversationResponse { + contractId: string; + messagesRemoved: number; + newMessageCount: number; +} + +// ============================================================================= +// Resume/Rewind/Fork API Functions +// ============================================================================= + +/** + * Rewind a task to a checkpoint. + */ +export async function rewindTask( + taskId: string, + request: RewindTaskRequest +): Promise<RewindTaskResponse> { + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/rewind`, { + method: "POST", + body: JSON.stringify(request), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to rewind task: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Fork a task from a checkpoint. + */ +export async function forkTask( + taskId: string, + request: ForkTaskRequest +): Promise<ForkTaskResponse> { + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/fork`, { + method: "POST", + body: JSON.stringify(request), + }); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to fork task: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Resume a supervisor. + */ +export async function resumeSupervisor( + contractId: string, + request: ResumeSupervisorRequest +): Promise<ResumeSupervisorResponse> { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/supervisor/resume`, + { + method: "POST", + body: JSON.stringify(request), + } + ); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to resume supervisor: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Rewind supervisor conversation. + */ +export async function rewindSupervisorConversation( + contractId: string, + request: RewindConversationRequest +): Promise<RewindConversationResponse> { + const res = await authFetch( + `${API_BASE}/api/v1/contracts/${contractId}/supervisor/conversation/rewind`, + { + method: "POST", + body: JSON.stringify(request), + } + ); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to rewind conversation: ${errorText || res.statusText}`); + } + return res.json(); +} + +/** + * Resume task from a checkpoint. + */ +export async function resumeFromCheckpoint( + taskId: string, + checkpointId: string, + request: { taskName?: string; plan: string; includeConversation?: boolean } +): Promise<{ taskId: string }> { + const res = await authFetch( + `${API_BASE}/api/v1/mesh/tasks/${taskId}/checkpoints/${checkpointId}/resume`, + { + method: "POST", + body: JSON.stringify(request), + } + ); + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Failed to resume from checkpoint: ${errorText || res.statusText}`); + } + return res.json(); +} diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index 5d389fc..5fd6a4e 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -13,6 +13,7 @@ import FilesPage from "./routes/files"; import ContractsPage from "./routes/contracts"; import WorkflowPage from "./routes/workflow"; import MeshPage from "./routes/mesh"; +import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; @@ -91,6 +92,22 @@ createRoot(document.getElementById("root")!).render( } /> <Route + path="/history" + element={ + <ProtectedRoute> + <HistoryPage /> + </ProtectedRoute> + } + /> + <Route + path="/history/:id" + element={ + <ProtectedRoute> + <HistoryPage /> + </ProtectedRoute> + } + /> + <Route path="/settings" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index 8ed4ab5..cd385f9 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -612,7 +612,7 @@ function ContractsPageContent() { </span> </div> <div className="text-[10px] text-[#556677] truncate"> - {suggestion.repositoryUrl || suggestion.localPath} + {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl} </div> </button> ))} diff --git a/makima/frontend/src/routes/history.tsx b/makima/frontend/src/routes/history.tsx new file mode 100644 index 0000000..fc88f0e --- /dev/null +++ b/makima/frontend/src/routes/history.tsx @@ -0,0 +1,325 @@ +import { useState, useCallback, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { TimelineList } from "../components/history/TimelineList"; +import { ConversationView } from "../components/history/ConversationView"; +import { CheckpointList } from "../components/history/CheckpointList"; +import { HistoryFilters } from "../components/history/HistoryFilters"; +import { ResumeControls } from "../components/history/ResumeControls"; +import { useAuth } from "../contexts/AuthContext"; +import type { + HistoryEvent, + TaskConversationResponse, + SupervisorConversationResponse, + TaskCheckpoint, + ContractSummary, +} from "../lib/api"; +import { + getTimeline, + getTaskConversation, + getSupervisorConversation, + getTaskCheckpoints, + listContracts, +} from "../lib/api"; + +// Detail view modes +type DetailMode = "conversation" | "checkpoints"; + +export default function HistoryPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + + // Timeline state + const [events, setEvents] = useState<HistoryEvent[]>([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + // Filters + const [contracts, setContracts] = useState<ContractSummary[]>([]); + const [selectedContractId, setSelectedContractId] = useState<string | null>(null); + const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null); + const [dateFrom, setDateFrom] = useState<string>(""); + const [dateTo, setDateTo] = useState<string>(""); + + // Selected event and detail + const [selectedEvent, setSelectedEvent] = useState<HistoryEvent | null>(null); + const [detailMode, setDetailMode] = useState<DetailMode>("conversation"); + const [conversation, setConversation] = useState< + TaskConversationResponse | SupervisorConversationResponse | null + >(null); + const [checkpoints, setCheckpoints] = useState<TaskCheckpoint[]>([]); + const [detailLoading, setDetailLoading] = useState(false); + + // Redirect to login if not authenticated + useEffect(() => { + if (!authLoading && isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); + + // Load contracts for filter dropdown + useEffect(() => { + async function loadContracts() { + try { + const response = await listContracts(); + setContracts(response.contracts); + } catch (e) { + console.error("Failed to load contracts:", e); + } + } + if (isAuthenticated || !isAuthConfigured) { + loadContracts(); + } + }, [isAuthenticated, isAuthConfigured]); + + // Load timeline + const loadTimeline = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await getTimeline({ + contractId: selectedContractId || undefined, + taskId: selectedTaskId || undefined, + from: dateFrom || undefined, + to: dateTo || undefined, + limit: 100, + }); + setEvents(response.entries); + setTotalCount(response.totalCount); + } catch (e) { + console.error("Failed to load timeline:", e); + setError(e instanceof Error ? e.message : "Failed to load timeline"); + } finally { + setLoading(false); + } + }, [selectedContractId, selectedTaskId, dateFrom, dateTo]); + + // Load timeline on mount and filter change + useEffect(() => { + if (isAuthenticated || !isAuthConfigured) { + loadTimeline(); + } + }, [loadTimeline, isAuthenticated, isAuthConfigured]); + + // Load detail when event selected + const handleSelectEvent = useCallback(async (event: HistoryEvent) => { + setSelectedEvent(event); + setDetailLoading(true); + + try { + // Determine if this is a task or supervisor event + if (event.taskId) { + // Load task conversation and checkpoints + const [conv, cps] = await Promise.all([ + getTaskConversation(event.taskId, { + includeToolCalls: true, + includeToolResults: true, + }), + getTaskCheckpoints(event.taskId).catch(() => []), + ]); + setConversation(conv); + setCheckpoints(cps); + } else if (event.contractId) { + // Load supervisor conversation + const conv = await getSupervisorConversation(event.contractId); + setConversation(conv); + setCheckpoints([]); + } + } catch (e) { + console.error("Failed to load event details:", e); + } finally { + setDetailLoading(false); + } + }, []); + + // Handle URL param for direct navigation + useEffect(() => { + if (id && events.length > 0) { + const event = events.find((e) => e.taskId === id || e.contractId === id); + if (event && event !== selectedEvent) { + handleSelectEvent(event); + } + } + }, [id, events, selectedEvent, handleSelectEvent]); + + // Clear selection + const handleClearSelection = useCallback(() => { + setSelectedEvent(null); + setConversation(null); + setCheckpoints([]); + navigate("/history"); + }, [navigate]); + + // Handle filter changes + const handleContractChange = useCallback((contractId: string | null) => { + setSelectedContractId(contractId); + setSelectedTaskId(null); // Reset task filter when contract changes + }, []); + + // Handle actions completed + const handleActionComplete = useCallback(() => { + // Refresh timeline and detail after action + loadTimeline(); + if (selectedEvent?.taskId) { + handleSelectEvent(selectedEvent); + } + }, [loadTimeline, selectedEvent, handleSelectEvent]); + + if (authLoading) { + return ( + <div className="relative z-10 min-h-screen bg-[#0a1628] flex flex-col"> + <Masthead /> + <div className="flex-1 flex items-center justify-center"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + </div> + ); + } + + return ( + <div className="relative z-10 min-h-screen bg-[#0a1628] flex flex-col"> + <Masthead /> + + <main className="flex-1 flex flex-col overflow-hidden p-4 gap-4"> + {/* Filters */} + <HistoryFilters + contracts={contracts} + selectedContractId={selectedContractId} + onContractChange={handleContractChange} + dateFrom={dateFrom} + dateTo={dateTo} + onDateFromChange={setDateFrom} + onDateToChange={setDateTo} + totalCount={totalCount} + /> + + {/* Main content area */} + <div className="flex-1 flex gap-4 min-h-0 overflow-hidden"> + {/* Timeline list */} + <div className="w-1/3 min-w-[300px] max-w-[400px] flex flex-col"> + <TimelineList + events={events} + loading={loading} + error={error} + selectedEvent={selectedEvent} + onSelectEvent={handleSelectEvent} + onRefresh={loadTimeline} + /> + </div> + + {/* Detail panel */} + <div className="flex-1 flex flex-col min-h-0 overflow-hidden panel"> + {selectedEvent ? ( + <> + {/* Detail header */} + <div className="shrink-0 p-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between"> + <div className="flex items-center gap-4"> + <button + onClick={handleClearSelection} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg + className="w-5 h-5" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M15 19l-7-7 7-7" + /> + </svg> + </button> + <div> + <div className="font-mono text-xs text-[#9bc3ff] uppercase"> + {selectedEvent.eventType} + {selectedEvent.eventSubtype && ` / ${selectedEvent.eventSubtype}`} + </div> + <div className="font-mono text-[10px] text-[#7788aa]"> + {new Date(selectedEvent.createdAt).toLocaleString()} + </div> + </div> + </div> + + {/* Mode toggle (only if task has checkpoints) */} + {checkpoints.length > 0 && ( + <div className="flex gap-1"> + <button + onClick={() => setDetailMode("conversation")} + className={`px-3 py-1 font-mono text-[10px] uppercase border transition-colors ${ + detailMode === "conversation" + ? "border-[#3f6fb3] text-[#9bc3ff] bg-[rgba(63,111,179,0.2)]" + : "border-[rgba(117,170,252,0.25)] text-[#7788aa] hover:border-[rgba(117,170,252,0.35)]" + }`} + > + Conversation + </button> + <button + onClick={() => setDetailMode("checkpoints")} + className={`px-3 py-1 font-mono text-[10px] uppercase border transition-colors ${ + detailMode === "checkpoints" + ? "border-[#3f6fb3] text-[#9bc3ff] bg-[rgba(63,111,179,0.2)]" + : "border-[rgba(117,170,252,0.25)] text-[#7788aa] hover:border-[rgba(117,170,252,0.35)]" + }`} + > + Checkpoints ({checkpoints.length}) + </button> + </div> + )} + </div> + + {/* Detail content */} + <div className="flex-1 overflow-auto"> + {detailLoading ? ( + <div className="flex items-center justify-center h-full"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + ) : detailMode === "conversation" && conversation ? ( + <ConversationView conversation={conversation} /> + ) : detailMode === "checkpoints" && checkpoints.length > 0 ? ( + <CheckpointList + checkpoints={checkpoints} + taskId={selectedEvent.taskId!} + onActionComplete={handleActionComplete} + /> + ) : ( + <div className="flex items-center justify-center h-full"> + <div className="font-mono text-[#7788aa] text-xs"> + No {detailMode} data available + </div> + </div> + )} + </div> + + {/* Resume controls */} + {selectedEvent.taskId && ( + <ResumeControls + taskId={selectedEvent.taskId} + contractId={selectedEvent.contractId} + checkpoints={checkpoints} + onActionComplete={handleActionComplete} + /> + )} + </> + ) : ( + <div className="flex items-center justify-center h-full"> + <div className="text-center"> + <div className="font-mono text-[#7788aa] text-sm mb-2"> + Select an event to view details + </div> + <div className="font-mono text-[#556677] text-xs"> + View conversation history, checkpoints, and more + </div> + </div> + </div> + )} + </div> + </div> + </main> + </div> + ); +} diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 7af14b5..33deafa 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file |
