-
- 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()}
-
-
-
-
-
-
-
-
- Session
-
-
- Model
-
-
- Usage
-
-
- Last active
-
-
- Open
-
-
-
-
- {sessionsQuery.isLoading ? (
- Array.from({ length: 5 }).map((_, index) => (
-
-
-
+
+
+
+
+
+ Session
+
+
+ Model
+
+
+ Usage
+
+
+ Last active
+
+
+ Open
+
+
+
+
+ {sessionsQuery.isLoading ? (
+ Array.from({ length: 5 }).map((_, index) => (
+
+
+
+
+
+ ))
+ ) : sessionsQuery.isError ? (
+
+
+
+
+ {selectedProviderLabel} sessions unavailable
+
+
+ The backend could not read this source. Check the
+ API server and try again.
+
- ))
- ) : sessionsQuery.isError ? (
-
-
-
-
- Claude Code sessions unavailable
-
-
- The backend could not read local session data. Check
- the API server and try again.
-
-
-
- ) : filteredSessions.length === 0 ? (
-
-
-
-
- No Claude Code sessions found
-
-
- Sessions appear here after Claude Code writes local
- JSONL history under your configured projects
- directory.
-
-
-
- ) : (
- filteredSessions.map((session) => (
- openSession(session)}
- onKeyDown={(event) => {
- if (event.key === "Enter") openSession(session);
- }}
- >
-
-
-
-
-
-
-
-
- {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."}
+
+
+
+ ) : (
+ filteredSessions.map((session) => (
+
openSession(session)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter") openSession(session);
+ }}
+ >
+
+
+
+
+
+
+
+
+ {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."}
-
+
- Back to Claude Code
+ Back to Agent Sessions
@@ -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) {
-
+
- Back to Claude Code
+ Back to Agent Sessions
+
+ {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) {
copyToClipboard(inputJson, () => markCopied("input"))}
+ onClick={() =>
+ copyToClipboard(inputJson, () => markCopied("input"))
+ }
>
{copied === "input" ? (
@@ -138,7 +142,9 @@ export function ToolCallBlock({ tool }: ToolCallBlockProps) {
type="button"
className="inline-flex h-8 items-center gap-2 rounded-lg border border-[color:var(--border)] px-2.5 text-xs font-semibold text-[color:var(--text-muted)] transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
onClick={() =>
- copyToClipboard(tool.result ?? "", () => markCopied("result"))
+ copyToClipboard(tool.result ?? "", () =>
+ markCopied("result"),
+ )
}
>
{copied === "result" ? (
diff --git a/frontend/src/components/claude/ToolFrequencyChart.tsx b/frontend/src/components/claude/ToolFrequencyChart.tsx
index 0bec344..c33b781 100644
--- a/frontend/src/components/claude/ToolFrequencyChart.tsx
+++ b/frontend/src/components/claude/ToolFrequencyChart.tsx
@@ -31,8 +31,8 @@ export function ToolFrequencyChart({ toolCounts }: ToolFrequencyChartProps) {
No tool calls in this range
- Tool activity appears after Claude Code sessions include assistant
- tool calls.
+ Tool activity appears after agent sessions include assistant tool
+ calls.
);
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;
+}