summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
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 /makima/frontend/src/routes
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
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/contracts.tsx2
-rw-r--r--makima/frontend/src/routes/history.tsx325
2 files changed, 326 insertions, 1 deletions
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>
+ );
+}