summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-16 12:23:49 +0000
committersoryu <soryu@soryu.co>2026-01-16 12:23:49 +0000
commit205ab8a223ddf6591a3e8bfc9108506502977c11 (patch)
treed768063acff233dbeea223d7b6ea69d7e3038300
parent05931d19bc0c161d0177c3f983d0cd903d5e8ae3 (diff)
downloadsoryu-205ab8a223ddf6591a3e8bfc9108506502977c11.tar.gz
soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.zip
Fixup: use default api.makima.jp URL and fix default branch detection
Also add checkpointing/history
-rw-r--r--makima/docs/CLI.md4
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/contracts/RepositoryPanel.tsx2
-rw-r--r--makima/frontend/src/components/history/CheckpointCard.tsx284
-rw-r--r--makima/frontend/src/components/history/CheckpointList.tsx51
-rw-r--r--makima/frontend/src/components/history/ConversationMessage.tsx147
-rw-r--r--makima/frontend/src/components/history/ConversationView.tsx114
-rw-r--r--makima/frontend/src/components/history/HistoryFilters.tsx84
-rw-r--r--makima/frontend/src/components/history/ResumeControls.tsx306
-rw-r--r--makima/frontend/src/components/history/TimelineEventCard.tsx139
-rw-r--r--makima/frontend/src/components/history/TimelineList.tsx80
-rw-r--r--makima/frontend/src/components/history/index.ts8
-rw-r--r--makima/frontend/src/lib/api.ts399
-rw-r--r--makima/frontend/src/main.tsx17
-rw-r--r--makima/frontend/src/routes/contracts.tsx2
-rw-r--r--makima/frontend/src/routes/history.tsx325
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
-rw-r--r--makima/src/bin/makima.rs7
-rw-r--r--makima/src/daemon/api/client.rs8
-rw-r--r--makima/src/daemon/cli/contract.rs2
-rw-r--r--makima/src/daemon/cli/supervisor.rs2
-rw-r--r--makima/src/daemon/task/manager.rs18
-rw-r--r--makima/src/daemon/worktree/manager.rs61
-rw-r--r--makima/src/server/handlers/repository_history.rs19
-rw-r--r--makima/src/server/mod.rs8
25 files changed, 2071 insertions, 19 deletions
diff --git a/makima/docs/CLI.md b/makima/docs/CLI.md
index 5246a30..0d4e499 100644
--- a/makima/docs/CLI.md
+++ b/makima/docs/CLI.md
@@ -140,7 +140,7 @@ All supervisor subcommands accept these common options:
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
-| `--api-url <URL>` | `MAKIMA_API_URL` | `http://localhost:8080` | API URL |
+| `--api-url <URL>` | `MAKIMA_API_URL` | `https://api.makima.jp` | API URL |
| `--api-key <KEY>` | `MAKIMA_API_KEY` | - | API key for authentication |
| `--contract-id <UUID>` | `MAKIMA_CONTRACT_ID` | - | Contract ID |
| `--task-id <UUID>` | `MAKIMA_TASK_ID` | - | Current task ID (optional) |
@@ -314,7 +314,7 @@ All contract subcommands accept these common options:
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
-| `--api-url <URL>` | `MAKIMA_API_URL` | `http://localhost:8080` | API URL |
+| `--api-url <URL>` | `MAKIMA_API_URL` | `https://api.makima.jp` | API URL |
| `--api-key <KEY>` | `MAKIMA_API_KEY` | - | API key for authentication |
| `--contract-id <UUID>` | `MAKIMA_CONTRACT_ID` | - | Contract ID |
| `--task-id <UUID>` | `MAKIMA_TASK_ID` | - | Current task ID (optional) |
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
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 9c9ac77..47e627b 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -168,6 +168,12 @@ async fn run_daemon(
} else {
None
};
+ // Derive HTTP API URL from WebSocket server URL (wss://... -> https://...)
+ let api_url = config
+ .server
+ .url
+ .replace("wss://", "https://")
+ .replace("ws://", "http://");
let task_config = TaskConfig {
max_concurrent_tasks: config.process.max_concurrent_tasks,
worktree_base_dir: config.worktree.base_dir.clone(),
@@ -178,6 +184,7 @@ async fn run_daemon(
enable_permissions: config.process.enable_permissions,
disable_verbose: config.process.disable_verbose,
bubblewrap: bubblewrap_config,
+ api_url,
};
// Create task manager
diff --git a/makima/src/daemon/api/client.rs b/makima/src/daemon/api/client.rs
index b27d606..2318d5a 100644
--- a/makima/src/daemon/api/client.rs
+++ b/makima/src/daemon/api/client.rs
@@ -42,7 +42,9 @@ impl ApiClient {
let url = format!("{}{}", self.base_url, path);
let response = self.client
.get(&url)
+ // Send both headers - server will try tool key first, then API key
.header("X-Makima-Tool-Key", &self.api_key)
+ .header("X-Makima-API-Key", &self.api_key)
.send()
.await?;
@@ -58,7 +60,9 @@ impl ApiClient {
let url = format!("{}{}", self.base_url, path);
let response = self.client
.post(&url)
+ // Send both headers - server will try tool key first, then API key
.header("X-Makima-Tool-Key", &self.api_key)
+ .header("X-Makima-API-Key", &self.api_key)
.header("Content-Type", "application/json")
.json(body)
.send()
@@ -72,7 +76,9 @@ impl ApiClient {
let url = format!("{}{}", self.base_url, path);
let response = self.client
.post(&url)
+ // Send both headers - server will try tool key first, then API key
.header("X-Makima-Tool-Key", &self.api_key)
+ .header("X-Makima-API-Key", &self.api_key)
.send()
.await?;
@@ -88,7 +94,9 @@ impl ApiClient {
let url = format!("{}{}", self.base_url, path);
let response = self.client
.put(&url)
+ // Send both headers - server will try tool key first, then API key
.header("X-Makima-Tool-Key", &self.api_key)
+ .header("X-Makima-API-Key", &self.api_key)
.header("Content-Type", "application/json")
.json(body)
.send()
diff --git a/makima/src/daemon/cli/contract.rs b/makima/src/daemon/cli/contract.rs
index 5fef5ec..a443b85 100644
--- a/makima/src/daemon/cli/contract.rs
+++ b/makima/src/daemon/cli/contract.rs
@@ -7,7 +7,7 @@ use uuid::Uuid;
#[derive(Args, Debug, Clone)]
pub struct ContractArgs {
/// API URL
- #[arg(long, env = "MAKIMA_API_URL", default_value = "http://localhost:8080", global = true)]
+ #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)]
pub api_url: String,
/// API key for authentication
diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs
index ba4fb2b..db30cf1 100644
--- a/makima/src/daemon/cli/supervisor.rs
+++ b/makima/src/daemon/cli/supervisor.rs
@@ -7,7 +7,7 @@ use uuid::Uuid;
#[derive(Args, Debug, Clone)]
pub struct SupervisorArgs {
/// API URL
- #[arg(long, env = "MAKIMA_API_URL", default_value = "http://localhost:8080")]
+ #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp")]
pub api_url: String,
/// API key for authentication
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 5491934..fccebc5 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -978,6 +978,8 @@ pub struct TaskConfig {
pub disable_verbose: bool,
/// Bubblewrap sandbox configuration.
pub bubblewrap: Option<crate::daemon::config::BubblewrapConfig>,
+ /// API URL for spawned tasks (HTTP endpoint for makima CLI).
+ pub api_url: String,
}
impl Default for TaskConfig {
@@ -992,6 +994,7 @@ impl Default for TaskConfig {
enable_permissions: false,
disable_verbose: false,
bubblewrap: None,
+ api_url: "https://api.makima.jp".to_string(),
}
}
}
@@ -1583,6 +1586,7 @@ impl TaskManager {
active_pids: self.active_pids.clone(),
git_user_email: self.git_user_email.clone(),
git_user_name: self.git_user_name.clone(),
+ api_url: self.config.api_url.clone(),
}
}
@@ -2877,6 +2881,7 @@ struct TaskManagerInner {
active_pids: Arc<RwLock<HashMap<Uuid, u32>>>,
git_user_email: Arc<RwLock<Option<String>>>,
git_user_name: Arc<RwLock<Option<String>>>,
+ api_url: String,
}
impl TaskManagerInner {
@@ -3196,8 +3201,7 @@ impl TaskManagerInner {
// Set up environment variables for makima CLI
let mut env = HashMap::new();
- // TODO: Make API URL configurable
- env.insert("MAKIMA_API_URL".to_string(), "http://localhost:8080".to_string());
+ env.insert("MAKIMA_API_URL".to_string(), self.api_url.clone());
env.insert("MAKIMA_API_KEY".to_string(), tool_key.clone());
env.insert("MAKIMA_TASK_ID".to_string(), task_id.to_string());
// Supervisor needs contract ID for its tools
@@ -3207,7 +3211,7 @@ impl TaskManagerInner {
tracing::info!(
task_id = %task_id,
- api_url = "http://localhost:8080",
+ api_url = %self.api_url,
tool_key_preview = &tool_key[..8.min(tool_key.len())],
"Set supervisor environment variables"
);
@@ -3252,14 +3256,13 @@ impl TaskManagerInner {
// Set up environment variables for makima CLI
let mut env = HashMap::new();
- // TODO: Make API URL configurable
- env.insert("MAKIMA_API_URL".to_string(), "http://localhost:8080".to_string());
+ env.insert("MAKIMA_API_URL".to_string(), self.api_url.clone());
env.insert("MAKIMA_API_KEY".to_string(), tool_key.clone());
env.insert("MAKIMA_TASK_ID".to_string(), task_id.to_string());
tracing::info!(
task_id = %task_id,
- api_url = "http://localhost:8080",
+ api_url = %self.api_url,
tool_key_preview = &tool_key[..8.min(tool_key.len())],
"Set orchestrator environment variables"
);
@@ -3313,7 +3316,7 @@ impl TaskManagerInner {
tracing::warn!(task_id = %task_id, "Failed to register contract tool key");
}
- env.insert("MAKIMA_API_URL".to_string(), "http://localhost:8080".to_string());
+ env.insert("MAKIMA_API_URL".to_string(), self.api_url.clone());
env.insert("MAKIMA_API_KEY".to_string(), tool_key);
env.insert("MAKIMA_TASK_ID".to_string(), task_id.to_string());
}
@@ -4126,6 +4129,7 @@ impl Clone for TaskManagerInner {
active_pids: self.active_pids.clone(),
git_user_email: self.git_user_email.clone(),
git_user_name: self.git_user_name.clone(),
+ api_url: self.api_url.clone(),
}
}
}
diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs
index ff0e9e7..d370828 100644
--- a/makima/src/daemon/worktree/manager.rs
+++ b/makima/src/daemon/worktree/manager.rs
@@ -128,8 +128,30 @@ impl WorktreeManager {
/// Detect the default branch of a repository.
/// Tries to find HEAD's target, falling back to common branch names.
+ /// Works for both regular and bare repositories.
pub async fn detect_default_branch(&self, repo_path: &Path) -> Result<String, WorktreeError> {
- // Try to get the branch that HEAD points to
+ tracing::debug!("Detecting default branch for repo: {}", repo_path.display());
+
+ // First, try to read HEAD directly (works for bare repos)
+ // In bare repos, HEAD is a symbolic ref to the default branch
+ let output = Command::new("git")
+ .args(["symbolic-ref", "HEAD", "--short"])
+ .current_dir(repo_path)
+ .output()
+ .await?;
+
+ if output.status.success() {
+ let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !branch.is_empty() {
+ tracing::debug!("Detected default branch from HEAD: {}", branch);
+ return Ok(branch);
+ }
+ } else {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ tracing::debug!("symbolic-ref HEAD failed: {}", stderr.trim());
+ }
+
+ // Try to get the branch that origin/HEAD points to (for regular clones)
let output = Command::new("git")
.args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
.current_dir(repo_path)
@@ -141,11 +163,12 @@ impl WorktreeManager {
// Remove "origin/" prefix if present
let branch = branch.strip_prefix("origin/").unwrap_or(&branch).to_string();
if !branch.is_empty() {
+ tracing::debug!("Detected default branch from origin/HEAD: {}", branch);
return Ok(branch);
}
}
- // Try common branch names
+ // Try common branch names in refs/heads (works for bare and regular repos)
for branch in ["main", "master", "develop", "trunk"] {
let output = Command::new("git")
.args(["rev-parse", "--verify", &format!("refs/heads/{}", branch)])
@@ -154,11 +177,12 @@ impl WorktreeManager {
.await?;
if output.status.success() {
+ tracing::debug!("Detected default branch from refs/heads: {}", branch);
return Ok(branch.to_string());
}
}
- // Fall back to getting the current branch
+ // Fall back to getting the current branch (for regular repos)
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.current_dir(repo_path)
@@ -168,12 +192,41 @@ impl WorktreeManager {
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !branch.is_empty() && branch != "HEAD" {
+ tracing::debug!("Detected default branch from rev-parse: {}", branch);
+ return Ok(branch);
+ }
+ }
+
+ // Final fallback: list all branches and pick the first one
+ let output = Command::new("git")
+ .args(["for-each-ref", "--format=%(refname:short)", "refs/heads/", "--count=1"])
+ .current_dir(repo_path)
+ .output()
+ .await?;
+
+ if output.status.success() {
+ let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !branch.is_empty() {
+ tracing::warn!("Using first available branch as fallback: {}", branch);
return Ok(branch);
}
}
+ // Log what branches exist for debugging
+ let output = Command::new("git")
+ .args(["for-each-ref", "--format=%(refname)", "refs/"])
+ .current_dir(repo_path)
+ .output()
+ .await?;
+
+ let available_refs = String::from_utf8_lossy(&output.stdout);
+ tracing::error!(
+ "Could not detect default branch. Available refs:\n{}",
+ available_refs
+ );
+
Err(WorktreeError::GitCommand(
- "Could not detect default branch".to_string(),
+ format!("Could not detect default branch. Check if the repository at {} has any branches.", repo_path.display()),
))
}
diff --git a/makima/src/server/handlers/repository_history.rs b/makima/src/server/handlers/repository_history.rs
index c788d84..9c309c0 100644
--- a/makima/src/server/handlers/repository_history.rs
+++ b/makima/src/server/handlers/repository_history.rs
@@ -97,6 +97,14 @@ pub async fn get_repository_suggestions(
let limit = params.limit.unwrap_or(10).min(50); // Cap at 50 for safety
+ tracing::debug!(
+ owner_id = %auth.owner_id,
+ source_type = ?params.source_type,
+ query = ?params.query,
+ limit = limit,
+ "Fetching repository suggestions"
+ );
+
match repository::get_repository_suggestions(
pool,
auth.owner_id,
@@ -107,6 +115,17 @@ pub async fn get_repository_suggestions(
.await
{
Ok(entries) => {
+ // Debug log to help diagnose filtering issues
+ for entry in &entries {
+ tracing::debug!(
+ id = %entry.id,
+ name = %entry.name,
+ source_type = %entry.source_type,
+ has_url = entry.repository_url.is_some(),
+ has_path = entry.local_path.is_some(),
+ "Repository suggestion entry"
+ );
+ }
let total = entries.len() as i64;
Json(RepositoryHistoryListResponse { entries, total }).into_response()
}
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index e244a08..a4cb3d1 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, templates, transcript_analysis, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, templates, transcript_analysis, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -107,6 +107,7 @@ pub fn make_router(state: SharedState) -> Router {
// Checkpoint endpoints
.route("/mesh/tasks/{id}/checkpoint", post(mesh_supervisor::create_checkpoint))
.route("/mesh/tasks/{id}/checkpoints", get(mesh_supervisor::list_checkpoints))
+ .route("/mesh/tasks/{id}/conversation", get(history::get_task_conversation))
// Resume and rewind endpoints
.route("/mesh/tasks/{id}/rewind", post(mesh::rewind_task))
.route("/mesh/tasks/{id}/fork", post(mesh::fork_task))
@@ -166,6 +167,9 @@ pub fn make_router(state: SharedState) -> Router {
// Contract supervisor resume endpoints
.route("/contracts/{id}/supervisor/resume", post(mesh_supervisor::resume_supervisor))
.route("/contracts/{id}/supervisor/conversation/rewind", post(mesh_supervisor::rewind_conversation))
+ // History endpoints
+ .route("/contracts/{id}/history", get(history::get_contract_history))
+ .route("/contracts/{id}/supervisor/conversation", get(history::get_supervisor_conversation))
// Contract daemon endpoints (for tasks to interact with contracts)
.route("/contracts/{id}/daemon/status", get(contract_daemon::get_contract_status))
.route("/contracts/{id}/daemon/checklist", get(contract_daemon::get_contract_checklist))
@@ -198,6 +202,8 @@ pub fn make_router(state: SharedState) -> Router {
"/contracts/{id}/tasks/{task_id}",
post(contracts::add_task_to_contract).delete(contracts::remove_task_from_contract),
)
+ // Timeline endpoint (unified history for user)
+ .route("/timeline", get(history::get_timeline))
// Template endpoints
.route("/templates", get(templates::list_templates))
.route("/templates/{id}", get(templates::get_template))