From e26f3aa06889dc9bbd6340eb57ef2db73081480b Mon Sep 17 00:00:00 2001 From: null Date: Sun, 24 May 2026 18:23:02 -0500 Subject: [PATCH] refactor: migrate Claude Code components to Agent Sessions - Updated session detail page to use agent session API and handle different sources. - Refactored SessionHeroHeader to accept agent session data and provider label. - Adjusted SessionMessageThread and SessionTimelineNav to utilize new agent session types. - Modified ToolAnalyticsPanel and ToolFrequencyChart to reflect changes in session terminology. - Created new agent-sessions API module to manage agent session data and analytics. - Updated DashboardSidebar to rename Claude Code to Agent Sessions. --- frontend/src/app/claude-code/page.tsx | 690 ++++++++++++------ .../app/claude-code/sessions/[id]/page.tsx | 75 +- .../components/claude/SessionHeroHeader.tsx | 28 +- .../claude/SessionMessageThread.tsx | 18 +- .../components/claude/SessionTimelineNav.tsx | 5 +- .../components/claude/ToolAnalyticsPanel.tsx | 31 +- .../src/components/claude/ToolCallBlock.tsx | 22 +- .../components/claude/ToolFrequencyChart.tsx | 4 +- .../components/organisms/DashboardSidebar.tsx | 5 +- frontend/src/lib/api/agent-sessions.ts | 254 +++++++ 10 files changed, 843 insertions(+), 289 deletions(-) create mode 100644 frontend/src/lib/api/agent-sessions.ts diff --git a/frontend/src/app/claude-code/page.tsx b/frontend/src/app/claude-code/page.tsx index 83443be..5a6f05e 100644 --- a/frontend/src/app/claude-code/page.tsx +++ b/frontend/src/app/claude-code/page.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { + AlertTriangle, ArrowUpRight, Bot, Clock3, @@ -28,16 +29,65 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { listClaudeSessions, type ClaudeSession } from "@/lib/api/claude-code"; +import { + type AgentSession, + type AgentSessionSource, + type AgentSessionSourceCard, + listAgentSessions, + listAgentSessionSources, +} from "@/lib/api/agent-sessions"; import { formatRelativeTimestamp, formatTimestamp, truncateText, } from "@/lib/formatters"; +import { cn } from "@/lib/utils"; -type ClaudeCodeTab = "sessions" | "analytics"; +type AgentSessionsTab = "sessions" | "analytics"; const ANALYTICS_DAYS: AnalyticsRangeDays[] = [7, 30, 90]; +const SOURCE_ORDER: AgentSessionSource[] = [ + "claude_code", + "codex_cli", + "openai_api", +]; + +const FALLBACK_SOURCES: AgentSessionSourceCard[] = [ + { + source: "claude_code", + provider_label: "Claude Code", + source_status: "unavailable", + source_path: null, + session_count: 0, + last_activity_at: null, + last_scanned_at: null, + unavailable_reason: "Claude Code source status has not loaded yet.", + setup_hint: null, + }, + { + source: "codex_cli", + provider_label: "Codex CLI", + source_status: "unavailable", + source_path: null, + session_count: 0, + last_activity_at: null, + last_scanned_at: null, + unavailable_reason: "Codex CLI source status has not loaded yet.", + setup_hint: null, + }, + { + source: "openai_api", + provider_label: "OpenAI API", + source_status: "unavailable", + source_path: null, + session_count: 0, + last_activity_at: null, + last_scanned_at: null, + unavailable_reason: + "OpenAI API history needs an owned Pipeline event source.", + setup_hint: null, + }, +]; function formatCost(value: number) { return new Intl.NumberFormat(undefined, { @@ -47,18 +97,128 @@ function formatCost(value: number) { }).format(value); } -function sessionHref(session: ClaudeSession) { - return `/claude-code/sessions/${encodeURIComponent(session.session_id)}`; +function normalizeSource(value: string | null): AgentSessionSource { + if (value === "codex_cli" || value === "openai_api") return value; + return "claude_code"; } -export default function ClaudeCodePage() { +function providerLabel( + source: AgentSessionSource, + card?: AgentSessionSourceCard, +) { + if (card?.provider_label) return card.provider_label; + if (source === "codex_cli") return "Codex CLI"; + if (source === "openai_api") return "OpenAI API"; + return "Claude Code"; +} + +function sessionHref(session: AgentSession, source: AgentSessionSource) { + const params = new URLSearchParams({ source }); + return `/claude-code/sessions/${encodeURIComponent(session.session_id)}?${params.toString()}`; +} + +function sourceTone(source: AgentSessionSource) { + if (source === "codex_cli") return "emerald"; + if (source === "openai_api") return "violet"; + return "cyan"; +} + +function SourceIcon({ source }: { source: AgentSessionSource }) { + if (source === "openai_api") return ; + return ; +} + +function SourceCard({ + card, + selected, + onSelect, +}: { + card: AgentSessionSourceCard; + selected: boolean; + onSelect: () => void; +}) { + const available = card.source_status === "available"; + const tone = sourceTone(card.source); + + return ( + + ); +} + +function UnavailableSourcePanel({ + card, +}: { + card: AgentSessionSourceCard | undefined; +}) { + return ( +
+ +

+ {card?.provider_label ?? "Source"} is not available yet +

+

+ {card?.unavailable_reason ?? + "This provider does not have readable local session history yet."} +

+ {card?.setup_hint ? ( +

+ {card.setup_hint} +

+ ) : null} +
+ ); +} + +export default function AgentSessionsPage() { const { isSignedIn } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const [search, setSearch] = useState(""); const [activeOnly, setActiveOnly] = useState(false); - const selectedTab: ClaudeCodeTab = + const selectedTab: AgentSessionsTab = searchParams.get("tab") === "analytics" ? "analytics" : "sessions"; + const selectedSource = normalizeSource(searchParams.get("source")); const rawDays = Number(searchParams.get("days")); const selectedDays: AnalyticsRangeDays = ANALYTICS_DAYS.includes( rawDays as AnalyticsRangeDays, @@ -66,10 +226,39 @@ export default function ClaudeCodePage() { ? (rawDays as AnalyticsRangeDays) : 30; + const sourcesQuery = useQuery({ + queryKey: ["agent-sessions", "sources"], + queryFn: listAgentSessionSources, + enabled: Boolean(isSignedIn), + refetchInterval: 30_000, + refetchOnMount: "always", + }); + + const sourceCards = useMemo(() => { + const cards = sourcesQuery.data?.sources ?? FALLBACK_SOURCES; + return [...cards].sort( + (left, right) => + SOURCE_ORDER.indexOf(left.source) - SOURCE_ORDER.indexOf(right.source), + ); + }, [sourcesQuery.data?.sources]); + + const activeSourceCard = sourceCards.find( + (card) => card.source === selectedSource, + ); + const selectedProviderLabel = providerLabel(selectedSource, activeSourceCard); + const sourceSupportsSessions = selectedSource !== "openai_api"; + const sessionsQuery = useQuery({ - queryKey: ["claude-code", "sessions", activeOnly], - queryFn: () => listClaudeSessions({ activeOnly, limit: 300 }), - enabled: Boolean(isSignedIn && selectedTab === "sessions"), + queryKey: ["agent-sessions", selectedSource, "sessions", activeOnly], + queryFn: () => + listAgentSessions({ + source: selectedSource, + activeOnly, + limit: 300, + }), + enabled: Boolean( + isSignedIn && selectedTab === "sessions" && sourceSupportsSessions, + ), refetchInterval: 30_000, refetchOnMount: "always", }); @@ -89,6 +278,7 @@ export default function ClaudeCodePage() { session.project_dir, session.cwd, session.git_branch, + session.provider_label, ...session.models, ] .filter(Boolean) @@ -98,38 +288,41 @@ export default function ClaudeCodePage() { }); }, [normalizedSearch, sessions]); - const openSession = (session: ClaudeSession) => { - router.push(sessionHref(session)); + const openSession = (session: AgentSession) => { + router.push(sessionHref(session, selectedSource)); }; - const updateCommandCenterUrl = ( - tab: ClaudeCodeTab, - days: AnalyticsRangeDays = selectedDays, - ) => { + const updateCommandCenterUrl = ({ + source = selectedSource, + tab = selectedTab, + days = selectedDays, + }: { + source?: AgentSessionSource; + tab?: AgentSessionsTab; + days?: AnalyticsRangeDays; + }) => { const params = new URLSearchParams(searchParams.toString()); + params.set("source", source); params.set("tab", tab); if (tab === "analytics") { params.set("days", String(days)); } else { params.delete("days"); } - const query = params.toString(); - router.replace(`/claude-code${query ? `?${query}` : ""}`, { - scroll: false, - }); + router.replace(`/claude-code?${params.toString()}`, { scroll: false }); }; return ( +
+ {sourceCards.map((card) => ( + + updateCommandCenterUrl({ source: card.source, tab: "sessions" }) + } + /> + ))} +
+ { if (value === "sessions" || value === "analytics") { - updateCommandCenterUrl(value); + updateCommandCenterUrl({ tab: value }); } }} className="space-y-5" >
- + Sessions @@ -162,234 +371,253 @@ export default function ClaudeCodePage() { Tool Analytics +

+ {selectedProviderLabel} + {activeSourceCard?.last_scanned_at + ? ` · scanned ${formatRelativeTimestamp(activeSourceCard.last_scanned_at)}` + : ""} +

-
-
-
-
-
-
- - - -
-

- Session command center -

-

- Open a session to read the exact conversation, tool - calls, and thinking trail. + {!sourceSupportsSessions ? ( + + ) : ( +

+
+
+
+
+
+ + + +
+

+ Trace Command Center +

+

+ Open a {selectedProviderLabel} session to inspect + conversation turns, tool calls, reasoning, and usage. +

+
+
+
+ +
+
+

+ Sessions +

+

+ {(stats?.session_count ?? 0).toLocaleString()} +

+
+
+

+ Active +

+

+ {(stats?.active_sessions ?? 0).toLocaleString()} +

+
+
+

+ Tokens +

+

+ {(stats?.total_tokens ?? 0).toLocaleString()} +

+
+
+

+ Spend +

+

+ {formatCost(stats?.total_cost_usd ?? 0)}

+
-
-
-

- Sessions -

-

- {(stats?.session_count ?? 0).toLocaleString()} -

-
-
-

- Active -

-

- {(stats?.active_sessions ?? 0).toLocaleString()} -

-
-
-

- Tokens -

-

- {(stats?.total_tokens ?? 0).toLocaleString()} -

-
-
-

- Spend -

-

- {formatCost(stats?.total_cost_usd ?? 0)} -

-
+
+
+ + setSearch(event.target.value)} + placeholder="Search sessions, projects, models, branches..." + className="pl-10" + />
+

+ Showing {filteredSessions.length.toLocaleString()} of{" "} + {sessions.length.toLocaleString()} +

-
-
-
- - setSearch(event.target.value)} - placeholder="Search sessions, projects, models, branches..." - className="pl-10" - /> -
-

- Showing {filteredSessions.length.toLocaleString()} of{" "} - {sessions.length.toLocaleString()} -

-
- -
- - - - - - - - - - - - {sessionsQuery.isLoading ? ( - Array.from({ length: 5 }).map((_, index) => ( - - + + + )) + )} + +
- Session - - Model - - Usage - - Last active - - Open -
-
+
+ + + + + + + + + + + + {sessionsQuery.isLoading ? ( + Array.from({ length: 5 }).map((_, index) => ( + + + + )) + ) : sessionsQuery.isError ? ( + + - )) - ) : sessionsQuery.isError ? ( - - - - ) : filteredSessions.length === 0 ? ( - - - - ) : ( - filteredSessions.map((session) => ( - openSession(session)} - onKeyDown={(event) => { - if (event.key === "Enter") openSession(session); - }} - > - + + + ) : ( + filteredSessions.map((session) => ( + openSession(session)} + onKeyDown={(event) => { + if (event.key === "Enter") openSession(session); + }} + > + + + - - + - - - - )) - )} - -
+ Session + + Model + + Usage + + Last active + + Open +
+
+
+ +

+ {selectedProviderLabel} sessions unavailable +

+

+ The backend could not read this source. Check the + API server and try again. +

- -

- Claude Code sessions unavailable -

-

- The backend could not read local session data. Check - the API server and try again. -

-
- -

- No Claude Code sessions found -

-

- Sessions appear here after Claude Code writes local - JSONL history under your configured projects - directory. -

-
-
- - - -
-
-

- {session.title || - truncateText(session.session_id, 20)} + ) : filteredSessions.length === 0 ? ( +

+ +

+ No {selectedProviderLabel} sessions found +

+

+ {sessionsQuery.data?.unavailable_reason ?? + activeSourceCard?.unavailable_reason ?? + "Sessions appear here after the local session source writes readable history."} +

+
+
+ + + +
+
+

+ {session.title || + truncateText(session.session_id, 20)} +

+ {session.is_active ? ( + Active + ) : null} +
+

+ {session.cwd ?? session.project_dir}

- {session.is_active ? ( - Active - ) : null}
-

- {session.cwd ?? session.project_dir} +

+
+
+ {(session.models.length > 0 + ? session.models + : ["Unknown model"] + ).map((model) => ( + + {model} + + ))} +
+
+
+

+ + {formatCost(session.cost_usd)} +

+

+ {session.tokens.total.toLocaleString()} tokens ·{" "} + {session.message_count.toLocaleString()} turns

- -
-
- {(session.models.length > 0 - ? session.models - : ["Unknown model"] - ).map((model) => ( - - {model} - - ))} -
-
-
-

- - {formatCost(session.cost_usd)} +

+

+ + {formatRelativeTimestamp(session.last_message_at)}

-

- {session.tokens.total.toLocaleString()} tokens ·{" "} - {session.message_count.toLocaleString()} turns +

+ {formatTimestamp(session.last_message_at)}

- -
-

- - {formatRelativeTimestamp(session.last_message_at)} -

-

- {formatTimestamp(session.last_message_at)} -

-
- event.stopPropagation()} - > - - -
-
- +
+ event.stopPropagation()} + > + + +
+
+
+ )} - updateCommandCenterUrl("analytics", days)} - /> + {selectedSource === "openai_api" ? ( + + ) : ( + + updateCommandCenterUrl({ tab: "analytics", days }) + } + /> + )} diff --git a/frontend/src/app/claude-code/sessions/[id]/page.tsx b/frontend/src/app/claude-code/sessions/[id]/page.tsx index c84552a..0b90d7b 100644 --- a/frontend/src/app/claude-code/sessions/[id]/page.tsx +++ b/frontend/src/app/claude-code/sessions/[id]/page.tsx @@ -3,9 +3,14 @@ export const dynamic = "force-dynamic"; import Link from "next/link"; -import { useParams } from "next/navigation"; +import { useParams, useSearchParams } from "next/navigation"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { AlertTriangle, ArrowLeft, Loader2, MessagesSquare } from "lucide-react"; +import { + AlertTriangle, + ArrowLeft, + Loader2, + MessagesSquare, +} from "lucide-react"; import { ApiError } from "@/api/mutator"; import { SessionHeroHeader } from "@/components/claude/SessionHeroHeader"; @@ -14,10 +19,11 @@ import { SessionTimelineNav } from "@/components/claude/SessionTimelineNav"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { Button } from "@/components/ui/button"; import { - getClaudeSession, - getSessionMessages, + type AgentSessionSource, + getAgentSession, + getAgentSessionMessages, type SessionMessage, -} from "@/lib/api/claude-code"; +} from "@/lib/api/agent-sessions"; const PAGE_SIZE = 200; @@ -35,7 +41,13 @@ function LoadingState() { ); } -function ErrorState({ isNotFound }: { isNotFound: boolean }) { +function ErrorState({ + isNotFound, + source, +}: { + isNotFound: boolean; + source: AgentSessionSource; +}) { return (
@@ -47,13 +59,13 @@ function ErrorState({ isNotFound }: { isNotFound: boolean }) {

{isNotFound - ? "This Claude Code session could not be found in the local history." - : "The backend could not load this Claude Code session. Check the API server and try again."} + ? "This agent session could not be found in the local history." + : "The backend could not load this agent session. Check the API server and try again."}

- +
@@ -61,21 +73,36 @@ function ErrorState({ isNotFound }: { isNotFound: boolean }) { ); } +function normalizeSource(value: string | null): AgentSessionSource { + if (value === "codex_cli" || value === "openai_api") return value; + return "claude_code"; +} + +function providerLabel(source: AgentSessionSource) { + if (source === "codex_cli") return "Codex CLI"; + if (source === "openai_api") return "OpenAI API"; + return "Claude Code"; +} + export default function ClaudeSessionDetailPage() { const params = useParams<{ id: string }>(); + const searchParams = useSearchParams(); const sessionId = decodeURIComponent(params.id); + const source = normalizeSource(searchParams.get("source")); + const label = providerLabel(source); const sessionQuery = useQuery({ - queryKey: ["claude-code", "session", sessionId], - queryFn: () => getClaudeSession(sessionId), - enabled: Boolean(sessionId), + queryKey: ["agent-sessions", source, "session", sessionId], + queryFn: () => getAgentSession(source, sessionId), + enabled: Boolean(sessionId && source !== "openai_api"), refetchOnMount: "always", }); const messagesQuery = useInfiniteQuery({ - queryKey: ["claude-code", "session", sessionId, "messages"], + queryKey: ["agent-sessions", source, "session", sessionId, "messages"], queryFn: ({ pageParam }) => - getSessionMessages({ + getAgentSessionMessages({ + source, sessionId, limit: PAGE_SIZE, offset: pageParam, @@ -85,7 +112,7 @@ export default function ClaudeSessionDetailPage() { if (!lastPage.has_more) return undefined; return pages.reduce((total, page) => total + page.messages.length, 0); }, - enabled: Boolean(sessionId), + enabled: Boolean(sessionId && source !== "openai_api"), refetchOnMount: "always", }); @@ -98,11 +125,11 @@ export default function ClaudeSessionDetailPage() { return (
) : sessionQuery.isError || messagesQuery.isError ? ( - + ) : sessionQuery.data ? ( <> - +
diff --git a/frontend/src/components/claude/SessionHeroHeader.tsx b/frontend/src/components/claude/SessionHeroHeader.tsx index 6b61b43..c5a1b6d 100644 --- a/frontend/src/components/claude/SessionHeroHeader.tsx +++ b/frontend/src/components/claude/SessionHeroHeader.tsx @@ -12,11 +12,16 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { type ClaudeSession } from "@/lib/api/claude-code"; +import { + type AgentSession, + type AgentSessionSource, +} from "@/lib/api/agent-sessions"; import { formatTimestamp, truncateText } from "@/lib/formatters"; type SessionHeroHeaderProps = { - session: ClaudeSession; + session: AgentSession; + source: AgentSessionSource; + providerLabel: string; }; function formatCost(value: number) { @@ -27,9 +32,14 @@ function formatCost(value: number) { }).format(value); } -export function SessionHeroHeader({ session }: SessionHeroHeaderProps) { +export function SessionHeroHeader({ + session, + source, + providerLabel, +}: SessionHeroHeaderProps) { const title = session.title?.trim() || truncateText(session.session_id, 18); const model = session.models[0] ?? "Model unavailable"; + const backHref = `/claude-code?source=${encodeURIComponent(source)}`; return (
@@ -37,18 +47,24 @@ export function SessionHeroHeader({ session }: SessionHeroHeaderProps) {
- +
+ + {providerLabel} + {session.is_active ? "Active" : "Complete"} {session.git_branch ? ( - + {session.git_branch} diff --git a/frontend/src/components/claude/SessionMessageThread.tsx b/frontend/src/components/claude/SessionMessageThread.tsx index a5689d0..0b18378 100644 --- a/frontend/src/components/claude/SessionMessageThread.tsx +++ b/frontend/src/components/claude/SessionMessageThread.tsx @@ -12,7 +12,7 @@ import { } from "lucide-react"; import { ToolCallBlock } from "@/components/claude/ToolCallBlock"; -import { type SessionMessage } from "@/lib/api/claude-code"; +import { type SessionMessage } from "@/lib/api/agent-sessions"; import { formatTimestamp } from "@/lib/formatters"; import { cn } from "@/lib/utils"; @@ -21,7 +21,8 @@ type SessionMessageThreadProps = { }; async function copyText(value: string, onCopied: () => void) { - if (!value || typeof navigator === "undefined" || !navigator.clipboard) return; + if (!value || typeof navigator === "undefined" || !navigator.clipboard) + return; await navigator.clipboard.writeText(value); onCopied(); } @@ -57,8 +58,8 @@ export function SessionMessageThread({ messages }: SessionMessageThreadProps) { No conversation messages

- This session was found, but there are no displayable user or assistant turns - in the selected page. + This session was found, but there are no displayable user or assistant + turns in the selected page.

); @@ -115,7 +116,9 @@ export function SessionMessageThread({ messages }: SessionMessageThreadProps) {

{formatTimestamp(message.timestamp)} - {tokens !== null ? ` · ${tokens.toLocaleString()} tokens` : ""} + {tokens !== null + ? ` · ${tokens.toLocaleString()} tokens` + : ""}

@@ -155,7 +158,10 @@ export function SessionMessageThread({ messages }: SessionMessageThreadProps) { Thinking {thinkingOpen ? ( diff --git a/frontend/src/components/claude/SessionTimelineNav.tsx b/frontend/src/components/claude/SessionTimelineNav.tsx index f9c9561..5259ad9 100644 --- a/frontend/src/components/claude/SessionTimelineNav.tsx +++ b/frontend/src/components/claude/SessionTimelineNav.tsx @@ -2,7 +2,7 @@ import { Bot, Terminal, UserRound } from "lucide-react"; -import { type SessionMessage } from "@/lib/api/claude-code"; +import { type SessionMessage } from "@/lib/api/agent-sessions"; import { formatTimestamp, truncateText } from "@/lib/formatters"; import { cn } from "@/lib/utils"; @@ -72,7 +72,8 @@ export function SessionTimelineNav({ messages }: SessionTimelineNavProps) { })} {messages.length > visibleMessages.length ? (

- Showing first {visibleMessages.length.toLocaleString()} timeline items. + Showing first {visibleMessages.length.toLocaleString()} timeline + items.

) : null} diff --git a/frontend/src/components/claude/ToolAnalyticsPanel.tsx b/frontend/src/components/claude/ToolAnalyticsPanel.tsx index 9e013ec..910ce95 100644 --- a/frontend/src/components/claude/ToolAnalyticsPanel.tsx +++ b/frontend/src/components/claude/ToolAnalyticsPanel.tsx @@ -16,15 +16,21 @@ import { } from "@/components/claude/RankedAnalyticsList"; import { ToolFrequencyChart } from "@/components/claude/ToolFrequencyChart"; import { Button } from "@/components/ui/button"; -import { getToolAnalytics } from "@/lib/api/claude-code"; +import { + type AgentSessionSource, + getAgentToolAnalytics, +} from "@/lib/api/agent-sessions"; import { cn } from "@/lib/utils"; export type AnalyticsRangeDays = 7 | 30 | 90; type ToolAnalyticsPanelProps = { + source: AgentSessionSource; + providerLabel: string; days: AnalyticsRangeDays; onDaysChange: (days: AnalyticsRangeDays) => void; enabled?: boolean; + unavailableReason?: string | null; }; const RANGE_OPTIONS: AnalyticsRangeDays[] = [7, 30, 90]; @@ -63,7 +69,7 @@ function LoadingSkeleton() { ); } -function EmptyState() { +function EmptyState({ providerLabel }: { providerLabel: string }) { return (
@@ -71,21 +77,24 @@ function EmptyState() { No tool analytics yet

- Tool analytics appear after local Claude Code sessions include assistant - tool calls in the selected date range. + Tool analytics appear after local {providerLabel} sessions include + assistant tool calls in the selected date range.

); } export function ToolAnalyticsPanel({ + source, + providerLabel, days, onDaysChange, enabled = true, + unavailableReason, }: ToolAnalyticsPanelProps) { const analyticsQuery = useQuery({ - queryKey: ["claude-code", "tool-analytics", days], - queryFn: () => getToolAnalytics({ days }), + queryKey: ["agent-sessions", source, "tool-analytics", days], + queryFn: () => getAgentToolAnalytics({ source, days }), enabled, refetchOnMount: "always", }); @@ -120,9 +129,11 @@ export function ToolAnalyticsPanel({ Tool Analytics

- {analyticsQuery.isFetching && !analyticsQuery.isLoading - ? "Refreshing selected range..." - : `${days.toLocaleString()} day operating window`} + {unavailableReason + ? unavailableReason + : analyticsQuery.isFetching && !analyticsQuery.isLoading + ? "Refreshing selected range..." + : `${providerLabel} · ${days.toLocaleString()} day operating window`}

+ ) : null} {analytics && totalCalls > 0 ? ( diff --git a/frontend/src/components/claude/ToolCallBlock.tsx b/frontend/src/components/claude/ToolCallBlock.tsx index 223eec8..6d94c55 100644 --- a/frontend/src/components/claude/ToolCallBlock.tsx +++ b/frontend/src/components/claude/ToolCallBlock.tsx @@ -10,7 +10,7 @@ import { Wrench, } from "lucide-react"; -import { type SessionToolUseBlock } from "@/lib/api/claude-code"; +import { type SessionToolUseBlock } from "@/lib/api/agent-sessions"; import { cn } from "@/lib/utils"; type ToolCallBlockProps = { @@ -38,7 +38,8 @@ function summarizeToolInput(tool: SessionToolUseBlock): string { } async function copyToClipboard(value: string, onCopied: () => void) { - if (!value || typeof navigator === "undefined" || !navigator.clipboard) return; + if (!value || typeof navigator === "undefined" || !navigator.clipboard) + return; await navigator.clipboard.writeText(value); onCopied(); } @@ -46,7 +47,10 @@ async function copyToClipboard(value: string, onCopied: () => void) { export function ToolCallBlock({ tool }: ToolCallBlockProps) { const [open, setOpen] = useState(false); const [copied, setCopied] = useState<"input" | "result" | null>(null); - const inputJson = useMemo(() => JSON.stringify(tool.input, null, 2), [tool.input]); + const inputJson = useMemo( + () => JSON.stringify(tool.input, null, 2), + [tool.input], + ); const summary = summarizeToolInput(tool); const markCopied = (kind: "input" | "result") => { @@ -58,9 +62,7 @@ export function ToolCallBlock({ tool }: ToolCallBlockProps) {
); diff --git a/frontend/src/components/organisms/DashboardSidebar.tsx b/frontend/src/components/organisms/DashboardSidebar.tsx index 58b5b1b..cbeb4cc 100644 --- a/frontend/src/components/organisms/DashboardSidebar.tsx +++ b/frontend/src/components/organisms/DashboardSidebar.tsx @@ -227,7 +227,7 @@ export function DashboardSidebar() {
} tone="cyan" active={isActive("/claude-code")} @@ -308,7 +308,8 @@ export function DashboardSidebar() { = { + data: T; + status: number; + headers: Headers; +}; + +export type AgentSessionSource = "claude_code" | "codex_cli" | "openai_api"; +export type AgentSourceStatus = "available" | "unavailable" | "unsupported"; + +export type AgentSessionTokens = { + input: number; + output: number; + cache_read: number; + cache_write: number; + total: number; +}; + +export type AgentSession = { + session_id: string; + source?: AgentSessionSource; + provider_label?: string; + project_dir: string; + cwd: string | null; + title: string | null; + models: string[]; + tokens: AgentSessionTokens; + cost_usd: number; + billing_source?: string; + message_count: number; + first_message_at: string | null; + last_message_at: string | null; + is_active: boolean; + entrypoints: string[]; + git_branch: string | null; + version: string | null; +}; + +export type AgentSessionStats = { + session_count: number; + active_sessions: number; + total_tokens: number; + total_cost_usd: number; + models: string[]; +}; + +export type AgentSessionListResponse = { + sessions: AgentSession[]; + total: number; + stats: AgentSessionStats; + source?: AgentSessionSource; + provider_label?: string; + source_status?: AgentSourceStatus; + source_path?: string | null; + last_scanned_at?: string | null; + unavailable_reason?: string | null; + setup_hint?: string | null; +}; + +export type ToolAnalyticsFileEntry = { + path: string; + count: number; +}; + +export type ToolAnalyticsCommandEntry = { + command: string; + count: number; +}; + +export type ToolAnalyticsResponse = { + tool_counts: Record; + top_files_read: ToolAnalyticsFileEntry[]; + top_files_written: ToolAnalyticsFileEntry[]; + top_commands: ToolAnalyticsCommandEntry[]; + session_count: number; + date_range_days: number; + source?: AgentSessionSource | null; + source_status?: AgentSourceStatus | null; + source_path?: string | null; + last_scanned_at?: string | null; +}; + +export type SessionTextBlock = { + text: string; + truncated: boolean; +}; + +export type SessionThinkingBlock = { + text: string; + truncated: boolean; +}; + +export type SessionToolUseBlock = { + tool_use_id: string; + tool_name: string; + input: Record; + input_truncated: boolean; + result: string | null; + result_truncated: boolean; + is_error: boolean; +}; + +export type SessionTokenUsage = { + input: number; + output: number; + cache_read: number; + cache_write: number; +}; + +export type SessionMessage = { + uuid: string; + role: "user" | "assistant" | string; + timestamp: string | null; + text_blocks: SessionTextBlock[]; + thinking_blocks: SessionThinkingBlock[]; + tool_uses: SessionToolUseBlock[]; + model: string | null; + tokens: SessionTokenUsage | null; +}; + +export type SessionMessagesResponse = { + session_id: string; + source?: AgentSessionSource; + source_status?: AgentSourceStatus; + source_path?: string | null; + last_scanned_at?: string | null; + messages: SessionMessage[]; + total: number; + has_more: boolean; +}; + +export type AgentSessionSourceCard = { + source: AgentSessionSource; + provider_label: string; + source_status: AgentSourceStatus; + source_path: string | null; + session_count: number; + last_activity_at: string | null; + last_scanned_at: string | null; + unavailable_reason: string | null; + setup_hint: string | null; +}; + +export type AgentSessionSourcesResponse = { + sources: AgentSessionSourceCard[]; + last_scanned_at: string; +}; + +export type ListAgentSessionsParams = { + source: AgentSessionSource; + project?: string; + activeOnly?: boolean; + limit?: number; +}; + +export type GetToolAnalyticsParams = { + source: AgentSessionSource; + days?: 7 | 30 | 90 | number; + project?: string; +}; + +export type GetSessionMessagesParams = { + source: AgentSessionSource; + sessionId: string; + limit?: number; + offset?: number; +}; + +function sourcePath(source: AgentSessionSource) { + if (source === "codex_cli") return "codex"; + if (source === "claude_code") return "claude-code"; + return null; +} + +function supportedSourcePath(source: AgentSessionSource) { + const path = sourcePath(source); + if (!path) { + throw new Error("OpenAI API session history is not available yet."); + } + return path; +} + +export async function listAgentSessionSources(): Promise { + const response = await customFetch>( + "/api/v1/agent-sessions/sources", + { method: "GET" }, + ); + return response.data; +} + +export async function listAgentSessions({ + source, + project, + activeOnly = false, + limit = 100, +}: ListAgentSessionsParams): Promise { + const params = new URLSearchParams(); + if (project?.trim()) params.set("project", project.trim()); + if (activeOnly) params.set("active_only", "true"); + params.set("limit", String(limit)); + + const query = params.toString(); + const response = await customFetch>( + `/api/v1/${supportedSourcePath(source)}/sessions${query ? `?${query}` : ""}`, + { method: "GET" }, + ); + return response.data; +} + +export async function getAgentToolAnalytics({ + source, + days = 30, + project, +}: GetToolAnalyticsParams): Promise { + const params = new URLSearchParams({ days: String(days) }); + if (project?.trim()) params.set("project", project.trim()); + + const response = await customFetch>( + `/api/v1/${supportedSourcePath(source)}/analytics/tools?${params.toString()}`, + { method: "GET" }, + ); + return response.data; +} + +export async function getAgentSession( + source: AgentSessionSource, + sessionId: string, +): Promise { + const response = await customFetch>( + `/api/v1/${supportedSourcePath(source)}/sessions/${encodeURIComponent(sessionId)}`, + { method: "GET" }, + ); + return response.data; +} + +export async function getAgentSessionMessages({ + source, + sessionId, + limit = 200, + offset = 0, +}: GetSessionMessagesParams): Promise { + const params = new URLSearchParams({ + limit: String(limit), + offset: String(offset), + }); + const response = await customFetch>( + `/api/v1/${supportedSourcePath(source)}/sessions/${encodeURIComponent(sessionId)}/messages?${params.toString()}`, + { method: "GET" }, + ); + return response.data; +}