Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/(app)/code-reviews/ReviewAgentPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
<AlertTitle>No Jobs Yet</AlertTitle>
<AlertDescription>
Connect GitLab and configure your review settings to see code review jobs here.
Connect GitLab and configure your review settings to see processed reviews here.
</AlertDescription>
</Alert>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ export function ReviewAgentPageClient({
<ListChecks className="h-4 w-4" />
<AlertTitle>No Jobs Yet</AlertTitle>
<AlertDescription>
Connect GitLab and configure your review settings to see code review jobs here.
Connect GitLab and configure your review settings to see processed reviews here.
</AlertDescription>
</Alert>
)}
Expand Down
42 changes: 38 additions & 4 deletions src/components/code-reviews/CodeReviewJobsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,29 @@ import {
ChevronRight,
RotateCcw,
Ban,
DollarSign,
Cpu,
} from 'lucide-react';
import { toast } from 'sonner';
import { useTRPC } from '@/lib/trpc/utils';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { formatDistanceToNow } from 'date-fns';
import { CodeReviewStreamView } from './CodeReviewStreamView';

function formatCostFromMicrodollars(microdollars: number): string {
const dollars = microdollars / 1_000_000;
if (dollars < 0.01) {
return '<$0.01';
}
return `$${dollars.toFixed(2)}`;
}

function formatModelName(model: string): string {
// Strip provider prefix (e.g. "anthropic/claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514")
const lastSlash = model.lastIndexOf('/');
return lastSlash >= 0 ? model.slice(lastSlash + 1) : model;
}

type Platform = 'github' | 'gitlab';

type CodeReviewJobsCardProps = {
Expand Down Expand Up @@ -159,7 +175,7 @@ export function CodeReviewJobsCard({
return (
<Card>
<CardHeader>
<CardTitle>Code Review Jobs</CardTitle>
<CardTitle>Processed Reviews</CardTitle>
<CardDescription>Loading...</CardDescription>
</CardHeader>
</Card>
Expand All @@ -180,13 +196,13 @@ export function CodeReviewJobsCard({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitPullRequest className="h-5 w-5" />
Code Review Jobs
Processed Reviews
</CardTitle>
<CardDescription>No code reviews yet</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-sm">
Code review jobs will appear here when {prLabel} are reviewed.
Processed reviews will appear here when {prLabel} are reviewed.
</p>
</CardContent>
</Card>
Expand All @@ -198,7 +214,7 @@ export function CodeReviewJobsCard({
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GitPullRequest className="h-5 w-5" />
Code Review Jobs
Processed Reviews
</CardTitle>
<CardDescription>
{total > 0 ? (
Expand Down Expand Up @@ -293,6 +309,24 @@ export function CodeReviewJobsCard({
)}
</div>

{/* Cost & Model */}
{(review.total_cost_microdollars != null || review.model) && (
<div className="text-muted-foreground flex items-center gap-3 text-xs">
{review.total_cost_microdollars != null && (
<span className="inline-flex items-center gap-1">
<DollarSign className="h-3 w-3" />
{formatCostFromMicrodollars(review.total_cost_microdollars)}
</span>
)}
{review.model && (
<span className="inline-flex items-center gap-1">
<Cpu className="h-3 w-3" />
{formatModelName(review.model)}
</span>
)}
</div>
)}

{/* Error Message */}
{review.error_message && (
<div className="text-destructive mt-1 text-xs">
Expand Down
4 changes: 2 additions & 2 deletions src/lib/code-reviews/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import * as z from 'zod';
import type { CloudAgentCodeReview } from '@/db/schema';
import type { CodeReviewWithCostAndModel } from '@/lib/code-reviews/db/code-reviews';
import { CodeReviewAgentConfigSchema } from '@/lib/agent-config/core/types';

// ============================================================================
Expand Down Expand Up @@ -236,7 +236,7 @@ export type TriggerReviewParams = z.infer<typeof TriggerReviewParamsSchema>;
* Response type for list code reviews
*/
export type ListCodeReviewsResponse = {
reviews: CloudAgentCodeReview[];
reviews: CodeReviewWithCostAndModel[];
total: number;
hasMore: boolean;
};
57 changes: 53 additions & 4 deletions src/lib/code-reviews/db/code-reviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
*/

import { db } from '@/lib/drizzle';
import { cloud_agent_code_reviews } from '@/db/schema';
import { eq, and, desc, count, ne, inArray } from 'drizzle-orm';
import { cloud_agent_code_reviews, cliSessions, microdollar_usage } from '@/db/schema';
import { eq, and, desc, count, ne, inArray, sql, sum } from 'drizzle-orm';
import { captureException } from '@sentry/nextjs';
import type { CreateReviewParams, CodeReviewStatus, ListReviewsParams, Owner } from '../core';
import type { CloudAgentCodeReview } from '@/db/schema';

export type CodeReviewWithCostAndModel = CloudAgentCodeReview & {
total_cost_microdollars: number | null;
model: string | null;
};

/**
* Creates a new code review record
* Returns the created review ID
Expand Down Expand Up @@ -137,8 +142,11 @@ export async function updateCodeReviewStatus(
* Lists code reviews for an owner (org or user)
* Supports filtering by status and repository
* Returns reviews sorted by creation date (newest first)
* Includes cost (from microdollar_usage) and model (from cliSessions) when available
*/
export async function listCodeReviews(params: ListReviewsParams): Promise<CloudAgentCodeReview[]> {
export async function listCodeReviews(
params: ListReviewsParams
): Promise<CodeReviewWithCostAndModel[]> {
try {
const { owner, limit = 50, offset = 0, status, repoFullName, platform } = params;

Expand Down Expand Up @@ -174,9 +182,50 @@ export async function listCodeReviews(params: ListReviewsParams): Promise<CloudA
conditions.push(eq(cloud_agent_code_reviews.platform, platform));
}

// Cost subquery: sum microdollar_usage.cost where project_id matches the cli_session_id
const costSubquery = db
.select({
session_id: microdollar_usage.project_id,
total_cost: sum(microdollar_usage.cost).as('total_cost'),
})
.from(microdollar_usage)
.groupBy(microdollar_usage.project_id)
.as('cost_sq');

const reviews = await db
.select()
.select({
id: cloud_agent_code_reviews.id,
owned_by_organization_id: cloud_agent_code_reviews.owned_by_organization_id,
owned_by_user_id: cloud_agent_code_reviews.owned_by_user_id,
platform_integration_id: cloud_agent_code_reviews.platform_integration_id,
repo_full_name: cloud_agent_code_reviews.repo_full_name,
pr_number: cloud_agent_code_reviews.pr_number,
pr_url: cloud_agent_code_reviews.pr_url,
pr_title: cloud_agent_code_reviews.pr_title,
pr_author: cloud_agent_code_reviews.pr_author,
pr_author_github_id: cloud_agent_code_reviews.pr_author_github_id,
base_ref: cloud_agent_code_reviews.base_ref,
head_ref: cloud_agent_code_reviews.head_ref,
head_sha: cloud_agent_code_reviews.head_sha,
platform: cloud_agent_code_reviews.platform,
platform_project_id: cloud_agent_code_reviews.platform_project_id,
session_id: cloud_agent_code_reviews.session_id,
cli_session_id: cloud_agent_code_reviews.cli_session_id,
status: cloud_agent_code_reviews.status,
error_message: cloud_agent_code_reviews.error_message,
started_at: cloud_agent_code_reviews.started_at,
completed_at: cloud_agent_code_reviews.completed_at,
created_at: cloud_agent_code_reviews.created_at,
updated_at: cloud_agent_code_reviews.updated_at,
total_cost_microdollars: sql<number | null>`${costSubquery.total_cost}::bigint`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Potential bigint/string vs number mismatch for total_cost_microdollars

sum(microdollar_usage.cost) on Postgres often comes back as a string (and large values can exceed JS safe integer). Typing this as sql<number | null> can make the UI assume it is a number and can also hide precision/serialization pitfalls. Consider returning a string (or bigint) consistently and formatting/parsing at the edge (API/UI), or using a Drizzle mapping helper to coerce to a number safely.

model: cliSessions.last_model,
})
.from(cloud_agent_code_reviews)
.leftJoin(cliSessions, eq(cloud_agent_code_reviews.cli_session_id, cliSessions.session_id))
.leftJoin(
costSubquery,
sql`${costSubquery.session_id} = ${cloud_agent_code_reviews.cli_session_id}::text`
)
.where(and(...conditions))
.orderBy(desc(cloud_agent_code_reviews.created_at))
.limit(limit)
Expand Down
Loading