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.
This commit is contained in:
parent
fe6d9f219a
commit
e26f3aa068
|
|
@ -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 <Bot className="h-4 w-4" />;
|
||||
return <TerminalSquare className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
function SourceCard({
|
||||
card,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
card: AgentSessionSourceCard;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const available = card.source_status === "available";
|
||||
const tone = sourceTone(card.source);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"min-h-[124px] rounded-2xl border p-4 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]",
|
||||
selected
|
||||
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)] shadow-sm"
|
||||
: "border-[color:var(--border)] bg-[color:var(--surface)] hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-muted)]/45",
|
||||
)}
|
||||
onClick={onSelect}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-9 w-9 items-center justify-center rounded-xl ring-1",
|
||||
tone === "emerald" &&
|
||||
"bg-emerald-500/15 text-emerald-300 ring-emerald-400/20",
|
||||
tone === "violet" &&
|
||||
"bg-violet-500/15 text-violet-300 ring-violet-400/20",
|
||||
tone === "cyan" && "bg-cyan-500/15 text-cyan-300 ring-cyan-400/20",
|
||||
)}
|
||||
>
|
||||
<SourceIcon source={card.source} />
|
||||
</span>
|
||||
<Badge variant={available ? "success" : "outline"}>
|
||||
{available ? "Available" : "Unavailable"}
|
||||
</Badge>
|
||||
</div>
|
||||
<h2 className="mt-3 text-sm font-semibold text-[color:var(--text)]">
|
||||
{card.provider_label}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-[color:var(--text-muted)]">
|
||||
{available
|
||||
? `${card.session_count.toLocaleString()} sessions · ${formatRelativeTimestamp(card.last_activity_at)}`
|
||||
: card.unavailable_reason}
|
||||
</p>
|
||||
{card.source_path ? (
|
||||
<p className="mt-2 truncate font-mono text-[11px] text-[color:var(--text-quiet)]">
|
||||
{card.source_path}
|
||||
</p>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function UnavailableSourcePanel({
|
||||
card,
|
||||
}: {
|
||||
card: AgentSessionSourceCard | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-[color:var(--border-strong)] bg-[color:var(--surface)] p-10 text-center">
|
||||
<AlertTriangle className="mx-auto h-10 w-10 text-[color:var(--text-muted)]" />
|
||||
<h2 className="mt-4 text-lg font-semibold text-[color:var(--text)]">
|
||||
{card?.provider_label ?? "Source"} is not available yet
|
||||
</h2>
|
||||
<p className="mx-auto mt-2 max-w-xl text-sm leading-6 text-[color:var(--text-muted)]">
|
||||
{card?.unavailable_reason ??
|
||||
"This provider does not have readable local session history yet."}
|
||||
</p>
|
||||
{card?.setup_hint ? (
|
||||
<p className="mx-auto mt-3 max-w-xl text-sm leading-6 text-[color:var(--text-muted)]">
|
||||
{card.setup_hint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DashboardPageLayout
|
||||
signedOut={{
|
||||
message: "Sign in to view local Claude Code sessions.",
|
||||
message: "Sign in to view local agent sessions.",
|
||||
forceRedirectUrl: "/claude-code",
|
||||
signUpForceRedirectUrl: "/claude-code",
|
||||
}}
|
||||
title="Claude Code"
|
||||
description="Inspect local agent sessions, costs, tools, and conversation history."
|
||||
title="Agent Sessions"
|
||||
description="Inspect local Claude, Codex, and future OpenAI agent traces from one operational view."
|
||||
headerActions={
|
||||
selectedTab === "sessions" ? (
|
||||
selectedTab === "sessions" && sourceSupportsSessions ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant={activeOnly ? "primary" : "outline"}
|
||||
|
|
@ -142,17 +335,33 @@ export default function ClaudeCodePage() {
|
|||
}
|
||||
contentClassName="space-y-6"
|
||||
>
|
||||
<section
|
||||
className="grid gap-3 md:grid-cols-3"
|
||||
aria-label="Agent session sources"
|
||||
>
|
||||
{sourceCards.map((card) => (
|
||||
<SourceCard
|
||||
key={card.source}
|
||||
card={card}
|
||||
selected={card.source === selectedSource}
|
||||
onSelect={() =>
|
||||
updateCommandCenterUrl({ source: card.source, tab: "sessions" })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<Tabs
|
||||
value={selectedTab}
|
||||
onValueChange={(value) => {
|
||||
if (value === "sessions" || value === "analytics") {
|
||||
updateCommandCenterUrl(value);
|
||||
updateCommandCenterUrl({ tab: value });
|
||||
}
|
||||
}}
|
||||
className="space-y-5"
|
||||
>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<TabsList aria-label="Claude Code views">
|
||||
<TabsList aria-label="Agent session views">
|
||||
<TabsTrigger value="sessions">
|
||||
<MessagesSquare className="mr-2 h-3.5 w-3.5" />
|
||||
Sessions
|
||||
|
|
@ -162,12 +371,21 @@ export default function ClaudeCodePage() {
|
|||
Tool Analytics
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<p className="text-sm text-[color:var(--text-muted)]">
|
||||
{selectedProviderLabel}
|
||||
{activeSourceCard?.last_scanned_at
|
||||
? ` · scanned ${formatRelativeTimestamp(activeSourceCard.last_scanned_at)}`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TabsContent value="sessions" className="mt-0">
|
||||
{!sourceSupportsSessions ? (
|
||||
<UnavailableSourcePanel card={activeSourceCard} />
|
||||
) : (
|
||||
<section className="overflow-hidden rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] shadow-sm">
|
||||
<div className="relative border-b border-[color:var(--border)] px-5 py-6 md:px-6">
|
||||
<div className="absolute inset-x-0 top-0 h-1 bg-[linear-gradient(90deg,#06b6d4,#8b5cf6,#22c55e)]" />
|
||||
<div className="absolute inset-x-0 top-0 h-1 bg-[linear-gradient(90deg,#06b6d4,#22c55e,#8b5cf6)]" />
|
||||
<div className="flex flex-col gap-5 xl:flex-row xl:items-end xl:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -176,11 +394,11 @@ export default function ClaudeCodePage() {
|
|||
</span>
|
||||
<div>
|
||||
<h2 className="font-heading text-xl font-semibold text-[color:var(--text)]">
|
||||
Session command center
|
||||
Trace Command Center
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-[color:var(--text-muted)]">
|
||||
Open a session to read the exact conversation, tool
|
||||
calls, and thinking trail.
|
||||
Open a {selectedProviderLabel} session to inspect
|
||||
conversation turns, tool calls, reasoning, and usage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -272,13 +490,13 @@ export default function ClaudeCodePage() {
|
|||
) : sessionsQuery.isError ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-5 py-16 text-center">
|
||||
<Bot className="mx-auto h-10 w-10 text-rose-300" />
|
||||
<AlertTriangle className="mx-auto h-10 w-10 text-rose-300" />
|
||||
<h3 className="mt-4 text-lg font-semibold text-[color:var(--text)]">
|
||||
Claude Code sessions unavailable
|
||||
{selectedProviderLabel} sessions unavailable
|
||||
</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-[color:var(--text-muted)]">
|
||||
The backend could not read local session data. Check
|
||||
the API server and try again.
|
||||
The backend could not read this source. Check the
|
||||
API server and try again.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -287,12 +505,12 @@ export default function ClaudeCodePage() {
|
|||
<td colSpan={5} className="px-5 py-16 text-center">
|
||||
<MessagesSquare className="mx-auto h-10 w-10 text-[color:var(--text-muted)]" />
|
||||
<h3 className="mt-4 text-lg font-semibold text-[color:var(--text)]">
|
||||
No Claude Code sessions found
|
||||
No {selectedProviderLabel} sessions found
|
||||
</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-[color:var(--text-muted)]">
|
||||
Sessions appear here after Claude Code writes local
|
||||
JSONL history under your configured projects
|
||||
directory.
|
||||
{sessionsQuery.data?.unavailable_reason ??
|
||||
activeSourceCard?.unavailable_reason ??
|
||||
"Sessions appear here after the local session source writes readable history."}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -367,7 +585,7 @@ export default function ClaudeCodePage() {
|
|||
</td>
|
||||
<td className="px-5 py-4 text-right">
|
||||
<Link
|
||||
href={sessionHref(session)}
|
||||
href={sessionHref(session, selectedSource)}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-[color:var(--border)] 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)]"
|
||||
aria-label={`Open ${session.title ?? session.session_id}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
|
|
@ -382,14 +600,24 @@ export default function ClaudeCodePage() {
|
|||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="mt-0">
|
||||
{selectedSource === "openai_api" ? (
|
||||
<UnavailableSourcePanel card={activeSourceCard} />
|
||||
) : (
|
||||
<ToolAnalyticsPanel
|
||||
source={selectedSource}
|
||||
providerLabel={selectedProviderLabel}
|
||||
days={selectedDays}
|
||||
enabled={Boolean(isSignedIn && selectedTab === "analytics")}
|
||||
onDaysChange={(days) => updateCommandCenterUrl("analytics", days)}
|
||||
unavailableReason={activeSourceCard?.unavailable_reason}
|
||||
onDaysChange={(days) =>
|
||||
updateCommandCenterUrl({ tab: "analytics", days })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DashboardPageLayout>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex min-h-[520px] items-center justify-center px-4">
|
||||
<div className="max-w-lg rounded-2xl border border-[color:var(--border)] bg-[color:var(--surface)] p-8 text-center shadow-sm">
|
||||
|
|
@ -47,13 +59,13 @@ function ErrorState({ isNotFound }: { isNotFound: boolean }) {
|
|||
</h1>
|
||||
<p className="mt-2 text-sm leading-6 text-[color:var(--text-muted)]">
|
||||
{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."}
|
||||
</p>
|
||||
<Link href="/claude-code">
|
||||
<Link href={`/claude-code?source=${encodeURIComponent(source)}`}>
|
||||
<Button className="mt-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Claude Code
|
||||
Back to Agent Sessions
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -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 (
|
||||
<DashboardPageLayout
|
||||
signedOut={{
|
||||
message: "Sign in to view Claude Code session details.",
|
||||
forceRedirectUrl: `/claude-code/sessions/${encodeURIComponent(sessionId)}`,
|
||||
signUpForceRedirectUrl: `/claude-code/sessions/${encodeURIComponent(sessionId)}`,
|
||||
message: "Sign in to view agent session details.",
|
||||
forceRedirectUrl: `/claude-code/sessions/${encodeURIComponent(sessionId)}?source=${encodeURIComponent(source)}`,
|
||||
signUpForceRedirectUrl: `/claude-code/sessions/${encodeURIComponent(sessionId)}?source=${encodeURIComponent(source)}`,
|
||||
}}
|
||||
title="Claude Code session"
|
||||
title="Agent session"
|
||||
description="Conversation trace, thinking blocks, and tool activity."
|
||||
contentClassName="p-0 md:p-0"
|
||||
headerClassName="sr-only"
|
||||
|
|
@ -112,10 +139,14 @@ export default function ClaudeSessionDetailPage() {
|
|||
<LoadingState />
|
||||
</div>
|
||||
) : sessionQuery.isError || messagesQuery.isError ? (
|
||||
<ErrorState isNotFound={isNotFound} />
|
||||
<ErrorState isNotFound={isNotFound} source={source} />
|
||||
) : sessionQuery.data ? (
|
||||
<>
|
||||
<SessionHeroHeader session={sessionQuery.data} />
|
||||
<SessionHeroHeader
|
||||
session={sessionQuery.data}
|
||||
source={source}
|
||||
providerLabel={label}
|
||||
/>
|
||||
<div className="px-4 py-6 md:px-8">
|
||||
<div className="mx-auto flex max-w-[1480px] items-start gap-6">
|
||||
<main className="min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="relative overflow-hidden border-b border-[color:var(--border)] bg-[color:var(--surface)]">
|
||||
|
|
@ -37,18 +47,24 @@ export function SessionHeroHeader({ session }: SessionHeroHeaderProps) {
|
|||
<div className="px-4 py-5 md:px-8 md:py-7">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<Link href="/claude-code">
|
||||
<Link href={backHref}>
|
||||
<Button variant="ghost" size="sm" className="mb-4 px-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Claude Code
|
||||
Back to Agent Sessions
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline" className="normal-case tracking-normal">
|
||||
{providerLabel}
|
||||
</Badge>
|
||||
<Badge variant={session.is_active ? "success" : "outline"}>
|
||||
{session.is_active ? "Active" : "Complete"}
|
||||
</Badge>
|
||||
{session.git_branch ? (
|
||||
<Badge variant="outline" className="normal-case tracking-normal">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="normal-case tracking-normal"
|
||||
>
|
||||
<GitBranch className="mr-1 h-3 w-3" />
|
||||
{session.git_branch}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-[color:var(--text-muted)]">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -115,7 +116,9 @@ export function SessionMessageThread({ messages }: SessionMessageThreadProps) {
|
|||
</div>
|
||||
<p className="mt-0.5 text-xs text-[color:var(--text-muted)]">
|
||||
{formatTimestamp(message.timestamp)}
|
||||
{tokens !== null ? ` · ${tokens.toLocaleString()} tokens` : ""}
|
||||
{tokens !== null
|
||||
? ` · ${tokens.toLocaleString()} tokens`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +158,10 @@ export function SessionMessageThread({ messages }: SessionMessageThreadProps) {
|
|||
Thinking
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={cn("h-4 w-4 transition", thinkingOpen && "rotate-180")}
|
||||
className={cn(
|
||||
"h-4 w-4 transition",
|
||||
thinkingOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{thinkingOpen ? (
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<p className="px-3 py-3 text-xs text-[color:var(--text-muted)]">
|
||||
Showing first {visibleMessages.length.toLocaleString()} timeline items.
|
||||
Showing first {visibleMessages.length.toLocaleString()} timeline
|
||||
items.
|
||||
</p>
|
||||
) : null}
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="rounded-2xl border border-dashed border-[color:var(--border-strong)] bg-[color:var(--surface)] p-10 text-center">
|
||||
<Wrench className="mx-auto h-10 w-10 text-[color:var(--text-muted)]" />
|
||||
|
|
@ -71,21 +77,24 @@ function EmptyState() {
|
|||
No tool analytics yet
|
||||
</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm leading-6 text-[color:var(--text-muted)]">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-[color:var(--text-muted)]">
|
||||
{analyticsQuery.isFetching && !analyticsQuery.isLoading
|
||||
{unavailableReason
|
||||
? unavailableReason
|
||||
: analyticsQuery.isFetching && !analyticsQuery.isLoading
|
||||
? "Refreshing selected range..."
|
||||
: `${days.toLocaleString()} day operating window`}
|
||||
: `${providerLabel} · ${days.toLocaleString()} day operating window`}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -177,7 +188,7 @@ export function ToolAnalyticsPanel({
|
|||
) : null}
|
||||
|
||||
{analytics && !analyticsQuery.isError && totalCalls === 0 ? (
|
||||
<EmptyState />
|
||||
<EmptyState providerLabel={providerLabel} />
|
||||
) : null}
|
||||
|
||||
{analytics && totalCalls > 0 ? (
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-xl border bg-[color:var(--surface)] shadow-sm",
|
||||
tool.is_error
|
||||
? "border-rose-400/30"
|
||||
: "border-[color:var(--border)]",
|
||||
tool.is_error ? "border-rose-400/30" : "border-[color:var(--border)]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
|
|
@ -112,7 +114,9 @@ export function ToolCallBlock({ tool }: ToolCallBlockProps) {
|
|||
<button
|
||||
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(inputJson, () => markCopied("input"))}
|
||||
onClick={() =>
|
||||
copyToClipboard(inputJson, () => markCopied("input"))
|
||||
}
|
||||
>
|
||||
{copied === "input" ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
|
|
@ -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" ? (
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ export function ToolFrequencyChart({ toolCounts }: ToolFrequencyChartProps) {
|
|||
No tool calls in this range
|
||||
</h3>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm text-[color:var(--text-muted)]">
|
||||
Tool activity appears after Claude Code sessions include assistant
|
||||
tool calls.
|
||||
Tool activity appears after agent sessions include assistant tool
|
||||
calls.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export function DashboardSidebar() {
|
|||
<div className="mt-2 space-y-1.5">
|
||||
<NavItem
|
||||
href="/claude-code"
|
||||
label="Claude Code"
|
||||
label="Agent Sessions"
|
||||
icon={<TerminalSquare className="h-4 w-4" />}
|
||||
tone="cyan"
|
||||
active={isActive("/claude-code")}
|
||||
|
|
@ -308,7 +308,8 @@ export function DashboardSidebar() {
|
|||
<span
|
||||
className={cn(
|
||||
"h-2.5 w-2.5 rounded-full shadow-[0_0_18px_currentColor]",
|
||||
systemStatus === "operational" && "bg-emerald-500 text-emerald-500",
|
||||
systemStatus === "operational" &&
|
||||
"bg-emerald-500 text-emerald-500",
|
||||
systemStatus === "degraded" && "bg-rose-500 text-rose-500",
|
||||
systemStatus === "unknown" &&
|
||||
"bg-[color:var(--text-quiet)] text-[color:var(--text-quiet)]",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,254 @@
|
|||
"use client";
|
||||
|
||||
import { customFetch } from "@/api/mutator";
|
||||
|
||||
type ApiResponse<T> = {
|
||||
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<string, number>;
|
||||
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<string, unknown>;
|
||||
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<AgentSessionSourcesResponse> {
|
||||
const response = await customFetch<ApiResponse<AgentSessionSourcesResponse>>(
|
||||
"/api/v1/agent-sessions/sources",
|
||||
{ method: "GET" },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function listAgentSessions({
|
||||
source,
|
||||
project,
|
||||
activeOnly = false,
|
||||
limit = 100,
|
||||
}: ListAgentSessionsParams): Promise<AgentSessionListResponse> {
|
||||
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<ApiResponse<AgentSessionListResponse>>(
|
||||
`/api/v1/${supportedSourcePath(source)}/sessions${query ? `?${query}` : ""}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getAgentToolAnalytics({
|
||||
source,
|
||||
days = 30,
|
||||
project,
|
||||
}: GetToolAnalyticsParams): Promise<ToolAnalyticsResponse> {
|
||||
const params = new URLSearchParams({ days: String(days) });
|
||||
if (project?.trim()) params.set("project", project.trim());
|
||||
|
||||
const response = await customFetch<ApiResponse<ToolAnalyticsResponse>>(
|
||||
`/api/v1/${supportedSourcePath(source)}/analytics/tools?${params.toString()}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getAgentSession(
|
||||
source: AgentSessionSource,
|
||||
sessionId: string,
|
||||
): Promise<AgentSession> {
|
||||
const response = await customFetch<ApiResponse<AgentSession>>(
|
||||
`/api/v1/${supportedSourcePath(source)}/sessions/${encodeURIComponent(sessionId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getAgentSessionMessages({
|
||||
source,
|
||||
sessionId,
|
||||
limit = 200,
|
||||
offset = 0,
|
||||
}: GetSessionMessagesParams): Promise<SessionMessagesResponse> {
|
||||
const params = new URLSearchParams({
|
||||
limit: String(limit),
|
||||
offset: String(offset),
|
||||
});
|
||||
const response = await customFetch<ApiResponse<SessionMessagesResponse>>(
|
||||
`/api/v1/${supportedSourcePath(source)}/sessions/${encodeURIComponent(sessionId)}/messages?${params.toString()}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
Loading…
Reference in New Issue