From 205ab8a223ddf6591a3e8bfc9108506502977c11 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Fri, 16 Jan 2026 12:23:49 +0000
Subject: Fixup: use default api.makima.jp URL and fix default branch detection

Also add checkpointing/history
---
 makima/frontend/src/routes/history.tsx | 325 +++++++++++++++++++++++++++++++++
 1 file changed, 325 insertions(+)
 create mode 100644 makima/frontend/src/routes/history.tsx

(limited to 'makima/frontend/src/routes/history.tsx')

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>
+  );
+}
-- 
cgit v1.2.3

