fix: provide JWT_SECRET to CI build workflows (#2607)

## Summary
- Reverts lazy JWT_SECRET initialization — keeps eager fail-fast at
module load
- Adds `JWT_SECRET` secret to both `deploy_cfwebapp_to_azure.yml` and
`nextjs-build.yaml` CI workflows so `next build` page data collection
succeeds for the `/codeflash/auth/oauth/token` route

## Context
The deploy workflow ([run
#24425211765](https://github.com/codeflash-ai/codeflash-internal/actions/runs/24425211765/job/71357530269))
was failing because `JWT_SECRET` isn't available during CI build,
causing an eager throw at module load time. The secret already exists as
a GitHub repo secret.
This commit is contained in:
Kevin Turcios 2026-04-14 19:25:41 -05:00 committed by GitHub
parent e6cec80c9d
commit e5374c3f50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 1198 additions and 635 deletions

View file

@ -13,6 +13,13 @@ jobs:
build:
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
REDIS_URL: ${{ secrets.REDIS_URL }}
AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }}
AUTH0_BASE_URL: ${{ secrets.AUTH0_BASE_URL }}
runs-on: ubuntu-latest
permissions:
contents: write

View file

@ -24,6 +24,9 @@ STRIPE_WEBHOOK_SECRET=
API_TOKEN_LIMIT=4000
JWT_SECRET=
# Redis (Azure Cache for Redis — used for rate limiting and JTI tracking)
REDIS_URL=
# Sentry (omit NEXT_PUBLIC_SENTRY_DISABLED to enable)
NEXT_PUBLIC_SENTRY_DISABLED=true
# SENTRY_AUTH_TOKEN= # set in CI for source map uploads

View file

@ -52,6 +52,7 @@
"date-fns": "^4.1.0",
"diff": "^8.0.2",
"dompurify": "^3.3.3",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^1.8.0",
"marked": "^18.0.0",
@ -83,6 +84,7 @@
},
"devDependencies": {
"@next/bundle-analyzer": "^16.2.2",
"@sentry/core": "^10.48.0",
"@testing-library/react": "^16.0.0",
"@types/dompurify": "^3.2.0",
"@types/jsonwebtoken": "^9.0.10",
@ -99,6 +101,7 @@
"prisma": "^7.7.0",
"simple-git-hooks": "^2.9.0",
"typescript": "^5.9.3",
"vite": "^8.0.8",
"vitest": "^4.1.4"
},
"optionalDependencies": {

View file

@ -4,14 +4,13 @@ import { getUserOrganizations } from "@/components/dashboard/action"
import { getUserId } from "@/app/utils/auth"
import crypto from "crypto"
import jwt from "jsonwebtoken"
import { CacheContainer } from "node-ts-cache"
import { MemoryStorage } from "node-ts-cache-storage-memory"
import { organizationMemberRepository } from "@codeflash-ai/common"
import { cookies } from "next/headers"
import { organizationMemberRepository } from "@codeflash-ai/common"
import { getRedis } from "@/lib/redis"
const RATE_LIMIT = 5
const RATE_LIMIT_WINDOW_MS = 60 * 1000
const rateLimitCache = new CacheContainer(new MemoryStorage())
const RATE_LIMIT_WINDOW_SECONDS = 60
function getJwtSecret(): string {
const secret = process.env.JWT_SECRET
if (!secret) {
@ -19,7 +18,6 @@ function getJwtSecret(): string {
}
return secret
}
const JWT_SECRET: string = getJwtSecret()
interface OAuthStatePayload {
userId: string
@ -111,22 +109,46 @@ interface OAuthParams {
vscodeState: string
}
const ALLOWED_CODE_CHALLENGE_METHODS = new Set(["S256", "sha256"])
const ALLOWED_CLIENT_IDS = new Set(["cf_vscode_app", "cf-cli-app"])
const ALLOWED_REDIRECT_URI_PATTERNS = [
/^vscode:\/\/codeflash\.codeflash\//,
/^http:\/\/localhost(:\d+)?\//, // local dev callbacks
]
function isAllowedRedirectUri(uri: string): boolean {
return ALLOWED_REDIRECT_URI_PATTERNS.some(pattern => pattern.test(uri))
}
export async function storeOAuthParams(params: OAuthParams): Promise<{ error?: string }> {
try {
if (!ALLOWED_CODE_CHALLENGE_METHODS.has(params.codeChallengeMethod)) {
return { error: "Invalid code challenge method" }
}
if (!ALLOWED_CLIENT_IDS.has(params.clientId)) {
return { error: "Invalid client application" }
}
if (!isAllowedRedirectUri(params.redirectUri)) {
return { error: "Invalid redirect URI" }
}
const userId = await getUserId()
if (!userId) {
return { error: "Unauthorized" }
}
const signed = jwt.sign({ ...params, type: "oauth_params" }, JWT_SECRET, {
const signed = jwt.sign({ ...params, type: "oauth_params" }, getJwtSecret(), {
expiresIn: "10m",
algorithm: "HS256",
})
const cookieStore = await cookies()
cookieStore.set(OAUTH_COOKIE_NAME, signed, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
secure: process.env.NODE_ENV !== "development",
path: "/codeflash/auth",
maxAge: 600,
})
@ -148,7 +170,9 @@ export async function getStoredOAuthParams(): Promise<{
return { error: "Session expired. Please refresh the page and try again." }
}
const payload = jwt.verify(cookie.value, JWT_SECRET) as unknown as OAuthParams & {
const payload = jwt.verify(cookie.value, getJwtSecret(), {
algorithms: ["HS256"],
}) as unknown as OAuthParams & {
type: string
}
if (payload.type !== "oauth_params") {
@ -175,29 +199,21 @@ async function clearOAuthCookie() {
}
export async function isRateLimited(userId: string): Promise<boolean> {
const cacheKey = `rate_limit_vsc_signin_${userId}`
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
const now = Date.now()
const redis = getRedis()
const key = `rate_limit:vsc_signin:${userId}`
const pipeline = redis.pipeline()
pipeline.incr(key)
pipeline.expire(key, RATE_LIMIT_WINDOW_SECONDS)
const results = await pipeline.exec()
const count = (results?.[0]?.[1] as number) ?? 1
return count > RATE_LIMIT
}
if (!record || now - record.startTime > RATE_LIMIT_WINDOW_MS) {
await rateLimitCache.setItem(
cacheKey,
{ count: 1, startTime: now },
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 },
)
return false
}
if (record.count >= RATE_LIMIT) {
return true
}
record.count++
await rateLimitCache.setItem(cacheKey, record, {
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
})
return false
async function markJtiUsed(jti: string, ttlSeconds: number): Promise<boolean> {
const redis = getRedis()
const key = `jti:${jti}`
const wasSet = await redis.set(key, "1", "EX", ttlSeconds, "NX")
return wasSet === "OK"
}
export async function createOAuthState(params: {
@ -234,8 +250,9 @@ export async function createOAuthState(params: {
type: "oauth_state",
}
const state = jwt.sign(statePayload, JWT_SECRET, {
const state = jwt.sign(statePayload, getJwtSecret(), {
expiresIn: "2m",
algorithm: "HS256",
jwtid: crypto.randomBytes(16).toString("hex"),
})
@ -258,7 +275,9 @@ export async function authorizeOAuth(state: string): Promise<{
let oauthState: OAuthStatePayload
try {
oauthState = jwt.verify(state, JWT_SECRET) as unknown as OAuthStatePayload
oauthState = jwt.verify(state, getJwtSecret(), {
algorithms: ["HS256"],
}) as unknown as OAuthStatePayload
} catch {
return { error: "Invalid or expired state" }
}
@ -281,8 +300,9 @@ export async function authorizeOAuth(state: string): Promise<{
type: "auth_code",
}
const code = jwt.sign(authCodePayload, JWT_SECRET, {
const code = jwt.sign(authCodePayload, getJwtSecret(), {
expiresIn: "2m",
algorithm: "HS256",
jwtid: crypto.randomBytes(16).toString("hex"),
})
@ -310,7 +330,9 @@ export async function exchangeCodeForToken(
try {
let codeData: AuthCodePayload
try {
codeData = jwt.verify(params.code, JWT_SECRET) as unknown as AuthCodePayload
codeData = jwt.verify(params.code, getJwtSecret(), {
algorithms: ["HS256"],
}) as unknown as AuthCodePayload
} catch {
return { error: "Invalid or expired authorization code" }
}
@ -319,6 +341,12 @@ export async function exchangeCodeForToken(
return { error: "Invalid authorization code" }
}
// Prevent auth code replay — each jti can only be used once
const jti = (codeData as unknown as { jti?: string }).jti
if (!jti || !(await markJtiUsed(jti, 120))) {
return { error: "Authorization code has already been used" }
}
if (codeData.clientId !== params.clientId) {
return { error: "Client ID mismatch" }
}
@ -327,11 +355,13 @@ export async function exchangeCodeForToken(
return { error: "Redirect URI mismatch" }
}
if (!ALLOWED_CODE_CHALLENGE_METHODS.has(codeData.codeChallengeMethod)) {
return { error: "Unsupported code challenge method" }
}
const computedChallenge = crypto
.createHash(codeData.codeChallengeMethod)
.createHash("sha256")
.update(params.codeVerifier)
.digest("base64url")
if (computedChallenge !== codeData.codeChallenge) {
return { error: "Code verifier validation failed" }
}

View file

@ -90,6 +90,11 @@ export default function CodeFlashAuthContent() {
return
}
if (codeChallengeMethod !== "S256" && codeChallengeMethod !== "sha256") {
setError("Invalid code challenge method")
return
}
if (!state) {
setError("Missing request identifier")
return

View file

@ -1,44 +1,27 @@
import { NextRequest, NextResponse } from "next/server"
import { exchangeCodeForToken } from "../../action"
export async function POST(request: NextRequest) {
console.log("=== Token Exchange Request Started ===")
const ALLOWED_CLIENT_IDS = new Set(["cf_vscode_app", "cf-cli-app"])
export async function POST(request: NextRequest) {
try {
const body = await request.json()
console.log("Request body:", {
grant_type: body.grant_type,
client_id: body.client_id,
redirect_uri: body.redirect_uri,
has_code: !!body.code,
has_code_verifier: !!body.code_verifier,
code_length: body.code?.length,
code_verifier_length: body.code_verifier?.length,
})
const { grant_type, code, redirect_uri, code_verifier, client_id } = body
// Validate grant type
if (grant_type !== "authorization_code") {
console.error("Invalid grant type:", grant_type)
return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 })
}
// Validate required parameters
if (!code || !redirect_uri || !code_verifier || !client_id) {
console.error("Missing required parameters:", {
has_code: !!code,
has_redirect_uri: !!redirect_uri,
has_code_verifier: !!code_verifier,
has_client_id: !!client_id,
})
return NextResponse.json(
{ error: "invalid_request", error_description: "Missing required parameters" },
{ status: 400 },
)
}
console.log("Exchanging code for token...")
if (!ALLOWED_CLIENT_IDS.has(client_id)) {
return NextResponse.json({ error: "invalid_client" }, { status: 401 })
}
const result = await exchangeCodeForToken({
code,
@ -48,26 +31,17 @@ export async function POST(request: NextRequest) {
})
if (result.error) {
console.error("Token exchange failed:", result.error)
return NextResponse.json(
{ error: "invalid_grant", error_description: result.error },
{ status: 400 },
)
}
console.log("=== Token Exchange Request Completed Successfully ===")
return NextResponse.json({
access_token: result.accessToken,
token_type: "Bearer",
})
} catch (error) {
console.error("=== Token Exchange Request Failed ===")
console.error("Error type:", error instanceof Error ? error.constructor.name : typeof error)
console.error("Error message:", error instanceof Error ? error.message : String(error))
console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
console.error("Full error object:", error)
} catch {
return NextResponse.json(
{ error: "server_error", error_description: "Internal server error" },
{ status: 500 },

View file

@ -11,7 +11,7 @@ import { auth0 } from "@/lib/auth0"
export async function upsertReferralSource(
referralSource: string,
additionalComments?: string,
): Promise<any> {
): Promise<void> {
const session = await auth0.getSession()
if (session != null) {
setUserReferralData(session.user.sub, referralSource, additionalComments)

View file

@ -136,7 +136,7 @@ export function CreateApiKeyDialog(): React.JSX.Element {
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
ownerType === "personal"
? "border-primary bg-primary/10 text-primary"
: "border-input hover:bg-accent hover:text-accent-foreground"
: "border-input hover:bg-accent hover:text-accent-foreground",
)}
>
<User className="h-4 w-4" />
@ -151,7 +151,7 @@ export function CreateApiKeyDialog(): React.JSX.Element {
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
ownerType === "organization"
? "border-primary bg-primary/10 text-primary"
: "border-input hover:bg-accent hover:text-accent-foreground"
: "border-input hover:bg-accent hover:text-accent-foreground",
)}
>
<Building2 className="h-4 w-4" />

View file

@ -56,9 +56,9 @@ export async function reactivateSubscription(userId: string) {
try {
await reactivateSubscriptionFromCommon(userId)
return { success: true }
} catch (error: any) {
} catch (error: unknown) {
console.error("Error reactivating subscription:", error)
Sentry.captureException(error)
return { success: false, error: error.message }
return { success: false, error: error instanceof Error ? error.message : "Unknown error" }
}
}

View file

@ -3,7 +3,7 @@ import { prisma } from "@codeflash-ai/common"
import { getActionAccountContext } from "@/lib/server/get-account-context"
vi.mock("@/lib/server-action-timing", () => ({
withTiming: vi.fn((_name: string, fn: Function) => fn),
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
}))
vi.mock("@/lib/analytics/tracking", () => ({
@ -59,8 +59,12 @@ describe("getOrganizationMembers", () => {
describe("successful retrieval", () => {
it("returns members when user has access", async () => {
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
)
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({
id: "member-1",
} as unknown as Awaited<ReturnType<typeof prisma.organization_members.findUnique>>)
const result = await getOrganizationMembers("org-1")
@ -69,8 +73,12 @@ describe("getOrganizationMembers", () => {
})
it("maps nested organization_members to flat Member structure", async () => {
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
)
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({
id: "member-1",
} as unknown as Awaited<ReturnType<typeof prisma.organization_members.findUnique>>)
const result = await getOrganizationMembers("org-1")
const member = result.data![0]
@ -115,7 +123,9 @@ describe("getOrganizationMembers", () => {
userId: "unknown-user",
username: "testuser",
})
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
)
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue(null)
const result = await getOrganizationMembers("org-1")

View file

@ -15,7 +15,7 @@ export default async function OrganizationMembersPage() {
<MembersClient
initialUserId={initData.userId}
initialOrgId={initData.orgId}
initialMembers={initData.members as any}
initialMembers={initData.members}
initialUserRole={initData.currentUserRole}
/>
</DashboardErrorBoundary>

View file

@ -1,11 +1,12 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { prisma } from "@codeflash-ai/common"
import type { AccountPayload } from "@codeflash-ai/common"
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
import { trackRepositoryConnected } from "@/lib/analytics/tracking"
import { getActionAccountContext } from "@/lib/server/get-account-context"
vi.mock("@/lib/server-action-timing", () => ({
withTiming: vi.fn((_name: string, fn: Function) => fn),
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
}))
vi.mock("@/lib/services/repository-utils", () => ({
@ -50,14 +51,16 @@ describe("getRepositoryById", () => {
describe("parallel fetch", () => {
it("fetches repo and authorized repoIds concurrently", async () => {
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
)
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["repo-1"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
vi.mocked(prisma.optimization_events.count).mockResolvedValue(5)
await getRepositoryById(mockPayload as any, "repo-1")
await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(prisma.repositories.findUnique).toHaveBeenCalledTimes(1)
expect(getRepositoriesForAccountCached).toHaveBeenCalledWith(mockPayload)
@ -68,37 +71,41 @@ describe("getRepositoryById", () => {
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["repo-1"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result).toBeNull()
})
it("returns null when repo is not in authorized list", async () => {
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
)
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["other-repo"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result).toBeNull()
})
})
describe("successful retrieval", () => {
beforeEach(() => {
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
)
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["repo-1"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
})
it("returns RepositoryWithUsage with all required fields", async () => {
vi.mocked(prisma.optimization_events.count).mockResolvedValue(3)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result).toEqual({
id: "repo-1",
@ -121,30 +128,32 @@ describe("getRepositoryById", () => {
it("sets is_active to false when no recent events", async () => {
vi.mocked(prisma.optimization_events.count).mockResolvedValue(0)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result!.is_active).toBe(false)
})
it("sets is_active to true when recent events exist", async () => {
vi.mocked(prisma.optimization_events.count).mockResolvedValue(10)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result!.is_active).toBe(true)
})
})
describe("analytics tracking", () => {
beforeEach(() => {
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
)
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["repo-1"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
vi.mocked(prisma.optimization_events.count).mockResolvedValue(1)
})
it("calls trackRepositoryConnected for user payloads", async () => {
await getRepositoryById(mockPayload as any, "repo-1")
await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(trackRepositoryConnected).toHaveBeenCalledWith("user-1", {
repositoryId: "repo-1",
@ -160,9 +169,9 @@ describe("getRepositoryById", () => {
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: ["repo-1"],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
const result = await getRepositoryById(mockPayload as any, "repo-1")
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
expect(result).toBeNull()
})
})
@ -178,14 +187,16 @@ describe("getRepositoryMembers", () => {
userId: "user-1",
username: "testuser",
})
;(prisma.repository_members as any).findMany = vi.fn()
;(prisma.repository_members as unknown as Record<string, unknown>).findMany = vi.fn()
const mod = await import("../action")
getRepositoryMembers = mod.getRepositoryMembers
})
it("uses the authenticated session user for the access check", async () => {
vi.mocked(prisma.repository_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
vi.mocked(prisma.repository_members.findUnique).mockResolvedValue({
id: "member-1",
} as unknown as Awaited<ReturnType<typeof prisma.repository_members.findUnique>>)
vi.mocked(prisma.repository_members.findMany).mockResolvedValue([
{
id: "member-1",
@ -194,7 +205,7 @@ describe("getRepositoryMembers", () => {
added_at: new Date("2024-01-01"),
user: { github_username: "alice" },
},
] as any)
] as unknown as Awaited<ReturnType<typeof prisma.repository_members.findMany>>)
const result = await getRepositoryMembers("repo-1")

View file

@ -40,7 +40,7 @@ export default async function RepositoryDetailPage({
repositoryId={repositoryId}
initialUserId={initData.userId}
initialOrgId={initData.orgId ?? null}
initialRepository={initData.repository as any}
initialRepository={initData.repository}
initialStats={initData.stats}
/>
</DashboardErrorBoundary>

View file

@ -5,8 +5,10 @@ import { ActionResponse, createErrorResponse, createSuccessResponse } from "@/li
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
import { auth0 } from "@/lib/auth0"
import { AccountPayload, buildOptimizationOrCondition, prisma } from "@codeflash-ai/common"
import { Prisma } from "@prisma/client"
import * as Sentry from "@sentry/nextjs"
import { trackOptimizationReviewed } from "@/lib/analytics/tracking"
import { PrCommentFields } from "@/lib/types"
import { getActionAccountContext } from "@/lib/server/get-account-context"
export interface DiffContent {
@ -35,10 +37,10 @@ export interface GetStagingCodeParams {
async function findAuthorizedOptimizationEvent(
payload: AccountPayload,
identifiers: { id?: string; trace_id?: string },
queryOptions: Record<string, unknown> = {},
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
) {
const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds
const where = {
const where: Prisma.optimization_eventsWhereInput = {
...identifiers,
...buildOptimizationOrCondition(payload, repoIds),
}
@ -46,14 +48,17 @@ async function findAuthorizedOptimizationEvent(
return prisma.optimization_events.findFirst({
where,
...queryOptions,
} as any)
})
}
async function getAuthorizedActionContext() {
return getActionAccountContext()
}
async function getAuthorizedEventById(eventId: string, queryOptions: Record<string, unknown> = {}) {
async function getAuthorizedEventById(
eventId: string,
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
) {
const accountContext = await getAuthorizedActionContext()
if (!accountContext) {
return null
@ -73,7 +78,7 @@ async function getAuthorizedEventById(eventId: string, queryOptions: Record<stri
async function getAuthorizedEventByTraceId(
traceId: string,
queryOptions: Record<string, unknown> = {},
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
) {
const accountContext = await getAuthorizedActionContext()
if (!accountContext) {
@ -137,10 +142,12 @@ async function getStagingCodeFromApi(
const data = await response.json()
return createSuccessResponse(data as StagingCodeResponse)
} catch (error: any) {
} catch (error: unknown) {
console.error("[getStagingCodeFromApi] Error:", error)
Sentry.captureException(error)
return createErrorResponse(error?.message || "Failed to fetch staging code")
return createErrorResponse(
error instanceof Error ? error.message : "Failed to fetch staging code",
)
}
}
@ -211,10 +218,10 @@ export async function commitStagingCode(
const data = await response.json()
return createSuccessResponse(data as CommitStagingCodeResponse)
} catch (error: any) {
} catch (error: unknown) {
console.error("[commitStagingCode] Error:", error)
Sentry.captureException(error)
return createErrorResponse(error?.message || "Failed to commit changes")
return createErrorResponse(error instanceof Error ? error.message : "Failed to commit changes")
}
}
@ -226,7 +233,7 @@ async function getOptimizationEventById({
trace_id: string
}) {
const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds
const where: any = {
const where: Prisma.optimization_eventsWhereInput = {
trace_id,
...buildOptimizationOrCondition(payload, repoIds),
}
@ -298,20 +305,21 @@ export async function saveOptimizationChanges({
try {
// Get the current metadata
const currentMetadata = (authorizedEvent.event.metadata as any) || {}
const currentDiffContents = currentMetadata.diffContents || {}
const currentMetadata = (authorizedEvent.event.metadata as Prisma.JsonObject | null) ?? {}
const currentDiffContents =
(currentMetadata.diffContents as Record<string, Prisma.JsonObject> | undefined) ?? {}
// Update only the specific file's content
const updatedDiffContents = {
const updatedDiffContents: Record<string, Prisma.JsonObject> = {
...currentDiffContents,
[filePath]: {
...currentDiffContents[filePath],
...(currentDiffContents[filePath] ?? {}),
newContent: newContent,
},
}
// Update the metadata with new diff contents
const updatedMetadata = {
const updatedMetadata: Prisma.JsonObject = {
...currentMetadata,
diffContents: updatedDiffContents,
lastModified: new Date().toISOString(),
@ -355,7 +363,7 @@ export async function createPullRequest({
}: {
traceId: string
diffContents: Record<string, { oldContent: string; newContent: string }>
prCommentFields?: any
prCommentFields?: PrCommentFields
generatedTests?: string
existingTests?: string
functionName?: string
@ -490,8 +498,9 @@ export async function createPullRequest({
})
return createErrorResponse(errorMessage)
} catch (error: any) {
const errorMessage = error?.message || "Something went wrong. Please try again."
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Something went wrong. Please try again."
Sentry.captureException(error)
return createErrorResponse(errorMessage)
}
@ -620,9 +629,9 @@ export async function getReviewPageInitData(traceId: string) {
}
// If git_branch storage, fetch staging code + comments in parallel
const metadata = (event.metadata as any) || {}
const metadata = (event.metadata as Record<string, unknown> | null) ?? {}
if (event.staging_storage_type === "git_branch") {
const stagingBranchName = metadata.staging_branch_name
const stagingBranchName = metadata.staging_branch_name as string | undefined
const repository = event.repository
if (stagingBranchName && repository?.full_name && repository?.installation_id) {

View file

@ -1,6 +1,6 @@
import { notFound } from "next/navigation"
import { getReviewPageInitData } from "./action"
import { OptimizationReviewClient } from "./review-client"
import { OptimizationReviewClient, type ReviewClientProps } from "./review-client"
interface ReviewPageProps {
params: Promise<{ traceId: string }>
@ -24,9 +24,9 @@ export default async function OptimizationReviewPage({ params }: ReviewPageProps
return (
<OptimizationReviewClient
traceId={traceId}
initialEvent={initData.event as any}
initialComments={initData.comments as any}
initialStagingCode={initData.stagingCode as any}
initialEvent={initData.event as ReviewClientProps["initialEvent"]}
initialComments={initData.comments as ReviewClientProps["initialComments"]}
initialStagingCode={initData.stagingCode as ReviewClientProps["initialStagingCode"]}
/>
)
}

View file

@ -19,7 +19,8 @@ export default async function LineProfilerPage({ params }: ProfilerPageProps) {
notFound()
}
const metadata = (initData.event.metadata as any) || {}
const metadata = (initData.event.metadata as Record<string, unknown> | null) ?? {}
const prCommentFields = (metadata.prCommentFields as Record<string, unknown> | undefined) ?? {}
return (
<ProfilerClient
@ -27,10 +28,10 @@ export default async function LineProfilerPage({ params }: ProfilerPageProps) {
functionName={initData.event.function_name ?? null}
filePath={initData.event.file_path ?? null}
speedupX={initData.event.speedup_x ?? null}
originalLineProfiler={metadata.originalLineProfiler}
optimizedLineProfiler={metadata.optimizedLineProfiler}
originalRuntime={metadata.prCommentFields?.original_runtime}
bestRuntime={metadata.prCommentFields?.best_runtime}
originalLineProfiler={metadata.originalLineProfiler as string | undefined}
optimizedLineProfiler={metadata.optimizedLineProfiler as string | undefined}
originalRuntime={prCommentFields.original_runtime as string | undefined}
bestRuntime={prCommentFields.best_runtime as string | undefined}
/>
)
}

View file

@ -1,9 +1,12 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { prisma } from "@codeflash-ai/common"
import type { AccountPayload } from "@codeflash-ai/common"
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
const prismaWithRaw = prisma as unknown as Record<string, ReturnType<typeof vi.fn>>
vi.mock("@/lib/server-action-timing", () => ({
withTiming: vi.fn((_name: string, fn: Function) => fn),
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
}))
vi.mock("@/lib/services/repository-utils", () => ({
@ -49,7 +52,7 @@ const mockFeatures = [
]
/** Helper: extract SQL pattern from a $queryRaw tagged template mock call */
function getTaggedSql(mockFn: any, callIndex: number): string {
function getTaggedSql(mockFn: ReturnType<typeof vi.fn>, callIndex: number): string {
const args = mockFn.mock.calls[callIndex]
const strings = args[0] as string[]
return strings.join("$?")
@ -62,10 +65,10 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: mockRepoIds,
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
// $queryRaw is used as a tagged template literal — auto-mock doesn't create it
;(prisma as any).$queryRaw = vi.fn()
prismaWithRaw.$queryRaw = vi.fn()
const mod = await import("../action")
getAllOptimizationEvents = mod.getAllOptimizationEvents
@ -74,22 +77,30 @@ describe("getAllOptimizationEvents", () => {
describe("Path B: standard Prisma query (org account)", () => {
// Org accounts use Prisma findMany/count (not raw SQL) when not sorting by review_quality
it("calls findMany and count in parallel", async () => {
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
)
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
expect(prisma.optimization_events.findMany).toHaveBeenCalledTimes(1)
expect(prisma.optimization_events.count).toHaveBeenCalledTimes(1)
})
it("batch-fetches optimization_features by trace_id array (not N+1)", async () => {
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
)
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(
mockFeatures as unknown as Awaited<
ReturnType<typeof prisma.optimization_features.findMany>
>,
)
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
// Single batch query with all trace IDs — NOT one per event
expect(prisma.optimization_features.findMany).toHaveBeenCalledTimes(1)
@ -104,15 +115,23 @@ describe("getAllOptimizationEvents", () => {
})
it("merges review_quality into events", async () => {
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
)
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(
mockFeatures as unknown as Awaited<
ReturnType<typeof prisma.optimization_features.findMany>
>,
)
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as any })
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
expect((result.events[0] as any).review_quality).toBe("high")
expect((result.events[0] as any).review_explanation).toBe("Great optimization")
expect((result.events[1] as any).review_quality).toBeNull()
const event0 = result.events[0] as Record<string, unknown>
const event1 = result.events[1] as Record<string, unknown>
expect(event0.review_quality).toBe("high")
expect(event0.review_explanation).toBe("Great optimization")
expect(event1.review_quality).toBeNull()
})
it("returns totalCount from count query", async () => {
@ -120,7 +139,7 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(prisma.optimization_events.count).mockResolvedValue(42)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as any })
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
expect(result.totalCount).toBe(42)
})
@ -130,7 +149,7 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
page: 3,
pageSize: 25,
})
@ -148,7 +167,7 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(prisma.optimization_events.count).mockResolvedValue(0)
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
expect(prisma.optimization_events.findMany).toHaveBeenCalledWith(
expect.objectContaining({
@ -163,17 +182,21 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
search: "calc",
})
// Check findMany was called with a search-containing where clause
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as Record<
string,
Record<string, unknown>
>
// Should have AND with OR containing the search fields
expect(call.where.AND).toBeDefined()
const orClause = call.where.AND.find((c: any) => c.OR)
const andClauses = call.where.AND as Array<Record<string, unknown>>
const orClause = andClauses.find(c => c.OR)
expect(orClause).toBeDefined()
expect(orClause.OR).toHaveLength(3) // function_name, file_path, repository.full_name
expect(orClause!.OR).toHaveLength(3) // function_name, file_path, repository.full_name
})
it("applies repository_id filter", async () => {
@ -182,54 +205,58 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
filter: { repository_id: mockRepoIds[0] },
})
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as Record<
string,
Record<string, unknown>
>
// The repository_id filter should be in the AND clause
const repoFilter = call.where.AND.find((c: any) => c.repository_id !== undefined)
const andClauses = call.where.AND as Array<Record<string, unknown>>
const repoFilter = andClauses.find(c => c.repository_id !== undefined)
expect(repoFilter).toBeDefined()
expect(repoFilter.repository_id).toBe(mockRepoIds[0])
expect(repoFilter!.repository_id).toBe(mockRepoIds[0])
})
})
describe("Path A: raw SQL query (review_quality sort/filter)", () => {
it("triggers when sort includes review_quality", async () => {
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce([]) // events
.mockResolvedValueOnce([{ count: BigInt(0) }]) // count
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
sort: { review_quality: "desc" },
})
expect((prisma as any).$queryRaw).toHaveBeenCalledTimes(2)
expect(prismaWithRaw.$queryRaw).toHaveBeenCalledTimes(2)
// Should NOT use standard Prisma findMany
expect(prisma.optimization_events.findMany).not.toHaveBeenCalled()
})
it("triggers when filter includes review_quality", async () => {
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce([])
.mockResolvedValueOnce([{ count: BigInt(0) }])
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
filter: { review_quality: "high" },
})
expect((prisma as any).$queryRaw).toHaveBeenCalledTimes(2)
expect(prismaWithRaw.$queryRaw).toHaveBeenCalledTimes(2)
})
it("returns correct totalCount from BigInt conversion", async () => {
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce([])
.mockResolvedValueOnce([{ count: BigInt(99) }])
const result = await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
sort: { review_quality: "asc" },
})
@ -248,16 +275,16 @@ describe("getAllOptimizationEvents", () => {
repo_id: mockRepoIds[0],
},
]
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce(rawEvents)
.mockResolvedValueOnce([{ count: BigInt(1) }])
const result = await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
sort: { review_quality: "desc" },
})
expect((result.events[0] as any).repository).toEqual({
expect((result.events[0] as Record<string, unknown>).repository).toEqual({
id: mockRepoIds[0],
full_name: "org/repo",
name: "repo",
@ -276,30 +303,30 @@ describe("getAllOptimizationEvents", () => {
repo_id: null,
},
]
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce(rawEvents)
.mockResolvedValueOnce([{ count: BigInt(1) }])
const result = await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
sort: { review_quality: "desc" },
})
expect((result.events[0] as any).repository).toBeNull()
expect((result.events[0] as Record<string, unknown>).repository).toBeNull()
})
it("includes LEFT JOIN in raw SQL queries", async () => {
;(prisma as any).$queryRaw
prismaWithRaw.$queryRaw
.mockResolvedValueOnce([])
.mockResolvedValueOnce([{ count: BigInt(0) }])
await getAllOptimizationEvents({
payload: mockOrgPayload as any,
payload: mockOrgPayload as AccountPayload,
sort: { review_quality: "desc" },
})
// $queryRaw is a tagged template — first arg is TemplateStringsArray
const sql = getTaggedSql((prisma as any).$queryRaw, 0)
const sql = getTaggedSql(prismaWithRaw.$queryRaw, 0)
expect(sql).toContain("LEFT JOIN optimization_features")
expect(sql).toContain("LEFT JOIN repositories")
})
@ -310,9 +337,11 @@ describe("getAllOptimizationEvents", () => {
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
repoIds: [],
repos: [],
} as any)
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
const result = await getAllOptimizationEvents({ payload: mockPersonalPayload as any })
const result = await getAllOptimizationEvents({
payload: mockPersonalPayload as AccountPayload,
})
expect(result.events).toEqual([])
expect(result.totalCount).toBe(0)
})

View file

@ -305,7 +305,6 @@ export function OptimizationsTable({
}
}
// Flatten filter properties as deps to avoid object-reference churn
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
filters.page,
filters.search,

View file

@ -5,6 +5,18 @@ import { withTiming } from "@/lib/server-action-timing"
import { AccountPayload, prisma } from "@codeflash-ai/common"
import { Prisma } from "@prisma/client"
/** Row shape returned by raw SQL joins on optimization_events + optimization_features + repositories */
interface RawOptimizationEventRow extends Record<string, unknown> {
repo_id?: string
repo_full_name?: string
repo_name?: string
review_quality?: string | null
review_explanation?: string | null
}
/** Filter values accepted for optimization event queries */
type FilterValue = string | null | { not: null } | undefined
// Cached implementation for getRepositoriesWithStagingEvents
// React cache() ensures this is only executed once per unique payload within a single request
const getRepositoriesWithStagingEventsImpl = cache(
@ -91,7 +103,7 @@ const getAllOptimizationEventsImpl = async ({
}: {
payload: AccountPayload
search?: string
filter?: Record<string, any>
filter?: Record<string, FilterValue>
sort?: { [key: string]: "asc" | "desc" }
page?: number
pageSize?: number
@ -144,12 +156,18 @@ const getAllOptimizationEventsImpl = async ({
whereFragments.push(Prisma.sql`of.review_quality = ${filter.review_quality}`)
}
if (filter.repository_id !== undefined) {
if (filter.repository_id === null) {
const repoFilter = filter.repository_id
if (repoFilter === null) {
whereFragments.push(Prisma.sql`oe.repository_id IS NULL`)
} else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
} else if (
typeof repoFilter === "object" &&
repoFilter !== null &&
"not" in repoFilter &&
repoFilter.not === null
) {
whereFragments.push(Prisma.sql`oe.repository_id IS NOT NULL`)
} else if (typeof filter.repository_id === "string") {
whereFragments.push(Prisma.sql`oe.repository_id = ${filter.repository_id}`)
} else if (typeof repoFilter === "string") {
whereFragments.push(Prisma.sql`oe.repository_id = ${repoFilter}`)
}
}
}
@ -180,7 +198,7 @@ const getAllOptimizationEventsImpl = async ({
const paginationLimit = pageSize
const paginationOffset = (page - 1) * pageSize
const [events, countResult] = await Promise.all([
prisma.$queryRaw<any[]>`
prisma.$queryRaw<RawOptimizationEventRow[]>`
SELECT
oe.*,
of.review_quality,
@ -205,20 +223,12 @@ const getAllOptimizationEventsImpl = async ({
])
const totalCount = Number(countResult[0].count)
// Repository data is already included from the JOIN
const eventsWithRepo = events.map(
(
event: Record<string, unknown> & {
repo_id?: string
repo_full_name?: string
repo_name?: string
},
) => ({
...event,
repository: event.repo_id
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
: null,
}),
)
const eventsWithRepo = events.map((event: RawOptimizationEventRow) => ({
...event,
repository: event.repo_id
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
: null,
}))
return { events: eventsWithRepo, totalCount }
} else {
// Standard Prisma query with native orderBy (optimized with UNION for personal accounts)
@ -229,10 +239,12 @@ const getAllOptimizationEventsImpl = async ({
if ("orgId" in payload) {
// Organization account: simple IN clause
const where = {
const where: Prisma.optimization_eventsWhereInput & {
AND?: Prisma.optimization_eventsWhereInput[]
} = {
is_staging: true,
repository_id: { in: repoIds },
} as any
}
if (search) {
where.AND = where.AND || []
@ -266,9 +278,9 @@ const getAllOptimizationEventsImpl = async ({
Object.keys(filter).forEach(key => {
if (key === "repository_id") {
where.AND = where.AND || []
where.AND.push({ [key]: filter[key] })
where.AND.push({ [key]: filter[key] } as Prisma.optimization_eventsWhereInput)
} else if (key !== "review_quality") {
where[key] = filter[key]
;(where as Record<string, FilterValue>)[key] = filter[key]
}
})
}
@ -305,7 +317,12 @@ const getAllOptimizationEventsImpl = async ({
} else if (key === "repository_id") {
if (value === null) {
filterFragments.push(Prisma.sql`AND oe.repository_id IS NULL`)
} else if (value?.not === null) {
} else if (
typeof value === "object" &&
value !== null &&
"not" in value &&
value.not === null
) {
filterFragments.push(Prisma.sql`AND oe.repository_id IS NOT NULL`)
} else if (typeof value === "string") {
filterFragments.push(Prisma.sql`AND oe.repository_id = ${value}`)
@ -339,7 +356,7 @@ const getAllOptimizationEventsImpl = async ({
`
const [eventsResult, countResult] = await Promise.all([
prisma.$queryRaw<any[]>`
prisma.$queryRaw<RawOptimizationEventRow[]>`
WITH base_events AS (
SELECT oe.*, r.id as repo_id, r.full_name as repo_full_name, r.name as repo_name
FROM optimization_events oe
@ -363,20 +380,12 @@ const getAllOptimizationEventsImpl = async ({
])
totalCount = Number(countResult[0].count)
events = eventsResult.map(
(
event: Record<string, unknown> & {
repo_id?: string
repo_full_name?: string
repo_name?: string
},
) => ({
...event,
repository: event.repo_id
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
: null,
}),
)
events = eventsResult.map((event: RawOptimizationEventRow) => ({
...event,
repository: event.repo_id
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
: null,
}))
}
// Batch-fetch review data for all events in a single query

View file

@ -11,17 +11,39 @@ export default async function ReviewOptimizationsPage() {
getCachedRepositories(accountKey, accountPayload),
])
const initialEvents = (initialData?.events || []).map((event: any) => ({
...event,
created_at:
event.created_at instanceof Date ? event.created_at.toISOString() : event.created_at,
repository: event.repository
? {
id: event.repository.id,
full_name: event.repository.full_name || event.repository.name,
}
: null,
}))
interface OptimizationEventSerialized {
id: string
function_name?: string
file_path?: string
repository?: { id: string; full_name?: string } | null
speedup_x?: number
speedup_pct?: number
metadata?: Record<string, unknown> | null
created_at: string
status?: string
event_type?: string
trace_id: string
review_quality: string
}
const initialEvents: OptimizationEventSerialized[] = (initialData?.events || []).map(
(
event: Record<string, unknown> & {
repository?: { id: string; full_name?: string; name?: string } | null
},
) =>
({
...event,
created_at:
event.created_at instanceof Date ? event.created_at.toISOString() : event.created_at,
repository: event.repository
? {
id: event.repository.id,
full_name: event.repository.full_name || event.repository.name,
}
: null,
}) as OptimizationEventSerialized,
)
return (
<OptimizationsTable

View file

@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
import { Prisma } from "@prisma/client"
import { prisma } from "@/lib/prisma"
export async function POST(request: NextRequest, props: { params: Promise<{ trace_id: string }> }) {
@ -31,34 +32,39 @@ export async function POST(request: NextRequest, props: { params: Promise<{ trac
}
// Get existing metadata and experiment_metadata
const existingMetadata = (optimizationFeature.metadata as any) || {}
const experimentMetadata = (optimizationFeature.experiment_metadata as any) || {}
const existingMetadata = (optimizationFeature.metadata as Prisma.JsonObject | null) ?? {}
const experimentMetadata =
(optimizationFeature.experiment_metadata as Prisma.JsonObject | null) ?? {}
// Check if diffContents exists and has the fileKey
if (!experimentMetadata.diffContents || !experimentMetadata.diffContents[fileKey]) {
const diffContents = experimentMetadata.diffContents as
| Record<string, Prisma.JsonObject>
| undefined
if (!diffContents || !diffContents[fileKey]) {
return NextResponse.json({ error: "File not found in experiment metadata" }, { status: 404 })
}
// Backup original code if not already backed up
const originalCode = existingMetadata.originalCode || {}
const originalCode: Prisma.JsonObject =
(existingMetadata.originalCode as Prisma.JsonObject | undefined) ?? {}
if (!originalCode[fileKey]) {
originalCode[fileKey] = experimentMetadata.diffContents[fileKey].newContent
originalCode[fileKey] = diffContents[fileKey].newContent
}
// Update experiment_metadata with modified code
const updatedExperimentMetadata = {
const updatedExperimentMetadata: Prisma.JsonObject = {
...experimentMetadata,
diffContents: {
...experimentMetadata.diffContents,
...diffContents,
[fileKey]: {
...experimentMetadata.diffContents[fileKey],
...diffContents[fileKey],
newContent: modifiedCode,
},
},
}
// Update metadata with backup and tracking info
const updatedMetadata = {
const updatedMetadata: Prisma.JsonObject = {
...existingMetadata,
originalCode,
lastModified: new Date().toISOString(),

View file

@ -1,12 +1,7 @@
"use client"
import { useState, useEffect, useMemo, memo } from "react"
import {
FileText,
Code,
GitCompare,
Columns2,
} from "lucide-react"
import { FileText, Code, GitCompare, Columns2 } from "lucide-react"
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
import { parseAllCodeBlocks, findMatchingFile } from "./timeline-helpers"
import { DiffView, SideBySideDiffView } from "./diff-views"
@ -26,18 +21,17 @@ export const CandidateContent = memo(function CandidateContent({
const [unifiedDiff, setUnifiedDiff] = useState<string | null>(null)
const [diffLoading, setDiffLoading] = useState(false)
const originalCode =
content.type === "refinement" ? content.parentCode : content.originalCode
const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode
const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code])
const originalFiles = useMemo(
() => (originalCode ? parseAllCodeBlocks(originalCode) : []),
[originalCode]
[originalCode],
)
const selectedCandidateFile = useMemo(
() => candidateFiles[selectedFileIndex] || candidateFiles[0],
[candidateFiles, selectedFileIndex]
[candidateFiles, selectedFileIndex],
)
const matchingOriginalFile = useMemo(() => {
@ -61,9 +55,7 @@ export const CandidateContent = memo(function CandidateContent({
if (cancelled) return
const filename =
selectedCandidateFile.filename ||
matchingOriginalFile.filename ||
"code.py"
selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py"
const diff = createTwoFilesPatch(
`a/${filename}`,
@ -72,14 +64,12 @@ export const CandidateContent = memo(function CandidateContent({
selectedCandidateFile.code,
"",
"",
{ context: 3 }
{ context: 3 },
)
const lines = diff.split("\n")
const hunkStartIndex = lines.findIndex(line => line.startsWith("@@"))
const processedDiff = hunkStartIndex > 0
? lines.slice(hunkStartIndex).join("\n")
: diff
const processedDiff = hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff
setUnifiedDiff(processedDiff)
setDiffLoading(false)
@ -90,16 +80,15 @@ export const CandidateContent = memo(function CandidateContent({
setDiffLoading(false)
})
return () => { cancelled = true }
return () => {
cancelled = true
}
}, [viewMode, matchingOriginalFile, selectedCandidateFile])
const hasDiff = matchingOriginalFile !== null
const hasMultipleFiles = candidateFiles.length > 1
const codeContainerStyle = useMemo(
() => ({ maxHeight: isActive ? "80vh" : "200px" }),
[isActive]
)
const codeContainerStyle = useMemo(() => ({ maxHeight: isActive ? "80vh" : "200px" }), [isActive])
return (
<div className="space-y-3">
@ -164,7 +153,7 @@ export const CandidateContent = memo(function CandidateContent({
{hasMultipleFiles && (
<select
value={selectedFileIndex}
onChange={(e) => setSelectedFileIndex(Number(e.target.value))}
onChange={e => setSelectedFileIndex(Number(e.target.value))}
className="px-2 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700"
>
{candidateFiles.map((file, index) => (
@ -185,21 +174,21 @@ export const CandidateContent = memo(function CandidateContent({
<span className="text-sm font-mono font-medium text-zinc-700 dark:text-zinc-300">
{selectedCandidateFile.filename || "Code"}
</span>
{selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && (
<span className="text-xs text-zinc-500 dark:text-zinc-400">
({selectedCandidateFile.path})
</span>
)}
{selectedCandidateFile.path &&
selectedCandidateFile.path !== selectedCandidateFile.filename && (
<span className="text-xs text-zinc-500 dark:text-zinc-400">
({selectedCandidateFile.path})
</span>
)}
</div>
<span className="text-xs text-zinc-500">
{selectedCandidateFile.lineCount} lines
</span>
<span className="text-xs text-zinc-500">{selectedCandidateFile.lineCount} lines</span>
</div>
<div
className="overflow-y-auto"
style={codeContainerStyle}
>
<CodeHighlighter language={selectedCandidateFile.language} code={selectedCandidateFile.code} customStyle={CODE_STYLE} />
<div className="overflow-y-auto" style={codeContainerStyle}>
<CodeHighlighter
language={selectedCandidateFile.language}
code={selectedCandidateFile.code}
customStyle={CODE_STYLE}
/>
</div>
</div>
) : (

View file

@ -112,7 +112,9 @@ export const CodeContextSection = memo(function CodeContextSection({
role="button"
tabIndex={0}
onClick={() => setIsExpanded(!isExpanded)}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded)
}}
className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer"
>
<div className="flex items-center gap-2">
@ -134,7 +136,9 @@ export const CodeContextSection = memo(function CodeContextSection({
{metrics.totalFiles} files
</span>
</div>
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
<ChevronDown
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
/>
</div>
</div>
@ -153,7 +157,10 @@ export const CodeContextSection = memo(function CodeContextSection({
{filePath && (
<div className="flex flex-col">
<span className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">File</span>
<code className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit truncate max-w-full" title={filePath}>
<code
className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit truncate max-w-full"
title={filePath}
>
{filePath}
</code>
</div>
@ -242,7 +249,11 @@ interface CodeGroupSectionProps {
sectionKey: string
}
function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } {
function getAccentColorClasses(accentColor: "emerald" | "slate"): {
border: string
bg: string
icon: string
} {
switch (accentColor) {
case "emerald":
return {
@ -280,7 +291,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
role="button"
tabIndex={0}
onClick={onToggle}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggle() }}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") onToggle()
}}
className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`}
>
<div className="flex items-center gap-3">
@ -292,9 +305,12 @@ const CodeGroupSection = memo(function CodeGroupSection({
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-zinc-500 dark:text-zinc-400">
{tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files
{tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars ·{" "}
{files.length} files
</span>
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
<ChevronDown
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
/>
</div>
</div>
@ -310,7 +326,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
role="button"
tabIndex={0}
onClick={() => onToggleFile(fileKey)}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey)
}}
className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1"
>
<FileText className="h-3.5 w-3.5 text-zinc-400" />
@ -320,7 +338,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
<span className="text-xs text-zinc-500 dark:text-zinc-400" title={file.path}>
{file.path !== file.filename && `(${file.path})`}
</span>
<ChevronDown className={`h-3.5 w-3.5 text-zinc-400 transition-transform duration-200 ${isFileExpanded ? '' : '-rotate-90'}`} />
<ChevronDown
className={`h-3.5 w-3.5 text-zinc-400 transition-transform duration-200 ${isFileExpanded ? "" : "-rotate-90"}`}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500 dark:text-zinc-400">
@ -378,4 +398,4 @@ const TokenDistributionBar = memo(function TokenDistributionBar({
</div>
</div>
)
})
})

View file

@ -119,13 +119,8 @@ export const DiffView = memo(function DiffView({ diff }: DiffViewProps) {
const { bgClass, textClass, lineContent, indicator, borderClass } = getDiffLineStyle(line)
return (
<div
key={index}
className={`flex ${bgClass} border-l-2 ${borderClass}`}
>
<div className="w-8 flex-shrink-0 text-right pr-2 select-none">
{indicator}
</div>
<div key={index} className={`flex ${bgClass} border-l-2 ${borderClass}`}>
<div className="w-8 flex-shrink-0 text-right pr-2 select-none">{indicator}</div>
<pre className={`flex-1 px-2 py-0.5 ${textClass} whitespace-pre`}>
{lineContent || " "}
</pre>

View file

@ -1,12 +1,7 @@
"use client"
import { useState, useCallback, memo } from "react"
import {
XCircle,
AlertCircle,
AlertTriangle,
ChevronDown,
} from "lucide-react"
import { XCircle, AlertCircle, AlertTriangle, ChevronDown } from "lucide-react"
import { CopyButton } from "./copy-button"
interface ErrorContext {
@ -89,7 +84,9 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
role="button"
tabIndex={0}
onClick={() => toggleError(error.id)}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") toggleError(error.id)
}}
className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150"
>
<SeverityIcon className="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
@ -110,7 +107,9 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
</p>
</div>
{(hasContext || isTestFailure) && (
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
<ChevronDown
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
/>
)}
</div>
<div className="flex items-center gap-2">
@ -201,4 +200,4 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
</div>
</div>
)
})
})

View file

@ -132,7 +132,9 @@ export function formatTimelineForLLM(input: LLMExportInput): string {
lines.push("## Errors")
lines.push("")
for (const error of errors) {
lines.push(`### ${error.error_type || "Error"}${error.severity ? ` (${error.severity})` : ""}`)
lines.push(
`### ${error.error_type || "Error"}${error.severity ? ` (${error.severity})` : ""}`,
)
if (error.error_message) lines.push(error.error_message)
if (error.context) {
const ctx = error.context
@ -153,7 +155,11 @@ export function formatTimelineForLLM(input: LLMExportInput): string {
const totalCost = sections.reduce((sum, s) => sum + (s.cost ?? 0), 0)
const totalTokens = sections.reduce((sum, s) => sum + (s.tokens ?? 0), 0)
const candidatesCount = sections.filter(
s => s.type === "optimization" || s.type === "line_profiler" || s.type === "refinement" || s.type === "adaptive",
s =>
s.type === "optimization" ||
s.type === "line_profiler" ||
s.type === "refinement" ||
s.type === "adaptive",
).length
lines.push("---")

View file

@ -47,16 +47,17 @@ function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] {
function findTargetFile(files: ParsedFile[], filePath: string | null): ParsedFile | null {
if (!filePath || files.length === 0) return null
return files.find(file =>
filePath.endsWith(file.path) ||
file.path.endsWith(filePath) ||
file.path === filePath
) || null
return (
files.find(
file =>
filePath.endsWith(file.path) || file.path.endsWith(filePath) || file.path === filePath,
) || null
)
}
async function searchAllFiles(
files: ParsedFile[],
functionName: string
functionName: string,
): Promise<Array<{ file: ParsedFile; location: FunctionLocation } | null>> {
const searchPromises = files.map(async file => {
const location = await findFunctionInCode(file.code, functionName)
@ -119,7 +120,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
}
searchForFunction(functionName)
return () => { cancelled = true }
return () => {
cancelled = true
}
}, [functionName, filePath, allFiles])
const functionFile = actualFile ?? allFiles[0] ?? null
@ -143,11 +146,11 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
const lineHeight = 22.4
const paddingTop = 16
const targetLine = functionLocation.startLine - 1
const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3)
const scrollPosition = paddingTop + targetLine * lineHeight - container.clientHeight / 3
container.scrollTo({
top: Math.max(0, scrollPosition),
behavior: 'smooth'
behavior: "smooth",
})
}
@ -168,16 +171,16 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
role="button"
tabIndex={0}
onClick={() => setIsExpanded(!isExpanded)}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded)
}}
className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1"
>
<div className="p-2 bg-zinc-800 rounded-sm">
<Code className="h-4 w-4 text-zinc-400" />
</div>
<div className="text-left">
<h2 className="text-lg font-semibold text-zinc-50">
Function to Optimize
</h2>
<h2 className="text-lg font-semibold text-zinc-50">Function to Optimize</h2>
<div className="flex items-center gap-2 mt-1">
{functionName && (
<code className="text-sm font-mono text-zinc-300 bg-zinc-800 px-2 py-0.5 rounded-sm">
@ -191,7 +194,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
)}
</div>
</div>
<ChevronDown className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
<ChevronDown
className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
/>
</div>
<div className="flex items-center gap-2">
<CopyButton text={functionFile.code} label="function code" />
@ -206,13 +211,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
{functionFile.filename}
</span>
{functionFile.path !== functionFile.filename && (
<span className="text-xs font-mono text-zinc-500">
({functionFile.path})
</span>
<span className="text-xs font-mono text-zinc-500">({functionFile.path})</span>
)}
<span className="text-xs text-zinc-500 ml-auto">
{functionFile.lineCount} lines
</span>
<span className="text-xs text-zinc-500 ml-auto">{functionFile.lineCount} lines</span>
</div>
<div ref={codeContainerRef} className="max-h-[500px] overflow-y-auto">
@ -227,4 +228,4 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
)}
</div>
)
})
})

View file

@ -6,9 +6,14 @@ export { transformToTimelineSections } from "./timeline-types"
export { ErrorsSection } from "./errors-section"
export { FunctionToOptimizeSection } from "./function-to-optimize-section"
export { CodeContextSection } from "./code-context-section"
export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter"
export {
CodeHighlighter,
CODE_STYLE,
CODE_STYLE_RELAXED,
CODE_STYLE_SMALL,
} from "./code-highlighter"
export { findFunctionInCode } from "./python-parser"
export type { FunctionLocation } from "./python-parser"
export { CopyButton } from "./copy-button"
export { InfoIcon } from "./info-icon"
export { getTraceSource } from "./utils"
export { getTraceSource } from "./utils"

View file

@ -1,14 +1,7 @@
"use client"
import { useState, useRef, useEffect, memo, useMemo } from "react"
import {
ChevronDown,
ChevronUp,
Code,
Bug,
Search,
X,
} from "lucide-react"
import { ChevronDown, ChevronUp, Code, Bug, Search, X } from "lucide-react"
import {
Dialog,
DialogContent,
@ -97,7 +90,10 @@ const PromptContent = memo(function PromptContent({ content }: { content: string
{parts.map((part, index) => {
if (part.type === "code") {
return (
<div key={index} className="rounded border border-zinc-200 dark:border-zinc-700 overflow-hidden">
<div
key={index}
className="rounded border border-zinc-200 dark:border-zinc-700 overflow-hidden"
>
<CodeHighlighter
language={part.language || "python"}
code={part.content}
@ -194,12 +190,13 @@ function applySearchHighlights(container: HTMLElement, query: string): number {
function scrollToSearchMatch(container: HTMLElement, index: number) {
const marks = container.querySelectorAll("mark[data-search-highlight]")
marks.forEach(m => {
(m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]"
;(m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]"
})
const target = marks[index] as HTMLElement | undefined
if (target) {
target.className = "bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500"
target.className =
"bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500"
target.scrollIntoView({ behavior: "smooth", block: "center" })
}
}
@ -307,9 +304,10 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
function navigateMatch(direction: "next" | "prev") {
if (matchCount === 0) return
const next = direction === "next"
? (currentMatch + 1) % matchCount
: (currentMatch - 1 + matchCount) % matchCount
const next =
direction === "next"
? (currentMatch + 1) % matchCount
: (currentMatch - 1 + matchCount) % matchCount
setCurrentMatch(next)
if (contentRef.current) {
@ -411,7 +409,7 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
</DialogTrigger>
<DialogContent
className="w-[95vw] max-w-[95vw] h-[90vh] max-h-[90vh] flex flex-col overflow-hidden"
onKeyDown={(e) => {
onKeyDown={e => {
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
e.preventDefault()
setSearchOpen(true)
@ -457,12 +455,13 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
<p className="text-sm text-red-500">Failed to load debug data: {fetchError}</p>
</div>
) : isLoading ? (
<div className="flex-1 flex flex-col min-h-0 mt-3 p-4">
{loadingSkeleton}
</div>
<div className="flex-1 flex flex-col min-h-0 mt-3 p-4">{loadingSkeleton}</div>
) : showResponse ? (
<div className="flex-1 flex flex-col min-h-0 mt-3">
<div ref={contentRef} className="flex-1 overflow-y-auto p-4 bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700">
<div
ref={contentRef}
className="flex-1 overflow-y-auto p-4 bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700"
>
{fetchedData.rawResponse ? (
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
{fetchedData.rawResponse}
@ -473,7 +472,11 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
</div>
</div>
) : (
<Tabs value={activeTab} onValueChange={v => setActiveTab(v as "user" | "system")} className="flex-1 flex flex-col min-h-0 mt-3">
<Tabs
value={activeTab}
onValueChange={v => setActiveTab(v as "user" | "system")}
className="flex-1 flex flex-col min-h-0 mt-3"
>
<TabsList className="flex-shrink-0 w-fit mx-auto">
<TabsTrigger value="user">
User Prompt
@ -500,14 +503,12 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
) : (
<span className="text-zinc-400">No user prompt</span>
)
) : fetchedData.systemPrompt ? (
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
{fetchedData.systemPrompt}
</pre>
) : (
fetchedData.systemPrompt ? (
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
{fetchedData.systemPrompt}
</pre>
) : (
<span className="text-zinc-400">No system prompt</span>
)
<span className="text-zinc-400">No system prompt</span>
)}
</div>
</div>

View file

@ -35,7 +35,7 @@ async function getParser(): Promise<ParserType | null> {
export async function findFunctionInCode(
code: string,
functionName: string
functionName: string,
): Promise<FunctionLocation | null> {
const { className, methodName } = parseQualifiedName(functionName)
@ -52,7 +52,7 @@ export async function findFunctionInCode(
async function findWithTreeSitter(
code: string,
className: string | undefined,
methodName: string
methodName: string,
): Promise<FunctionLocation | null> {
try {
const parser = await getParser()
@ -80,7 +80,7 @@ async function findWithTreeSitter(
function findFunctionWithRegex(
code: string,
functionName: string,
className?: string
className?: string,
): FunctionLocation | null {
const lines = code.split("\n")
@ -92,14 +92,10 @@ function findFunctionWithRegex(
function findMethodInClassWithRegex(
lines: string[],
className: string,
methodName: string
methodName: string,
): FunctionLocation | null {
const classPattern = new RegExp(
`^(\\s*)class\\s+${escapeRegex(className)}\\s*[:(]`
)
const methodPattern = new RegExp(
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(methodName)}\\s*\\(`
)
const classPattern = new RegExp(`^(\\s*)class\\s+${escapeRegex(className)}\\s*[:(]`)
const methodPattern = new RegExp(`^(\\s*)(async\\s+)?def\\s+${escapeRegex(methodName)}\\s*\\(`)
let classIndent = -1
let inClass = false
@ -142,10 +138,10 @@ function findMethodInClassWithRegex(
function findStandaloneFunctionWithRegex(
lines: string[],
functionName: string
functionName: string,
): FunctionLocation | null {
const functionPattern = new RegExp(
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(`
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(`,
)
for (let i = 0; i < lines.length; i++) {
@ -159,11 +155,7 @@ function findStandaloneFunctionWithRegex(
return null
}
function findBlockEnd(
lines: string[],
startLine: number,
startIndent: number
): FunctionLocation {
function findBlockEnd(lines: string[], startLine: number, startIndent: number): FunctionLocation {
for (let i = startLine; i < lines.length; i++) {
const trimmed = lines[i].trim()
if (!trimmed || trimmed.startsWith("#")) continue
@ -199,11 +191,7 @@ function parseQualifiedName(functionName: string): {
return { className: undefined, methodName: functionName }
}
function findMethodInClass(
rootNode: Node,
className: string,
methodName: string
): Node | null {
function findMethodInClass(rootNode: Node, className: string, methodName: string): Node | null {
// Find all class definitions matching the class name
function searchClasses(node: Node): Node | null {
if (node.type === "class_definition") {
@ -238,10 +226,7 @@ function findMethodInClass(
}
function findFunctionNode(node: Node, functionName: string): Node | null {
if (
node.type === "function_definition" ||
node.type === "async_function_definition"
) {
if (node.type === "function_definition" || node.type === "async_function_definition") {
const nameNode = node.childForFieldName("name")
if (nameNode && nameNode.text === functionName) {
return node
@ -264,4 +249,4 @@ function findFunctionNode(node: Node, functionName: string): Node | null {
}
return null
}
}

View file

@ -12,7 +12,7 @@ interface RankingContentProps {
export const RankingContent = memo(function RankingContent({ content }: RankingContentProps) {
const strippedCodes = useMemo(
() => new Map(content.rankings.map(item => [item.id, stripCodeHeader(item.code)])),
[content.rankings]
[content.rankings],
)
return (
@ -27,7 +27,7 @@ export const RankingContent = memo(function RankingContent({ content }: RankingC
{content.rankings.length >= 1 && (
<div className="space-y-4">
{content.rankings.map((item) => (
{content.rankings.map(item => (
<div
key={item.id}
className={`rounded border ${

View file

@ -1,17 +1,11 @@
"use client"
import { useState, memo } from "react"
import {
ChevronDown,
FlaskConical,
CheckCircle2,
} from "lucide-react"
import { ChevronDown, FlaskConical, CheckCircle2 } from "lucide-react"
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
import type { TimelineSectionContent } from "./timeline-types"
function getEmptyMessage(
variant: "generated" | "instrumented" | "instrumentedPerf"
): string {
function getEmptyMessage(variant: "generated" | "instrumented" | "instrumentedPerf"): string {
switch (variant) {
case "generated":
return "No generated test available"
@ -31,7 +25,9 @@ interface TestContentProps {
export const TestContent = memo(function TestContent({ content }: TestContentProps) {
const [showDetails, setShowDetails] = useState(false)
const [expandedTest, setExpandedTest] = useState<number | null>(null)
const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated")
const [activeVariant, setActiveVariant] = useState<
"generated" | "instrumented" | "instrumentedPerf"
>("generated")
const testCount = content.testGroups.length
const hasInstrumented = content.testGroups.some(g => g.instrumented)
@ -70,15 +66,19 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
className="px-3 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors flex items-center gap-1.5"
>
{showDetails ? "Hide Details" : "View Details"}
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${showDetails ? "" : "-rotate-90"}`} />
<ChevronDown
className={`h-3.5 w-3.5 transition-transform duration-200 ${showDetails ? "" : "-rotate-90"}`}
/>
</button>
</div>
{showDetails && (
<div className="space-y-3 pt-2 border-t border-zinc-200 dark:border-zinc-700">
{content.testGroups.map((group) => {
{content.testGroups.map(group => {
const isExpanded = expandedTest === group.index
const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1
const hasMultipleVariants =
[group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length >
1
const currentCode = (() => {
switch (activeVariant) {
@ -94,7 +94,10 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
})()
return (
<div key={group.index} className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden">
<div
key={group.index}
className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden"
>
<button
onClick={() => setExpandedTest(isExpanded ? null : group.index)}
className="w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-800 flex items-center justify-between hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
@ -124,9 +127,14 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500">
{group.generated?.lines ?? group.instrumented?.lines ?? group.instrumentedPerf?.lines} lines
{group.generated?.lines ??
group.instrumented?.lines ??
group.instrumentedPerf?.lines}{" "}
lines
</span>
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`} />
<ChevronDown
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
/>
</div>
</button>

View file

@ -24,10 +24,7 @@ interface TimelineChatProps {
onClose: () => void
}
export const TimelineChat = memo(function TimelineChat({
traceId,
onClose,
}: TimelineChatProps) {
export const TimelineChat = memo(function TimelineChat({ traceId, onClose }: TimelineChatProps) {
const [messages, setMessages] = useState<ChatMessage[]>([])
const [input, setInput] = useState("")
const [isStreaming, setIsStreaming] = useState(false)
@ -63,7 +60,7 @@ export const TimelineChat = memo(function TimelineChat({
const controller = new AbortController()
abortRef.current = controller
setMessages((prev) => [...prev, { role: "assistant", content: "" }])
setMessages(prev => [...prev, { role: "assistant", content: "" }])
setCompletedRounds([])
setActiveSteps([])
@ -110,15 +107,17 @@ export const TimelineChat = memo(function TimelineChat({
// Handle typed events (new protocol)
if (parsed.type === "tool_start") {
setStatusMessage(null)
setActiveSteps((prev) => {
setActiveSteps(prev => {
// If all previous steps are done, this is a new round — commit previous steps
if (prev.length > 0 && prev.every((s) => s.status === "done")) {
setCompletedRounds((rounds) => [...rounds, prev])
return [{
tool: parsed.tool,
displayName: parsed.displayName ?? parsed.tool,
status: "running",
}]
if (prev.length > 0 && prev.every(s => s.status === "done")) {
setCompletedRounds(rounds => [...rounds, prev])
return [
{
tool: parsed.tool,
displayName: parsed.displayName ?? parsed.tool,
status: "running",
},
]
}
return [
...prev,
@ -133,8 +132,8 @@ export const TimelineChat = memo(function TimelineChat({
}
if (parsed.type === "tool_result") {
setActiveSteps((prev) =>
prev.map((step) =>
setActiveSteps(prev =>
prev.map(step =>
step.tool === parsed.tool && step.status === "running"
? { ...step, status: "done", summary: parsed.summary, content: parsed.content }
: step,
@ -147,7 +146,7 @@ export const TimelineChat = memo(function TimelineChat({
const textContent = parsed.type === "text" ? parsed.text : parsed.text
if (textContent) {
setStatusMessage(null)
setMessages((prev) => {
setMessages(prev => {
const updated = [...prev]
const last = updated[updated.length - 1]
if (last?.role === "assistant") {
@ -171,7 +170,7 @@ export const TimelineChat = memo(function TimelineChat({
} catch (err) {
if ((err as Error).name === "AbortError") return
setMessages((prev) => {
setMessages(prev => {
const updated = [...prev]
const last = updated[updated.length - 1]
if (last?.role === "assistant" && !last.content) {
@ -184,9 +183,9 @@ export const TimelineChat = memo(function TimelineChat({
})
} finally {
// Commit any remaining active steps as a final completed round
setActiveSteps((prev) => {
setActiveSteps(prev => {
if (prev.length > 0) {
setCompletedRounds((rounds) => [...rounds, prev])
setCompletedRounds(rounds => [...rounds, prev])
}
return []
})
@ -203,7 +202,7 @@ export const TimelineChat = memo(function TimelineChat({
sendMessage()
}
},
[sendMessage]
[sendMessage],
)
const stopStreaming = useCallback(() => {
@ -213,8 +212,8 @@ export const TimelineChat = memo(function TimelineChat({
const [copied, setCopied] = useState(false)
const exportChat = useCallback(() => {
const text = messages
.filter((m) => m.content)
.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
.filter(m => m.content)
.map(m => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
.join("\n\n")
navigator.clipboard.writeText(text).then(() => {
setCopied(true)
@ -227,9 +226,7 @@ export const TimelineChat = memo(function TimelineChat({
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-zinc-500" />
<span className="text-sm font-medium text-zinc-900 dark:text-white">
Chat with Trace
</span>
<span className="text-sm font-medium text-zinc-900 dark:text-white">Chat with Trace</span>
</div>
<div className="flex items-center gap-1">
{messages.length > 0 && (
@ -260,7 +257,8 @@ export const TimelineChat = memo(function TimelineChat({
Ask about this optimization trace
</p>
<p className="text-xs text-zinc-400 dark:text-zinc-500">
e.g. &quot;Why was candidate 1 ranked best?&quot; or &quot;What optimizations were attempted?&quot;
e.g. &quot;Why was candidate 1 ranked best?&quot; or &quot;What optimizations were
attempted?&quot;
</p>
</div>
)}
@ -297,13 +295,13 @@ export const TimelineChat = memo(function TimelineChat({
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask about this trace..."
rows={1}
className="flex-1 resize-none rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent max-h-32 min-h-[38px]"
style={{ height: "38px" }}
onInput={(e) => {
onInput={e => {
const target = e.target as HTMLTextAreaElement
target.style.height = "38px"
target.style.height = `${Math.min(target.scrollHeight, 128)}px`
@ -341,11 +339,11 @@ const ToolRoundBubble = memo(function ToolRoundBubble({
steps: ToolStep[]
isActive?: boolean
}) {
const allDone = steps.every((s) => s.status === "done")
const allDone = steps.every(s => s.status === "done")
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set())
const toggleStep = useCallback((index: number) => {
setExpandedSteps((prev) => {
setExpandedSteps(prev => {
const next = new Set(prev)
if (next.has(index)) {
next.delete(index)
@ -397,7 +395,10 @@ const ToolRoundBubble = memo(function ToolRoundBubble({
<span>
{step.displayName}
{step.summary && (
<span className="text-zinc-400 dark:text-zinc-500"> {step.summary}</span>
<span className="text-zinc-400 dark:text-zinc-500">
{" "}
{step.summary}
</span>
)}
</span>
</button>
@ -458,7 +459,11 @@ const ToolResultContent = memo(function ToolResultContent({ content }: { content
},
// Render plain text content as preformatted when no markdown structure detected
p({ children }) {
return <p className="whitespace-pre-wrap break-words text-xs text-zinc-600 dark:text-zinc-300">{children}</p>
return (
<p className="whitespace-pre-wrap break-words text-xs text-zinc-600 dark:text-zinc-300">
{children}
</p>
)
},
}}
>

View file

@ -100,7 +100,7 @@ export function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] {
export function findMatchingFile(
files: ParsedCodeBlock[],
targetPath: string | null
targetPath: string | null,
): ParsedCodeBlock | null {
if (!targetPath || files.length === 0) {
return files[0] || null

View file

@ -5,11 +5,7 @@ import { MessageSquare } from "lucide-react"
import { formatTime } from "./timeline-helpers"
import { TimelineSectionCard } from "./timeline-section-card"
import { TimelineChat } from "./timeline-chat"
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable"
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"
import type { Layout } from "react-resizable-panels"
import type { TimelineSection } from "./timeline-types"
@ -40,17 +36,25 @@ export const TimelinePageView = memo(function TimelinePageView({
try {
const stored = localStorage.getItem("obs-chat-layout")
if (stored) setSavedLayout(JSON.parse(stored))
} catch { /* ignore */ }
} catch {
/* ignore */
}
}, [])
const handleLayoutChanged = useCallback((layout: Layout) => {
try { localStorage.setItem("obs-chat-layout", JSON.stringify(layout)) } catch { /* ignore */ }
try {
localStorage.setItem("obs-chat-layout", JSON.stringify(layout))
} catch {
/* ignore */
}
}, [])
function getSectionRef(index: number) {
let cb = refCallbacks.current.get(index)
if (!cb) {
cb = (el: HTMLDivElement | null) => { sectionRefs.current[index] = el }
cb = (el: HTMLDivElement | null) => {
sectionRefs.current[index] = el
}
refCallbacks.current.set(index, cb)
}
return cb
@ -62,7 +66,7 @@ export const TimelinePageView = memo(function TimelinePageView({
const root = chatOpen ? scrollContainerRef.current : null
const observer = new IntersectionObserver(
(entries) => {
entries => {
let bestIndex = -1
let bestDistance = Infinity
@ -76,7 +80,7 @@ export const TimelinePageView = memo(function TimelinePageView({
const sectionMiddle = rect.top + rect.height / 2
const viewportH = root ? root.clientHeight : window.innerHeight
const rootTop = root ? root.getBoundingClientRect().top : 0
const distance = Math.abs((sectionMiddle - rootTop) - viewportH * 0.35)
const distance = Math.abs(sectionMiddle - rootTop - viewportH * 0.35)
if (distance < bestDistance) {
bestDistance = distance
@ -92,7 +96,7 @@ export const TimelinePageView = memo(function TimelinePageView({
root,
threshold: [0.5],
rootMargin: "-10% 0px -55% 0px",
}
},
)
sectionRefs.current.forEach((ref, index) => {
@ -105,7 +109,7 @@ export const TimelinePageView = memo(function TimelinePageView({
return () => observer.disconnect()
}, [sections.length, chatOpen])
const toggleChat = useCallback(() => setChatOpen((prev) => !prev), [])
const toggleChat = useCallback(() => setChatOpen(prev => !prev), [])
const closeChat = useCallback(() => setChatOpen(false), [])
if (sections.length === 0) {
@ -118,8 +122,7 @@ export const TimelinePageView = memo(function TimelinePageView({
const activeSection = sections[activeIndex]
const shouldExpandContainer =
activeSection?.content.type === "candidate" ||
activeSection?.content.type === "refinement"
activeSection?.content.type === "candidate" || activeSection?.content.type === "refinement"
const timelineSections = (
<>
@ -130,11 +133,7 @@ export const TimelinePageView = memo(function TimelinePageView({
</div>
{sections.map((section, index) => (
<div
key={section.id}
ref={getSectionRef(index)}
className="scroll-mt-24"
>
<div key={section.id} ref={getSectionRef(index)} className="scroll-mt-24">
<TimelineSectionCard
section={section}
isActive={index === activeIndex}
@ -148,16 +147,16 @@ export const TimelinePageView = memo(function TimelinePageView({
<div className="absolute right-6 top-0 h-6 w-px bg-zinc-200 dark:bg-zinc-700" />
<div className="absolute right-4 top-6 w-4 h-4 rounded-full bg-zinc-300 dark:bg-zinc-600 border-2 border-white dark:border-zinc-900 z-10" />
<div className="mr-14 py-4 text-right">
<span className="text-xs text-zinc-500 dark:text-zinc-400">
End
</span>
<span className="text-xs text-zinc-500 dark:text-zinc-400">End</span>
</div>
</div>
</>
)
const header = (
<div className={`${chatOpen ? "flex-shrink-0" : "sticky top-0"} z-30 bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700`}>
<div
className={`${chatOpen ? "flex-shrink-0" : "sticky top-0"} z-30 bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700`}
>
<div className="max-w-6xl mx-auto px-4 py-3">
<div className="flex items-center justify-between mb-2">
<div>

View file

@ -1,12 +1,7 @@
"use client"
import React, { memo } from "react"
import {
Clock,
CheckCircle2,
XCircle,
AlertCircle,
} from "lucide-react"
import { Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react"
import { TYPE_CONFIG, formatTime } from "./timeline-helpers"
import { LLMCallDebugDialog } from "./llm-call-debug-dialog"
import { TestContent } from "./test-content"
@ -42,11 +37,7 @@ export const TimelineSectionCard = memo(function TimelineSectionCard({
const Icon = config.icon
return (
<div
className={`relative ${
isActive ? "opacity-100" : "opacity-60"
}`}
>
<div className={`relative ${isActive ? "opacity-100" : "opacity-60"}`}>
<div className="absolute right-6 top-0 bottom-0 w-px bg-zinc-200 dark:bg-zinc-700" />
<div className="mr-14 mb-6">
@ -112,19 +103,12 @@ export const TimelineSectionCard = memo(function TimelineSectionCard({
</div>
<div className="p-4 bg-white dark:bg-zinc-800">
{section.content.type === "tests" && (
<TestContent content={section.content} />
)}
{(section.content.type === "candidate" ||
section.content.type === "refinement") && (
{section.content.type === "tests" && <TestContent content={section.content} />}
{(section.content.type === "candidate" || section.content.type === "refinement") && (
<CandidateContent content={section.content} isActive={isActive} />
)}
{section.content.type === "ranking" && (
<RankingContent content={section.content} />
)}
{section.content.type === "summary" && (
<SummaryContent content={section.content} />
)}
{section.content.type === "ranking" && <RankingContent content={section.content} />}
{section.content.type === "summary" && <SummaryContent content={section.content} />}
</div>
</div>
</div>

View file

@ -4,7 +4,14 @@ export interface LLMCallDebugData {
export interface TimelineSection {
id: string
type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "adaptive" | "ranking" | "summary"
type:
| "test_generation"
| "optimization"
| "line_profiler"
| "refinement"
| "adaptive"
| "ranking"
| "summary"
title: string
subtitle?: string
timestamp: number
@ -26,10 +33,37 @@ export interface TestGroup {
export type TimelineSectionContent =
| { type: "tests"; testGroups: TestGroup[]; testFramework?: string }
| { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
| { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
| { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean }
| { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } }
| {
type: "candidate"
code: string
originalCode: string | null
explanation?: string
rank?: number
isBest?: boolean
}
| {
type: "refinement"
code: string
parentCode: string | null
explanation?: string
rank?: number
isBest?: boolean
}
| {
type: "ranking"
explanation: string
rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>
usedForPr: boolean
}
| {
type: "summary"
metrics: {
totalCost: number
totalTokens: number
totalDuration: number
candidatesCount: number
}
}
export interface TransformInput {
calls: Array<{
@ -80,9 +114,10 @@ export interface TransformInput {
usedForPr: boolean
}
export function transformToTimelineSections(
input: TransformInput
): { sections: TimelineSection[]; totalDuration: number } {
export function transformToTimelineSections(input: TransformInput): {
sections: TimelineSection[]
totalDuration: number
} {
const {
calls,
optimizationCandidates,
@ -132,7 +167,7 @@ export function transformToTimelineSections(
const maxTestIndex = Math.max(
generatedTests.length,
instrumentedTests.length,
instrumentedPerfTests.length
instrumentedPerfTests.length,
)
const genMap = new Map(generatedTests.map(t => [t.index, t]))
@ -166,9 +201,7 @@ export function transformToTimelineSections(
if (testCalls.length > 0 || testGroups.length > 0) {
const firstTestCall = testCalls[0]
const firstTimestamp = firstTestCall
? timestampMap.get(firstTestCall.id)! - minTime
: 0
const firstTimestamp = firstTestCall ? timestampMap.get(firstTestCall.id)! - minTime : 0
const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0)
const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0)
@ -201,7 +234,12 @@ export function transformToTimelineSections(
})
}
const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates, ...adaptiveCandidates]
const allCandidates = [
...optimizationCandidates,
...lineProfilerCandidates,
...refinementCandidates,
...adaptiveCandidates,
]
const allCandidatesById = new Map(allCandidates.map(c => [c.id, c]))
// Pre-compute ranking data (identical for all ranking calls)
@ -252,7 +290,8 @@ export function transformToTimelineSections(
title: `Optimization Candidate ${candidate.index}`,
timestamp,
duration: call.latency_ms ?? undefined,
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
status:
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
model: call.model_name,
cost: call.llm_cost,
tokens: call.total_tokens,
@ -279,7 +318,8 @@ export function transformToTimelineSections(
subtitle: "Guided by profiling data",
timestamp,
duration: call.latency_ms ?? undefined,
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
status:
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
model: call.model_name,
cost: call.llm_cost,
tokens: call.total_tokens,
@ -306,9 +346,10 @@ export function transformToTimelineSections(
let parentLabel: string | undefined
if (parentCandidate) {
const source = (parentCandidate as { source?: string }).source
parentLabel = source === "REFINE"
? `From Refinement ${parentCandidate.index}`
: `From Candidate ${parentCandidate.index}`
parentLabel =
source === "REFINE"
? `From Refinement ${parentCandidate.index}`
: `From Candidate ${parentCandidate.index}`
}
sections.push({
id: call.id,
@ -317,7 +358,8 @@ export function transformToTimelineSections(
subtitle: parentLabel,
timestamp,
duration: call.latency_ms ?? undefined,
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
status:
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
model: call.model_name,
cost: call.llm_cost,
tokens: call.total_tokens,
@ -359,7 +401,8 @@ export function transformToTimelineSections(
subtitle: parentLabel ?? "Informed by full optimization history",
timestamp,
duration: call.latency_ms ?? undefined,
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
status:
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
model: call.model_name,
cost: call.llm_cost,
tokens: call.total_tokens,
@ -382,7 +425,8 @@ export function transformToTimelineSections(
subtitle: "Selecting the best optimization",
timestamp,
duration: call.latency_ms ?? undefined,
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
status:
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
model: call.model_name,
cost: call.llm_cost,
tokens: call.total_tokens,
@ -399,7 +443,7 @@ export function transformToTimelineSections(
// Fallback: create sections for adaptive candidates not matched to an LLM call
// (the backend currently logs adaptive LLM calls with an empty call_type)
const matchedAdaptiveCount = (callIndexByType.get("adaptive_optimize") ?? 0)
const matchedAdaptiveCount = callIndexByType.get("adaptive_optimize") ?? 0
for (let i = matchedAdaptiveCount; i < adaptiveCandidates.length; i++) {
const candidate = adaptiveCandidates[i]
const rank = candidateRankMap[candidate.id]
@ -451,7 +495,7 @@ export function transformToTimelineSections(
const candidateTypeSet = new Set(["optimization", "line_profiler", "refinement", "adaptive"])
const sectionSortIndex = new Map(
sections.map(s => [s, parseInt(s.title.match(/\d+$/)?.[0] ?? "0", 10)])
sections.map(s => [s, parseInt(s.title.match(/\d+$/)?.[0] ?? "0", 10)]),
)
sections.sort((a, b) => {
@ -465,4 +509,4 @@ export function transformToTimelineSections(
})
return { sections, totalDuration }
}
}

View file

@ -10,7 +10,11 @@ interface TraceSearchProps {
hasResults?: boolean
}
export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) {
export function TraceSearch({
initialTraceId = "",
isLoading = false,
hasResults = false,
}: TraceSearchProps) {
const [traceId, setTraceId] = useState(initialTraceId)
const router = useRouter()
@ -95,4 +99,4 @@ export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults
)}
</div>
)
}
}

View file

@ -13,4 +13,4 @@ export function getTraceSource(eventType: string | null): string {
}
return eventType
}
}

View file

@ -23,7 +23,7 @@ export default function ObservabilityLoading() {
{/* Timeline items skeleton */}
<div className="space-y-8">
{[1, 2, 3].map((i) => (
{[1, 2, 3].map(i => (
<div key={i} className="flex gap-4">
<div className="w-5 h-5 rounded-full bg-zinc-200 dark:bg-zinc-700 animate-pulse" />
<div className="flex-1">
@ -36,4 +36,4 @@ export default function ObservabilityLoading() {
</div>
</div>
)
}
}

View file

@ -69,8 +69,7 @@ export function Breadcrumb({
}
const breadcrumbs = generateBreadcrumbs()
const visibleBreadcrumbs =
hideRoot && breadcrumbs.length > 1 ? breadcrumbs.slice(1) : breadcrumbs
const visibleBreadcrumbs = hideRoot && breadcrumbs.length > 1 ? breadcrumbs.slice(1) : breadcrumbs
// Don't show breadcrumbs if we're on the root
if (visibleBreadcrumbs.length <= 1) {

View file

@ -23,7 +23,10 @@ export function ObservabilityNav() {
if (pathname === "/observability") {
setViewMode("timeline")
localStorage.setItem("observability-view-mode", "timeline")
} else if (pathname.startsWith("/observability/traces") || pathname.startsWith("/observability/llm-calls")) {
} else if (
pathname.startsWith("/observability/traces") ||
pathname.startsWith("/observability/llm-calls")
) {
setViewMode("classic")
localStorage.setItem("observability-view-mode", "classic")
} else {
@ -95,7 +98,7 @@ export function ObservabilityNav() {
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all",
viewMode === "classic"
? "bg-white dark:bg-gray-800 text-blue-700 dark:text-blue-300 shadow-sm"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",
)}
>
<Layers className="h-4 w-4" />
@ -107,7 +110,7 @@ export function ObservabilityNav() {
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all",
viewMode === "timeline"
? "bg-white dark:bg-gray-800 text-blue-700 dark:text-blue-300 shadow-sm"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",
)}
>
<Clock className="h-4 w-4" />

View file

@ -41,7 +41,14 @@ interface StatCardProps {
className?: string
}
export function StatCard({ label, value, helpText, icon, variant = "default", className }: StatCardProps) {
export function StatCard({
label,
value,
helpText,
icon,
variant = "default",
className,
}: StatCardProps) {
const IconComponent = icon ? iconMap[icon] : null
return (
@ -56,15 +63,15 @@ export function StatCard({ label, value, helpText, icon, variant = "default", cl
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
{IconComponent && <IconComponent className="h-4 w-4 text-gray-500 dark:text-gray-400" />}
{IconComponent && (
<IconComponent className="h-4 w-4 text-gray-500 dark:text-gray-400" />
)}
<div className="text-sm text-gray-600 dark:text-gray-400 font-medium flex items-center gap-1.5">
{label}
{helpText && <InfoIcon content={helpText} side="top" />}
</div>
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{value}
</div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">{value}</div>
</div>
</div>
</div>

View file

@ -610,7 +610,8 @@ export function RenderStepContent({
</h4>
<ApiKeyCard label="GitHub Actions Setup" value="codeflash init-actions" />
<p className="mt-2 text-sm text-muted-foreground font-medium">
Automate optimization of your future code by setting up Codeflash Github Actions{" "}
Automate optimization of your future code by setting up Codeflash Github
Actions{" "}
</p>
</div>
</div>

View file

@ -6,7 +6,6 @@ import { type JSX } from "react"
export function SignOut(): JSX.Element {
return (
<a href="/auth/logout">
<Button
variant="ghost"

View file

@ -23,10 +23,8 @@ const badgeVariants = cva(
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}

View file

@ -31,8 +31,7 @@ const buttonVariants = cva(
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
}

View file

@ -21,14 +21,14 @@ interface FormFieldContextValue<
> {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
@ -37,7 +37,6 @@ const FormField = <
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
@ -45,7 +44,6 @@ const useFormField = () => {
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
@ -65,7 +63,7 @@ const useFormField = () => {
interface FormItemContextValue {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
@ -142,7 +140,6 @@ const FormMessage = React.forwardRef<
const { error, formMessageId } = useFormField()
const body = error != null ? String(error?.message) : children
if (!body) {
return null
}

View file

@ -11,15 +11,9 @@ import {
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: GroupProps) => (
const ResizablePanelGroup = ({ className, ...props }: GroupProps) => (
<Group
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
)

View file

@ -63,7 +63,6 @@ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayNam
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content

View file

@ -8,7 +8,6 @@ import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}

View file

@ -135,7 +135,6 @@ function dispatch(action: Action): void {
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()

View file

@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import type { Span } from "@sentry/core"
import * as Sentry from "@sentry/nextjs"
import { withTiming } from "../server-action-timing"
@ -8,7 +9,7 @@ describe("withTiming", () => {
beforeEach(() => {
mockSetAttribute = vi.fn()
vi.mocked(Sentry.startSpan).mockImplementation((_opts, callback) =>
callback({ setAttribute: mockSetAttribute } as any),
callback({ setAttribute: mockSetAttribute } as unknown as Span),
)
})
@ -50,10 +51,7 @@ describe("withTiming", () => {
const wrapped = withTiming("test", vi.fn().mockResolvedValue(null))
await wrapped()
expect(mockSetAttribute).toHaveBeenCalledWith(
"server_action.duration_ms",
250,
)
expect(mockSetAttribute).toHaveBeenCalledWith("server_action.duration_ms", 250)
nowSpy.mockRestore()
})
})
@ -67,12 +65,8 @@ describe("withTiming", () => {
const wrapped = withTiming("slowAction", vi.fn().mockResolvedValue(null))
await wrapped()
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("slowAction"),
)
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("1500"),
)
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("slowAction"))
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("1500"))
warnSpy.mockRestore()
nowSpy.mockRestore()
@ -136,10 +130,7 @@ describe("withTiming", () => {
const wrapped = withTiming("errAction", vi.fn().mockRejectedValue(new Error("oops")))
await expect(wrapped()).rejects.toThrow()
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("errAction"),
expect.any(Error),
)
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("errAction"), expect.any(Error))
errorSpy.mockRestore()
nowSpy.mockRestore()

View file

@ -1,4 +1,4 @@
export type ActionResponse<T = any> = {
export type ActionResponse<T = unknown> = {
success: boolean
data?: T
error?: string
@ -12,7 +12,7 @@ export function createSuccessResponse<T>(data: T): ActionResponse<T> {
}
}
export function createErrorResponse(error: string): ActionResponse {
export function createErrorResponse<T = unknown>(error: string): ActionResponse<T> {
return {
success: false,
data: undefined,

View file

@ -25,9 +25,7 @@ let _auth0: Auth0Client | undefined
function getAuth0Client(): Auth0Client {
if (!_auth0) {
if (!auth0Domain) {
// During build/CI prerendering, Auth0 env vars aren't set.
// Return a stub — getSession() returning null is correct (no user).
return { getSession: async () => null } as unknown as Auth0Client
throw new Error("AUTH0_DOMAIN (or AUTH0_ISSUER_BASE_URL) environment variable is required")
}
_auth0 = new Auth0Client({
domain: auth0Domain,
@ -64,7 +62,7 @@ function getAuth0Client(): Auth0Client {
const match = errorMessage.match(re)
if (match != null) {
const userNickname = match[2]
return redirectTo(`/waitlist?username=${userNickname}`)
return redirectTo(`/waitlist?username=${encodeURIComponent(userNickname)}`)
}
}
@ -124,7 +122,7 @@ function getAuth0Client(): Auth0Client {
export const auth0: Auth0Client = new Proxy({} as Auth0Client, {
get(_, prop) {
const client = getAuth0Client()
const value = (client as any)[prop]
const value = client[prop as keyof Auth0Client]
return typeof value === "function" ? value.bind(client) : value
},
})

View file

@ -64,7 +64,10 @@ export function parseLineProfilerResults(rawResults: string): LineProfilerReport
// Detect format: JS format starts with "Line Profile Results:" or has space-separated columns
const isJsFormat =
rawResults.includes("Line Profile Results:") ||
(rawResults.includes("Line") && rawResults.includes("Hits") && rawResults.includes("Content") && !rawResults.includes("|"))
(rawResults.includes("Line") &&
rawResults.includes("Hits") &&
rawResults.includes("Content") &&
!rawResults.includes("|"))
if (isJsFormat) {
return parseJsLineProfilerResults(rawResults)
@ -193,7 +196,11 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
// Detect table header - handle variable whitespace in cells
// Tabulate may produce "| Hits |" with extra spaces
if (trimmedLine.includes("Hits") && trimmedLine.includes("Line Contents") && trimmedLine.startsWith("|")) {
if (
trimmedLine.includes("Hits") &&
trimmedLine.includes("Line Contents") &&
trimmedLine.startsWith("|")
) {
inTable = true
headerPassed = false
continue
@ -210,7 +217,7 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
// Split by | but preserve the original line for extracting code with whitespace
const rawParts = trimmedLine.split("|")
// Trim stats columns but NOT the code column
const statParts = rawParts.slice(1, 5).map((p) => p.trim())
const statParts = rawParts.slice(1, 5).map(p => p.trim())
// Keep code with original whitespace - join remaining parts (code may contain pipes)
// Use " " for empty lines to preserve blank lines in display
const codePart = rawParts.slice(5, -1).join("|") || " "
@ -252,9 +259,7 @@ export const HEAT_THRESHOLDS = {
* Get the heat level for a given percent time
* Returns a class suffix for CSS styling
*/
export function getHeatLevel(
percentTime: number,
): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
export function getHeatLevel(percentTime: number): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
if (percentTime >= HEAT_THRESHOLDS.HOT_4) return "hot-4"
if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3"
if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2"

View file

@ -31,7 +31,7 @@ export async function getModifiedCodeForTrace(
}
const experimentMetadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null
const additionalMetadata = (optimizationFeature.metadata as any) || {}
const additionalMetadata = (optimizationFeature.metadata as Record<string, unknown>) || {}
// Get modified code from metadata if available
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
@ -99,7 +99,7 @@ export async function hasModifiedCode(traceId: string): Promise<boolean> {
return false
}
const additionalMetadata = (optimizationFeature.metadata as any) || {}
const additionalMetadata = (optimizationFeature.metadata as Record<string, unknown>) || {}
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
return !!(modifiedCode && Object.keys(modifiedCode).length > 0)

View file

@ -7,12 +7,7 @@ export function getCallSource(
eventType: string | null,
context: Record<string, unknown> | null,
): string {
if (
context &&
typeof context === "object" &&
!Array.isArray(context) &&
"source" in context
) {
if (context && typeof context === "object" && !Array.isArray(context) && "source" in context) {
return String(context.source)
}
if (eventType) {

View file

@ -1,12 +1,31 @@
import { prisma } from "@codeflash-ai/common"
import * as Sentry from "@sentry/nextjs"
interface PrismaQueryEvent {
timestamp: Date
query: string
params: string
duration: number
target: string
}
interface PrismaLogEvent {
timestamp: Date
message: string
target: string
}
interface PrismaClientWithEvents {
$on(event: "query", listener: (e: PrismaQueryEvent) => void): void
$on(event: "warn" | "error", listener: (e: PrismaLogEvent) => void): void
}
const isProduction = process.env.NODE_ENV === "production"
const SLOW_QUERY_THRESHOLD_MS = 500
// Log slow queries in development
if (!isProduction) {
;(prisma as any).$on("query", (e: any) => {
;(prisma as unknown as PrismaClientWithEvents).$on("query", (e: PrismaQueryEvent) => {
if (e.duration > SLOW_QUERY_THRESHOLD_MS) {
console.warn(`[Prisma] Slow query (${e.duration}ms): ${e.query}`)
}
@ -14,10 +33,10 @@ if (!isProduction) {
}
// Forward Prisma warnings and errors to Sentry
;(prisma as any).$on("warn", (e: any) => {
;(prisma as unknown as PrismaClientWithEvents).$on("warn", (e: PrismaLogEvent) => {
console.warn("[Prisma] Warning:", e.message)
})
;(prisma as any).$on("error", (e: any) => {
;(prisma as unknown as PrismaClientWithEvents).$on("error", (e: PrismaLogEvent) => {
console.error("[Prisma] Error:", e.message)
Sentry.captureException(new Error(`Prisma error: ${e.message}`))
})

View file

@ -0,0 +1,20 @@
import Redis from "ioredis"
let _redis: Redis | undefined
export function getRedis(): Redis {
if (!_redis) {
const url = process.env.REDIS_URL
if (!url) {
throw new Error("REDIS_URL environment variable is required")
}
_redis = new Redis(url, {
tls: { rejectUnauthorized: false },
maxRetriesPerRequest: 3,
retryStrategy(times) {
return Math.min(times * 200, 2000)
},
})
}
return _redis
}

View file

@ -23,7 +23,7 @@ export function withTiming<TArgs extends unknown[], TReturn>(
"server_action.name": actionName,
},
},
async (span) => {
async span => {
const res = await fn(...args)
const durationMs = performance.now() - start
@ -43,10 +43,7 @@ export function withTiming<TArgs extends unknown[], TReturn>(
return result
} catch (error) {
const durationMs = performance.now() - start
console.error(
`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`,
error,
)
console.error(`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`, error)
Sentry.captureException(error, {
tags: { server_action: actionName },
extra: { duration_ms: durationMs },

View file

@ -37,8 +37,10 @@ export class GithubService {
)
}
}
private mapGitHubUserSearchResult(githubUsers: any): GitHubUserSearchResult[] {
return githubUsers.map((user: any) => ({
private mapGitHubUserSearchResult(
githubUsers: { login: string; id: number; avatar_url: string }[],
): GitHubUserSearchResult[] {
return githubUsers.map(user => ({
username: user.login,
githubUserId: user.id,
avatarUrl: user.avatar_url,

View file

@ -32,7 +32,8 @@ export async function getRepositoriesForAccountCached(
return dedup(`repos:${cacheKey}`, async () => {
const repoIdsCacheKey = `${cacheKey}_ids`
const cachedRepos = await memoryCache.getItem<any>(cacheKey)
const cachedRepos =
await memoryCache.getItem<Awaited<ReturnType<typeof getRepositoriesForAccount>>>(cacheKey)
const cachedRepoIds = await memoryCache.getItem<string[]>(repoIdsCacheKey)
if (cachedRepos && cachedRepoIds) {
return { repoIds: cachedRepoIds, repos: cachedRepos }

View file

@ -30,14 +30,14 @@ SyntaxHighlighter.registerLanguage("markup", markup)
SyntaxHighlighter.registerLanguage("bash", bash)
SyntaxHighlighter.registerLanguage("jsx", jsx)
SyntaxHighlighter.registerLanguage("tsx", tsx)
const mockPlaintext = (prism: any) => {
const mockPlaintext = (prism: { languages: Record<string, Record<string, unknown>> }) => {
prism.languages.plaintext = {}
prism.languages.text = {}
}
mockPlaintext.displayName = "plaintext"
mockPlaintext.aliases = ["text"]
SyntaxHighlighter.registerLanguage("plaintext", mockPlaintext as any)
SyntaxHighlighter.registerLanguage("text", mockPlaintext as any)
SyntaxHighlighter.registerLanguage("plaintext", mockPlaintext as typeof python)
SyntaxHighlighter.registerLanguage("text", mockPlaintext as typeof python)
export { SyntaxHighlighter }

View file

@ -16,7 +16,13 @@ export interface PrCommentFields {
report_table?: Record<string, { failed: number; passed: number }>
function_name?: string // The Python function optimized
original_runtime?: string
benchmark_details?: any
benchmark_details?: {
benchmark_name: string
test_function: string
original_timing: number
expected_new_timing: number
speedup_percent: number
}[]
optimization_explanation?: string
}

View file

@ -57,7 +57,12 @@ vi.mock("@codeflash-ai/common", () => {
// Mock: @sentry/nextjs
// ---------------------------------------------------------------------------
vi.mock("@sentry/nextjs", () => ({
startSpan: vi.fn((_opts: any, callback: any) => callback({ setAttribute: vi.fn() })),
startSpan: vi.fn(
(
_opts: Record<string, unknown>,
callback: (span: { setAttribute: ReturnType<typeof vi.fn> }) => unknown,
) => callback({ setAttribute: vi.fn() }),
),
captureException: vi.fn(),
}))

View file

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -23,9 +19,7 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
@ -36,7 +30,5 @@
"*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

View file

@ -1,9 +1,10 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import path from "path"
import type { PluginOption } from "vite"
export default defineConfig({
plugins: [react()] as any,
plugins: [react()] as PluginOption[],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),

View file

@ -236,7 +236,7 @@ importers:
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@sentry/nextjs':
specifier: ^10.38.0
version: 10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1)
version: 10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1(esbuild@0.27.7))
'@sentry/opentelemetry':
specifier: ^10.47.0
version: 10.48.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)
@ -273,6 +273,9 @@ importers:
dompurify:
specifier: ^3.3.3
version: 3.3.3
ioredis:
specifier: ^5.10.1
version: 5.10.1
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.3
@ -361,6 +364,9 @@ importers:
'@next/bundle-analyzer':
specifier: ^16.2.2
version: 16.2.3
'@sentry/core':
specifier: ^10.48.0
version: 10.48.0
'@testing-library/react':
specifier: ^16.0.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@ -375,7 +381,7 @@ importers:
version: 5.5.2
'@vitejs/plugin-react':
specifier: ^4.3.1
version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 4.7.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
autoprefixer:
specifier: ^10.0.1
version: 10.4.27(postcss@8.5.9)
@ -409,9 +415,12 @@ importers:
typescript:
specifier: ^5.9.3
version: 5.9.3
vite:
specifier: ^8.0.8
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vitest:
specifier: ^4.1.4
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
optionalDependencies:
tree-sitter-cli:
specifier: ^0.26.3
@ -1300,6 +1309,9 @@ packages:
cpu: [x64]
os: [win32]
'@ioredis/commands@1.5.1':
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
'@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@ -1415,6 +1427,12 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@napi-rs/wasm-runtime@1.1.3':
resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@next/bundle-analyzer@16.2.3':
resolution: {integrity: sha512-aDwW4f4SVqbQDWzSBHQJ1KI6H+lx8oX/vS3xGqzLajUu+KQb7uakK88AIMvRIf7TlIonce67g594rzpxvBuJIw==}
@ -2204,6 +2222,9 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@oxc-project/types@0.124.0':
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
@ -2751,9 +2772,107 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@rolldown/binding-android-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
'@rolldown/pluginutils@1.0.0-rc.15':
resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
'@rollup/plugin-commonjs@28.0.1':
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
@ -4028,6 +4147,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -5143,6 +5266,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
ioredis@5.10.1:
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
engines: {node: '>=12.22.0'}
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
@ -5609,6 +5736,80 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lightningcss-android-arm64@1.32.0:
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.32.0:
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.32.0:
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.32.0:
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.32.0:
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.32.0:
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.32.0:
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.32.0:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@ -5640,9 +5841,15 @@ packages:
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
@ -6744,6 +6951,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rolldown@1.0.0-rc.15:
resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rollup@4.60.1:
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -6932,6 +7144,9 @@ packages:
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
engines: {node: '>=6'}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
standardwebhooks@1.0.0:
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
@ -7481,15 +7696,16 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite@7.3.2:
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
vite@8.0.8:
resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
@ -7500,12 +7716,14 @@ packages:
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
@ -8555,6 +8773,8 @@ snapshots:
'@img/sharp-win32-x64@0.34.5':
optional: true
'@ioredis/commands@1.5.1': {}
'@istanbuljs/load-nyc-config@1.1.0':
dependencies:
camelcase: 5.3.1
@ -8778,6 +8998,13 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
dependencies:
'@emnapi/core': 1.9.2
'@emnapi/runtime': 1.9.2
'@tybys/wasm-util': 0.10.1
optional: true
'@next/bundle-analyzer@16.2.3':
dependencies:
webpack-bundle-analyzer: 4.10.1
@ -9826,6 +10053,8 @@ snapshots:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1)
'@oxc-project/types@0.124.0': {}
'@panva/hkdf@1.2.1': {}
'@paralleldrive/cuid2@2.3.1':
@ -10382,8 +10611,59 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
'@rolldown/binding-android-arm64@1.0.0-rc.15':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
dependencies:
'@emnapi/core': 1.9.2
'@emnapi/runtime': 1.9.2
'@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
optional: true
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rolldown/pluginutils@1.0.0-rc.15': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.60.1)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
@ -10573,7 +10853,7 @@ snapshots:
'@sentry/core@10.48.0': {}
'@sentry/nextjs@10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1)':
'@sentry/nextjs@10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1(esbuild@0.27.7))':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.40.0
@ -10585,7 +10865,7 @@ snapshots:
'@sentry/opentelemetry': 10.48.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)
'@sentry/react': 10.48.0(react@19.2.5)
'@sentry/vercel-edge': 10.48.0
'@sentry/webpack-plugin': 5.2.0(webpack@5.106.1)
'@sentry/webpack-plugin': 5.2.0(webpack@5.106.1(esbuild@0.27.7))
next: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
rollup: 4.60.1
stacktrace-parser: 0.1.11
@ -10684,10 +10964,10 @@ snapshots:
'@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1)
'@sentry/core': 10.48.0
'@sentry/webpack-plugin@5.2.0(webpack@5.106.1)':
'@sentry/webpack-plugin@5.2.0(webpack@5.106.1(esbuild@0.27.7))':
dependencies:
'@sentry/bundler-plugin-core': 5.2.0
webpack: 5.106.1
webpack: 5.106.1(esbuild@0.27.7)
transitivePeerDependencies:
- encoding
- supports-color
@ -11280,7 +11560,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vitejs/plugin-react@4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
'@vitejs/plugin-react@4.7.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
@ -11288,7 +11568,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@ -11301,13 +11581,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
'@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
'@vitest/spy': 4.1.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@vitest/pretty-format@4.1.4':
dependencies:
@ -11895,6 +12175,8 @@ snapshots:
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
co@4.6.0: {}
collect-v8-coverage@1.0.3: {}
@ -12355,7 +12637,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.3
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
@ -12390,7 +12672,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -12405,13 +12687,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
transitivePeerDependencies:
- supports-color
@ -12442,7 +12724,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -13320,6 +13602,20 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
ioredis@5.10.1:
dependencies:
'@ioredis/commands': 1.5.1
cluster-key-slot: 1.1.2
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ip-address@10.1.0: {}
ipaddr.js@1.9.1: {}
@ -13986,6 +14282,55 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lightningcss-android-arm64@1.32.0:
optional: true
lightningcss-darwin-arm64@1.32.0:
optional: true
lightningcss-darwin-x64@1.32.0:
optional: true
lightningcss-freebsd-x64@1.32.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.32.0:
optional: true
lightningcss-linux-arm64-gnu@1.32.0:
optional: true
lightningcss-linux-arm64-musl@1.32.0:
optional: true
lightningcss-linux-x64-gnu@1.32.0:
optional: true
lightningcss-linux-x64-musl@1.32.0:
optional: true
lightningcss-win32-arm64-msvc@1.32.0:
optional: true
lightningcss-win32-x64-msvc@1.32.0:
optional: true
lightningcss@1.32.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.32.0
lightningcss-darwin-arm64: 1.32.0
lightningcss-darwin-x64: 1.32.0
lightningcss-freebsd-x64: 1.32.0
lightningcss-linux-arm-gnueabihf: 1.32.0
lightningcss-linux-arm64-gnu: 1.32.0
lightningcss-linux-arm64-musl: 1.32.0
lightningcss-linux-x64-gnu: 1.32.0
lightningcss-linux-x64-musl: 1.32.0
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@ -14020,8 +14365,12 @@ snapshots:
lodash.camelcase@4.3.0: {}
lodash.defaults@4.2.0: {}
lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
@ -15343,6 +15692,27 @@ snapshots:
dependencies:
glob: 7.2.3
rolldown@1.0.0-rc.15:
dependencies:
'@oxc-project/types': 0.124.0
'@rolldown/pluginutils': 1.0.0-rc.15
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.15
'@rolldown/binding-darwin-arm64': 1.0.0-rc.15
'@rolldown/binding-darwin-x64': 1.0.0-rc.15
'@rolldown/binding-freebsd-x64': 1.0.0-rc.15
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
rollup@4.60.1:
dependencies:
'@types/estree': 1.0.8
@ -15607,6 +15977,8 @@ snapshots:
dependencies:
type-fest: 0.7.1
standard-as-callback@2.1.0: {}
standardwebhooks@1.0.0:
dependencies:
'@stablelib/base64': 1.0.1
@ -15838,13 +16210,15 @@ snapshots:
tapable@2.3.2: {}
terser-webpack-plugin@5.4.0(webpack@5.106.1):
terser-webpack-plugin@5.4.0(esbuild@0.27.7)(webpack@5.106.1(esbuild@0.27.7)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.46.1
webpack: 5.106.1
webpack: 5.106.1(esbuild@0.27.7)
optionalDependencies:
esbuild: 0.27.7
terser@5.46.1:
dependencies:
@ -16228,26 +16602,26 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
dependencies:
esbuild: 0.27.7
fdir: 6.5.0(picomatch@4.0.4)
lightningcss: 1.32.0
picomatch: 4.0.4
postcss: 8.5.9
rollup: 4.60.1
rolldown: 1.0.0-rc.15
tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 25.6.0
esbuild: 0.27.7
fsevents: 2.3.3
jiti: 1.21.7
terser: 5.46.1
tsx: 4.21.0
yaml: 2.8.3
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/pretty-format': 4.1.4
'@vitest/runner': 4.1.4
'@vitest/snapshot': 4.1.4
@ -16264,7 +16638,7 @@ snapshots:
tinyexec: 1.1.1
tinyglobby: 0.2.16
tinyrainbow: 3.1.0
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
@ -16317,7 +16691,7 @@ snapshots:
webpack-sources@3.3.4: {}
webpack@5.106.1:
webpack@5.106.1(esbuild@0.27.7):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
@ -16341,7 +16715,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.2
terser-webpack-plugin: 5.4.0(webpack@5.106.1)
terser-webpack-plugin: 5.4.0(esbuild@0.27.7)(webpack@5.106.1(esbuild@0.27.7))
watchpack: 2.5.1
webpack-sources: 3.3.4
transitivePeerDependencies: