mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
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:
parent
e6cec80c9d
commit
e5374c3f50
69 changed files with 1198 additions and 635 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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("---")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ${
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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. "Why was candidate 1 ranked best?" or "What optimizations were attempted?"
|
e.g. "Why was candidate 1 ranked best?" or "What optimizations were
|
||||||
|
attempted?"
|
||||||
</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>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,4 @@ export function getTraceSource(eventType: string | null): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventType
|
return eventType
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}`))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
20
js/cf-webapp/src/lib/redis.ts
Normal file
20
js/cf-webapp/src/lib/redis.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue