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: build:
env: env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write

View file

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

View file

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

View file

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

View file

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

View file

@ -1,44 +1,27 @@
import { NextRequest, NextResponse } from "next/server" import { NextRequest, NextResponse } from "next/server"
import { exchangeCodeForToken } from "../../action" import { exchangeCodeForToken } from "../../action"
export async function POST(request: NextRequest) { const ALLOWED_CLIENT_IDS = new Set(["cf_vscode_app", "cf-cli-app"])
console.log("=== Token Exchange Request Started ===")
export async function POST(request: NextRequest) {
try { try {
const body = await request.json() 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 const { grant_type, code, redirect_uri, code_verifier, client_id } = body
// Validate grant type
if (grant_type !== "authorization_code") { if (grant_type !== "authorization_code") {
console.error("Invalid grant type:", grant_type)
return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 }) return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 })
} }
// Validate required parameters
if (!code || !redirect_uri || !code_verifier || !client_id) { 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( return NextResponse.json(
{ error: "invalid_request", error_description: "Missing required parameters" }, { error: "invalid_request", error_description: "Missing required parameters" },
{ status: 400 }, { 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({ const result = await exchangeCodeForToken({
code, code,
@ -48,26 +31,17 @@ export async function POST(request: NextRequest) {
}) })
if (result.error) { if (result.error) {
console.error("Token exchange failed:", result.error)
return NextResponse.json( return NextResponse.json(
{ error: "invalid_grant", error_description: result.error }, { error: "invalid_grant", error_description: result.error },
{ status: 400 }, { status: 400 },
) )
} }
console.log("=== Token Exchange Request Completed Successfully ===")
return NextResponse.json({ return NextResponse.json({
access_token: result.accessToken, access_token: result.accessToken,
token_type: "Bearer", token_type: "Bearer",
}) })
} catch (error) { } catch {
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)
return NextResponse.json( return NextResponse.json(
{ error: "server_error", error_description: "Internal server error" }, { error: "server_error", error_description: "Internal server error" },
{ status: 500 }, { status: 500 },

View file

@ -11,7 +11,7 @@ import { auth0 } from "@/lib/auth0"
export async function upsertReferralSource( export async function upsertReferralSource(
referralSource: string, referralSource: string,
additionalComments?: string, additionalComments?: string,
): Promise<any> { ): Promise<void> {
const session = await auth0.getSession() const session = await auth0.getSession()
if (session != null) { if (session != null) {
setUserReferralData(session.user.sub, referralSource, additionalComments) 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", "flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
ownerType === "personal" ownerType === "personal"
? "border-primary bg-primary/10 text-primary" ? "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" /> <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", "flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
ownerType === "organization" ownerType === "organization"
? "border-primary bg-primary/10 text-primary" ? "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" /> <Building2 className="h-4 w-4" />

View file

@ -56,9 +56,9 @@ export async function reactivateSubscription(userId: string) {
try { try {
await reactivateSubscriptionFromCommon(userId) await reactivateSubscriptionFromCommon(userId)
return { success: true } return { success: true }
} catch (error: any) { } catch (error: unknown) {
console.error("Error reactivating subscription:", error) console.error("Error reactivating subscription:", error)
Sentry.captureException(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" import { getActionAccountContext } from "@/lib/server/get-account-context"
vi.mock("@/lib/server-action-timing", () => ({ 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", () => ({ vi.mock("@/lib/analytics/tracking", () => ({
@ -59,8 +59,12 @@ describe("getOrganizationMembers", () => {
describe("successful retrieval", () => { describe("successful retrieval", () => {
it("returns members when user has access", async () => { it("returns members when user has access", async () => {
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any) vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any) 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 result = await getOrganizationMembers("org-1")
@ -69,8 +73,12 @@ describe("getOrganizationMembers", () => {
}) })
it("maps nested organization_members to flat Member structure", async () => { it("maps nested organization_members to flat Member structure", async () => {
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any) vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any) 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 result = await getOrganizationMembers("org-1")
const member = result.data![0] const member = result.data![0]
@ -115,7 +123,9 @@ describe("getOrganizationMembers", () => {
userId: "unknown-user", userId: "unknown-user",
username: "testuser", 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) vi.mocked(prisma.organization_members.findUnique).mockResolvedValue(null)
const result = await getOrganizationMembers("org-1") const result = await getOrganizationMembers("org-1")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -112,7 +112,9 @@ export const CodeContextSection = memo(function CodeContextSection({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setIsExpanded(!isExpanded)} 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" 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"> <div className="flex items-center gap-2">
@ -134,7 +136,9 @@ export const CodeContextSection = memo(function CodeContextSection({
{metrics.totalFiles} files {metrics.totalFiles} files
</span> </span>
</div> </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>
</div> </div>
@ -153,7 +157,10 @@ export const CodeContextSection = memo(function CodeContextSection({
{filePath && ( {filePath && (
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">File</span> <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} {filePath}
</code> </code>
</div> </div>
@ -242,7 +249,11 @@ interface CodeGroupSectionProps {
sectionKey: string 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) { switch (accentColor) {
case "emerald": case "emerald":
return { return {
@ -280,7 +291,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={onToggle} 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}`} 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"> <div className="flex items-center gap-3">
@ -292,9 +305,12 @@ const CodeGroupSection = memo(function CodeGroupSection({
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xs text-zinc-500 dark:text-zinc-400"> <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> </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>
</div> </div>
@ -310,7 +326,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => onToggleFile(fileKey)} 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" className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1"
> >
<FileText className="h-3.5 w-3.5 text-zinc-400" /> <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}> <span className="text-xs text-zinc-500 dark:text-zinc-400" title={file.path}>
{file.path !== file.filename && `(${file.path})`} {file.path !== file.filename && `(${file.path})`}
</span> </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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-zinc-500 dark:text-zinc-400"> <span className="text-xs text-zinc-500 dark:text-zinc-400">
@ -378,4 +398,4 @@ const TokenDistributionBar = memo(function TokenDistributionBar({
</div> </div>
</div> </div>
) )
}) })

View file

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

View file

@ -1,12 +1,7 @@
"use client" "use client"
import { useState, useCallback, memo } from "react" import { useState, useCallback, memo } from "react"
import { import { XCircle, AlertCircle, AlertTriangle, ChevronDown } from "lucide-react"
XCircle,
AlertCircle,
AlertTriangle,
ChevronDown,
} from "lucide-react"
import { CopyButton } from "./copy-button" import { CopyButton } from "./copy-button"
interface ErrorContext { interface ErrorContext {
@ -89,7 +84,9 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => toggleError(error.id)} 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" 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" /> <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> </p>
</div> </div>
{(hasContext || isTestFailure) && ( {(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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -201,4 +200,4 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
</div> </div>
</div> </div>
) )
}) })

View file

@ -132,7 +132,9 @@ export function formatTimelineForLLM(input: LLMExportInput): string {
lines.push("## Errors") lines.push("## Errors")
lines.push("") lines.push("")
for (const error of errors) { 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.error_message) lines.push(error.error_message)
if (error.context) { if (error.context) {
const ctx = 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 totalCost = sections.reduce((sum, s) => sum + (s.cost ?? 0), 0)
const totalTokens = sections.reduce((sum, s) => sum + (s.tokens ?? 0), 0) const totalTokens = sections.reduce((sum, s) => sum + (s.tokens ?? 0), 0)
const candidatesCount = sections.filter( 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 ).length
lines.push("---") lines.push("---")

View file

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

View file

@ -6,9 +6,14 @@ export { transformToTimelineSections } from "./timeline-types"
export { ErrorsSection } from "./errors-section" export { ErrorsSection } from "./errors-section"
export { FunctionToOptimizeSection } from "./function-to-optimize-section" export { FunctionToOptimizeSection } from "./function-to-optimize-section"
export { CodeContextSection } from "./code-context-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 { findFunctionInCode } from "./python-parser"
export type { FunctionLocation } from "./python-parser" export type { FunctionLocation } from "./python-parser"
export { CopyButton } from "./copy-button" export { CopyButton } from "./copy-button"
export { InfoIcon } from "./info-icon" export { InfoIcon } from "./info-icon"
export { getTraceSource } from "./utils" export { getTraceSource } from "./utils"

View file

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

View file

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

View file

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

View file

@ -1,17 +1,11 @@
"use client" "use client"
import { useState, memo } from "react" import { useState, memo } from "react"
import { import { ChevronDown, FlaskConical, CheckCircle2 } from "lucide-react"
ChevronDown,
FlaskConical,
CheckCircle2,
} from "lucide-react"
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter" import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
import type { TimelineSectionContent } from "./timeline-types" import type { TimelineSectionContent } from "./timeline-types"
function getEmptyMessage( function getEmptyMessage(variant: "generated" | "instrumented" | "instrumentedPerf"): string {
variant: "generated" | "instrumented" | "instrumentedPerf"
): string {
switch (variant) { switch (variant) {
case "generated": case "generated":
return "No generated test available" return "No generated test available"
@ -31,7 +25,9 @@ interface TestContentProps {
export const TestContent = memo(function TestContent({ content }: TestContentProps) { export const TestContent = memo(function TestContent({ content }: TestContentProps) {
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
const [expandedTest, setExpandedTest] = useState<number | null>(null) 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 testCount = content.testGroups.length
const hasInstrumented = content.testGroups.some(g => g.instrumented) 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" 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"} {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> </button>
</div> </div>
{showDetails && ( {showDetails && (
<div className="space-y-3 pt-2 border-t border-zinc-200 dark:border-zinc-700"> <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 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 = (() => { const currentCode = (() => {
switch (activeVariant) { switch (activeVariant) {
@ -94,7 +94,10 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
})() })()
return ( 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 <button
onClick={() => setExpandedTest(isExpanded ? null : group.index)} 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" 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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-zinc-500"> <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> </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>
</button> </button>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,7 +41,14 @@ interface StatCardProps {
className?: string 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 const IconComponent = icon ? iconMap[icon] : null
return ( 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 items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-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"> <div className="text-sm text-gray-600 dark:text-gray-400 font-medium flex items-center gap-1.5">
{label} {label}
{helpText && <InfoIcon content={helpText} side="top" />} {helpText && <InfoIcon content={helpText} side="top" />}
</div> </div>
</div> </div>
<div className="text-2xl font-bold text-gray-900 dark:text-white"> <div className="text-2xl font-bold text-gray-900 dark:text-white">{value}</div>
{value}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -610,7 +610,8 @@ export function RenderStepContent({
</h4> </h4>
<ApiKeyCard label="GitHub Actions Setup" value="codeflash init-actions" /> <ApiKeyCard label="GitHub Actions Setup" value="codeflash init-actions" />
<p className="mt-2 text-sm text-muted-foreground font-medium"> <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> </p>
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
export type ActionResponse<T = any> = { export type ActionResponse<T = unknown> = {
success: boolean success: boolean
data?: T data?: T
error?: string 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 { return {
success: false, success: false,
data: undefined, data: undefined,

View file

@ -25,9 +25,7 @@ let _auth0: Auth0Client | undefined
function getAuth0Client(): Auth0Client { function getAuth0Client(): Auth0Client {
if (!_auth0) { if (!_auth0) {
if (!auth0Domain) { if (!auth0Domain) {
// During build/CI prerendering, Auth0 env vars aren't set. throw new Error("AUTH0_DOMAIN (or AUTH0_ISSUER_BASE_URL) environment variable is required")
// Return a stub — getSession() returning null is correct (no user).
return { getSession: async () => null } as unknown as Auth0Client
} }
_auth0 = new Auth0Client({ _auth0 = new Auth0Client({
domain: auth0Domain, domain: auth0Domain,
@ -64,7 +62,7 @@ function getAuth0Client(): Auth0Client {
const match = errorMessage.match(re) const match = errorMessage.match(re)
if (match != null) { if (match != null) {
const userNickname = match[2] 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, { export const auth0: Auth0Client = new Proxy({} as Auth0Client, {
get(_, prop) { get(_, prop) {
const client = getAuth0Client() const client = getAuth0Client()
const value = (client as any)[prop] const value = client[prop as keyof Auth0Client]
return typeof value === "function" ? value.bind(client) : value 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 // Detect format: JS format starts with "Line Profile Results:" or has space-separated columns
const isJsFormat = const isJsFormat =
rawResults.includes("Line Profile Results:") || 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) { if (isJsFormat) {
return parseJsLineProfilerResults(rawResults) return parseJsLineProfilerResults(rawResults)
@ -193,7 +196,11 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
// Detect table header - handle variable whitespace in cells // Detect table header - handle variable whitespace in cells
// Tabulate may produce "| Hits |" with extra spaces // 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 inTable = true
headerPassed = false headerPassed = false
continue continue
@ -210,7 +217,7 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
// Split by | but preserve the original line for extracting code with whitespace // Split by | but preserve the original line for extracting code with whitespace
const rawParts = trimmedLine.split("|") const rawParts = trimmedLine.split("|")
// Trim stats columns but NOT the code column // 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) // Keep code with original whitespace - join remaining parts (code may contain pipes)
// Use " " for empty lines to preserve blank lines in display // Use " " for empty lines to preserve blank lines in display
const codePart = rawParts.slice(5, -1).join("|") || " " const codePart = rawParts.slice(5, -1).join("|") || " "
@ -252,9 +259,7 @@ export const HEAT_THRESHOLDS = {
* Get the heat level for a given percent time * Get the heat level for a given percent time
* Returns a class suffix for CSS styling * Returns a class suffix for CSS styling
*/ */
export function getHeatLevel( export function getHeatLevel(percentTime: number): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
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_4) return "hot-4"
if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3" if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3"
if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2" 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 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 // Get modified code from metadata if available
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
@ -99,7 +99,7 @@ export async function hasModifiedCode(traceId: string): Promise<boolean> {
return false 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 const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
return !!(modifiedCode && Object.keys(modifiedCode).length > 0) return !!(modifiedCode && Object.keys(modifiedCode).length > 0)

View file

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

View file

@ -1,12 +1,31 @@
import { prisma } from "@codeflash-ai/common" import { prisma } from "@codeflash-ai/common"
import * as Sentry from "@sentry/nextjs" 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 isProduction = process.env.NODE_ENV === "production"
const SLOW_QUERY_THRESHOLD_MS = 500 const SLOW_QUERY_THRESHOLD_MS = 500
// Log slow queries in development // Log slow queries in development
if (!isProduction) { 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) { if (e.duration > SLOW_QUERY_THRESHOLD_MS) {
console.warn(`[Prisma] Slow query (${e.duration}ms): ${e.query}`) console.warn(`[Prisma] Slow query (${e.duration}ms): ${e.query}`)
} }
@ -14,10 +33,10 @@ if (!isProduction) {
} }
// Forward Prisma warnings and errors to Sentry // 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) 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) console.error("[Prisma] Error:", e.message)
Sentry.captureException(new 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, "server_action.name": actionName,
}, },
}, },
async (span) => { async span => {
const res = await fn(...args) const res = await fn(...args)
const durationMs = performance.now() - start const durationMs = performance.now() - start
@ -43,10 +43,7 @@ export function withTiming<TArgs extends unknown[], TReturn>(
return result return result
} catch (error) { } catch (error) {
const durationMs = performance.now() - start const durationMs = performance.now() - start
console.error( console.error(`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`, error)
`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`,
error,
)
Sentry.captureException(error, { Sentry.captureException(error, {
tags: { server_action: actionName }, tags: { server_action: actionName },
extra: { duration_ms: durationMs }, extra: { duration_ms: durationMs },

View file

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

View file

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

View file

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

View file

@ -16,7 +16,13 @@ export interface PrCommentFields {
report_table?: Record<string, { failed: number; passed: number }> report_table?: Record<string, { failed: number; passed: number }>
function_name?: string // The Python function optimized function_name?: string // The Python function optimized
original_runtime?: string 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 optimization_explanation?: string
} }

View file

@ -57,7 +57,12 @@ vi.mock("@codeflash-ai/common", () => {
// Mock: @sentry/nextjs // Mock: @sentry/nextjs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
vi.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(), captureException: vi.fn(),
})) }))

View file

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

View file

@ -1,9 +1,10 @@
import { defineConfig } from "vitest/config" import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react" import react from "@vitejs/plugin-react"
import path from "path" import path from "path"
import type { PluginOption } from "vite"
export default defineConfig({ export default defineConfig({
plugins: [react()] as any, plugins: [react()] as PluginOption[],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": 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) 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': '@sentry/nextjs':
specifier: ^10.38.0 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': '@sentry/opentelemetry':
specifier: ^10.47.0 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) 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: dompurify:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
ioredis:
specifier: ^5.10.1
version: 5.10.1
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.3 version: 9.0.3
@ -361,6 +364,9 @@ importers:
'@next/bundle-analyzer': '@next/bundle-analyzer':
specifier: ^16.2.2 specifier: ^16.2.2
version: 16.2.3 version: 16.2.3
'@sentry/core':
specifier: ^10.48.0
version: 10.48.0
'@testing-library/react': '@testing-library/react':
specifier: ^16.0.0 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) 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 version: 5.5.2
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.3.1 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: autoprefixer:
specifier: ^10.0.1 specifier: ^10.0.1
version: 10.4.27(postcss@8.5.9) version: 10.4.27(postcss@8.5.9)
@ -409,9 +415,12 @@ importers:
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 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: vitest:
specifier: ^4.1.4 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: optionalDependencies:
tree-sitter-cli: tree-sitter-cli:
specifier: ^0.26.3 specifier: ^0.26.3
@ -1300,6 +1309,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@ioredis/commands@1.5.1':
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
'@istanbuljs/load-nyc-config@1.1.0': '@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1415,6 +1427,12 @@ packages:
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} 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': '@next/bundle-analyzer@16.2.3':
resolution: {integrity: sha512-aDwW4f4SVqbQDWzSBHQJ1KI6H+lx8oX/vS3xGqzLajUu+KQb7uakK88AIMvRIf7TlIonce67g594rzpxvBuJIw==} resolution: {integrity: sha512-aDwW4f4SVqbQDWzSBHQJ1KI6H+lx8oX/vS3xGqzLajUu+KQb7uakK88AIMvRIf7TlIonce67g594rzpxvBuJIw==}
@ -2204,6 +2222,9 @@ packages:
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.1.0 '@opentelemetry/api': ^1.1.0
'@oxc-project/types@0.124.0':
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
'@panva/hkdf@1.2.1': '@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
@ -2751,9 +2772,107 @@ packages:
'@radix-ui/rect@1.1.1': '@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} 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': '@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
'@rolldown/pluginutils@1.0.0-rc.15':
resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
'@rollup/plugin-commonjs@28.0.1': '@rollup/plugin-commonjs@28.0.1':
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'} engines: {node: '>=16.0.0 || 14 >= 14.17'}
@ -4028,6 +4147,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} 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: co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -5143,6 +5266,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
ioredis@5.10.1:
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
engines: {node: '>=12.22.0'}
ip-address@10.1.0: ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@ -5609,6 +5736,80 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} 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: lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -5640,9 +5841,15 @@ packages:
lodash.camelcase@4.3.0: lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} 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: lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isboolean@3.0.3: lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
@ -6744,6 +6951,11 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true 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: rollup@4.60.1:
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -6932,6 +7144,9 @@ packages:
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
engines: {node: '>=6'} engines: {node: '>=6'}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
standardwebhooks@1.0.0: standardwebhooks@1.0.0:
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
@ -7481,15 +7696,16 @@ packages:
vfile@6.0.3: vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite@7.3.2: vite@8.0.8:
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0 '@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0' jiti: '>=1.21.0'
less: ^4.0.0 less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0 sass: ^1.70.0
sass-embedded: ^1.70.0 sass-embedded: ^1.70.0
stylus: '>=0.54.8' stylus: '>=0.54.8'
@ -7500,12 +7716,14 @@ packages:
peerDependenciesMeta: peerDependenciesMeta:
'@types/node': '@types/node':
optional: true optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti: jiti:
optional: true optional: true
less: less:
optional: true optional: true
lightningcss:
optional: true
sass: sass:
optional: true optional: true
sass-embedded: sass-embedded:
@ -8555,6 +8773,8 @@ snapshots:
'@img/sharp-win32-x64@0.34.5': '@img/sharp-win32-x64@0.34.5':
optional: true optional: true
'@ioredis/commands@1.5.1': {}
'@istanbuljs/load-nyc-config@1.1.0': '@istanbuljs/load-nyc-config@1.1.0':
dependencies: dependencies:
camelcase: 5.3.1 camelcase: 5.3.1
@ -8778,6 +8998,13 @@ snapshots:
'@tybys/wasm-util': 0.10.1 '@tybys/wasm-util': 0.10.1
optional: true 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': '@next/bundle-analyzer@16.2.3':
dependencies: dependencies:
webpack-bundle-analyzer: 4.10.1 webpack-bundle-analyzer: 4.10.1
@ -9826,6 +10053,8 @@ snapshots:
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.6.1(@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': {} '@panva/hkdf@1.2.1': {}
'@paralleldrive/cuid2@2.3.1': '@paralleldrive/cuid2@2.3.1':
@ -10382,8 +10611,59 @@ snapshots:
'@radix-ui/rect@1.1.1': {} '@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-beta.27': {}
'@rolldown/pluginutils@1.0.0-rc.15': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.60.1)': '@rollup/plugin-commonjs@28.0.1(rollup@4.60.1)':
dependencies: dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1) '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
@ -10573,7 +10853,7 @@ snapshots:
'@sentry/core@10.48.0': {} '@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: dependencies:
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.40.0 '@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/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/react': 10.48.0(react@19.2.5)
'@sentry/vercel-edge': 10.48.0 '@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) 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 rollup: 4.60.1
stacktrace-parser: 0.1.11 stacktrace-parser: 0.1.11
@ -10684,10 +10964,10 @@ snapshots:
'@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1)
'@sentry/core': 10.48.0 '@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: dependencies:
'@sentry/bundler-plugin-core': 5.2.0 '@sentry/bundler-plugin-core': 5.2.0
webpack: 5.106.1 webpack: 5.106.1(esbuild@0.27.7)
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
@ -11280,7 +11560,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1': '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true 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: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@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 '@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
react-refresh: 0.17.0 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: transitivePeerDependencies:
- supports-color - supports-color
@ -11301,13 +11581,13 @@ snapshots:
chai: 6.2.2 chai: 6.2.2
tinyrainbow: 3.1.0 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: dependencies:
'@vitest/spy': 4.1.4 '@vitest/spy': 4.1.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: 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': '@vitest/pretty-format@4.1.4':
dependencies: dependencies:
@ -11895,6 +12175,8 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
co@4.6.0: {} co@4.6.0: {}
collect-v8-coverage@1.0.3: {} collect-v8-coverage@1.0.3: {}
@ -12355,7 +12637,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.3 '@next/eslint-plugin-next': 16.2.3
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 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-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-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)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
@ -12390,7 +12672,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@ -12405,13 +12687,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 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: transitivePeerDependencies:
- supports-color - supports-color
@ -12442,7 +12724,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4(jiti@1.21.7) eslint: 9.39.4(jiti@1.21.7)
eslint-import-resolver-node: 0.3.10 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 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@ -13320,6 +13602,20 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
side-channel: 1.1.0 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: {} ip-address@10.1.0: {}
ipaddr.js@1.9.1: {} ipaddr.js@1.9.1: {}
@ -13986,6 +14282,55 @@ snapshots:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
type-check: 0.4.0 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: {} lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
@ -14020,8 +14365,12 @@ snapshots:
lodash.camelcase@4.3.0: {} lodash.camelcase@4.3.0: {}
lodash.defaults@4.2.0: {}
lodash.includes@4.3.0: {} lodash.includes@4.3.0: {}
lodash.isarguments@3.1.0: {}
lodash.isboolean@3.0.3: {} lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {} lodash.isinteger@4.0.4: {}
@ -15343,6 +15692,27 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 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: rollup@4.60.1:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -15607,6 +15977,8 @@ snapshots:
dependencies: dependencies:
type-fest: 0.7.1 type-fest: 0.7.1
standard-as-callback@2.1.0: {}
standardwebhooks@1.0.0: standardwebhooks@1.0.0:
dependencies: dependencies:
'@stablelib/base64': 1.0.1 '@stablelib/base64': 1.0.1
@ -15838,13 +16210,15 @@ snapshots:
tapable@2.3.2: {} 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: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1 jest-worker: 27.5.1
schema-utils: 4.3.3 schema-utils: 4.3.3
terser: 5.46.1 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: terser@5.46.1:
dependencies: dependencies:
@ -16228,26 +16602,26 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.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: dependencies:
esbuild: 0.27.7 lightningcss: 1.32.0
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
postcss: 8.5.9 postcss: 8.5.9
rollup: 4.60.1 rolldown: 1.0.0-rc.15
tinyglobby: 0.2.16 tinyglobby: 0.2.16
optionalDependencies: optionalDependencies:
'@types/node': 25.6.0 '@types/node': 25.6.0
esbuild: 0.27.7
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7 jiti: 1.21.7
terser: 5.46.1 terser: 5.46.1
tsx: 4.21.0 tsx: 4.21.0
yaml: 2.8.3 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: dependencies:
'@vitest/expect': 4.1.4 '@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/pretty-format': 4.1.4
'@vitest/runner': 4.1.4 '@vitest/runner': 4.1.4
'@vitest/snapshot': 4.1.4 '@vitest/snapshot': 4.1.4
@ -16264,7 +16638,7 @@ snapshots:
tinyexec: 1.1.1 tinyexec: 1.1.1
tinyglobby: 0.2.16 tinyglobby: 0.2.16
tinyrainbow: 3.1.0 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 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.1 '@opentelemetry/api': 1.9.1
@ -16317,7 +16691,7 @@ snapshots:
webpack-sources@3.3.4: {} webpack-sources@3.3.4: {}
webpack@5.106.1: webpack@5.106.1(esbuild@0.27.7):
dependencies: dependencies:
'@types/eslint-scope': 3.7.7 '@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -16341,7 +16715,7 @@ snapshots:
neo-async: 2.6.2 neo-async: 2.6.2
schema-utils: 4.3.3 schema-utils: 4.3.3
tapable: 2.3.2 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 watchpack: 2.5.1
webpack-sources: 3.3.4 webpack-sources: 3.3.4
transitivePeerDependencies: transitivePeerDependencies: