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:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||
AUTH0_ISSUER_BASE_URL: ${{ secrets.AUTH0_ISSUER_BASE_URL }}
|
||||
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
|
||||
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
|
||||
AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }}
|
||||
AUTH0_BASE_URL: ${{ secrets.AUTH0_BASE_URL }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ STRIPE_WEBHOOK_SECRET=
|
|||
API_TOKEN_LIMIT=4000
|
||||
JWT_SECRET=
|
||||
|
||||
# Redis (Azure Cache for Redis — used for rate limiting and JTI tracking)
|
||||
REDIS_URL=
|
||||
|
||||
# Sentry (omit NEXT_PUBLIC_SENTRY_DISABLED to enable)
|
||||
NEXT_PUBLIC_SENTRY_DISABLED=true
|
||||
# SENTRY_AUTH_TOKEN= # set in CI for source map uploads
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
"date-fns": "^4.1.0",
|
||||
"diff": "^8.0.2",
|
||||
"dompurify": "^3.3.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^1.8.0",
|
||||
"marked": "^18.0.0",
|
||||
|
|
@ -83,6 +84,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^16.2.2",
|
||||
"@sentry/core": "^10.48.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
|
|
@ -99,6 +101,7 @@
|
|||
"prisma": "^7.7.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ import { getUserOrganizations } from "@/components/dashboard/action"
|
|||
import { getUserId } from "@/app/utils/auth"
|
||||
import crypto from "crypto"
|
||||
import jwt from "jsonwebtoken"
|
||||
import { CacheContainer } from "node-ts-cache"
|
||||
import { MemoryStorage } from "node-ts-cache-storage-memory"
|
||||
import { organizationMemberRepository } from "@codeflash-ai/common"
|
||||
import { cookies } from "next/headers"
|
||||
import { organizationMemberRepository } from "@codeflash-ai/common"
|
||||
import { getRedis } from "@/lib/redis"
|
||||
|
||||
const RATE_LIMIT = 5
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000
|
||||
const rateLimitCache = new CacheContainer(new MemoryStorage())
|
||||
const RATE_LIMIT_WINDOW_SECONDS = 60
|
||||
|
||||
function getJwtSecret(): string {
|
||||
const secret = process.env.JWT_SECRET
|
||||
if (!secret) {
|
||||
|
|
@ -19,7 +18,6 @@ function getJwtSecret(): string {
|
|||
}
|
||||
return secret
|
||||
}
|
||||
const JWT_SECRET: string = getJwtSecret()
|
||||
|
||||
interface OAuthStatePayload {
|
||||
userId: string
|
||||
|
|
@ -111,22 +109,46 @@ interface OAuthParams {
|
|||
vscodeState: string
|
||||
}
|
||||
|
||||
const ALLOWED_CODE_CHALLENGE_METHODS = new Set(["S256", "sha256"])
|
||||
|
||||
const ALLOWED_CLIENT_IDS = new Set(["cf_vscode_app", "cf-cli-app"])
|
||||
|
||||
const ALLOWED_REDIRECT_URI_PATTERNS = [
|
||||
/^vscode:\/\/codeflash\.codeflash\//,
|
||||
/^http:\/\/localhost(:\d+)?\//, // local dev callbacks
|
||||
]
|
||||
|
||||
function isAllowedRedirectUri(uri: string): boolean {
|
||||
return ALLOWED_REDIRECT_URI_PATTERNS.some(pattern => pattern.test(uri))
|
||||
}
|
||||
|
||||
export async function storeOAuthParams(params: OAuthParams): Promise<{ error?: string }> {
|
||||
try {
|
||||
if (!ALLOWED_CODE_CHALLENGE_METHODS.has(params.codeChallengeMethod)) {
|
||||
return { error: "Invalid code challenge method" }
|
||||
}
|
||||
if (!ALLOWED_CLIENT_IDS.has(params.clientId)) {
|
||||
return { error: "Invalid client application" }
|
||||
}
|
||||
if (!isAllowedRedirectUri(params.redirectUri)) {
|
||||
return { error: "Invalid redirect URI" }
|
||||
}
|
||||
|
||||
const userId = await getUserId()
|
||||
if (!userId) {
|
||||
return { error: "Unauthorized" }
|
||||
}
|
||||
|
||||
const signed = jwt.sign({ ...params, type: "oauth_params" }, JWT_SECRET, {
|
||||
const signed = jwt.sign({ ...params, type: "oauth_params" }, getJwtSecret(), {
|
||||
expiresIn: "10m",
|
||||
algorithm: "HS256",
|
||||
})
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set(OAUTH_COOKIE_NAME, signed, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
secure: process.env.NODE_ENV !== "development",
|
||||
path: "/codeflash/auth",
|
||||
maxAge: 600,
|
||||
})
|
||||
|
|
@ -148,7 +170,9 @@ export async function getStoredOAuthParams(): Promise<{
|
|||
return { error: "Session expired. Please refresh the page and try again." }
|
||||
}
|
||||
|
||||
const payload = jwt.verify(cookie.value, JWT_SECRET) as unknown as OAuthParams & {
|
||||
const payload = jwt.verify(cookie.value, getJwtSecret(), {
|
||||
algorithms: ["HS256"],
|
||||
}) as unknown as OAuthParams & {
|
||||
type: string
|
||||
}
|
||||
if (payload.type !== "oauth_params") {
|
||||
|
|
@ -175,29 +199,21 @@ async function clearOAuthCookie() {
|
|||
}
|
||||
|
||||
export async function isRateLimited(userId: string): Promise<boolean> {
|
||||
const cacheKey = `rate_limit_vsc_signin_${userId}`
|
||||
const record = await rateLimitCache.getItem<{ count: number; startTime: number }>(cacheKey)
|
||||
const now = Date.now()
|
||||
const redis = getRedis()
|
||||
const key = `rate_limit:vsc_signin:${userId}`
|
||||
const pipeline = redis.pipeline()
|
||||
pipeline.incr(key)
|
||||
pipeline.expire(key, RATE_LIMIT_WINDOW_SECONDS)
|
||||
const results = await pipeline.exec()
|
||||
const count = (results?.[0]?.[1] as number) ?? 1
|
||||
return count > RATE_LIMIT
|
||||
}
|
||||
|
||||
if (!record || now - record.startTime > RATE_LIMIT_WINDOW_MS) {
|
||||
await rateLimitCache.setItem(
|
||||
cacheKey,
|
||||
{ count: 1, startTime: now },
|
||||
{ ttl: RATE_LIMIT_WINDOW_MS / 1000 },
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (record.count >= RATE_LIMIT) {
|
||||
return true
|
||||
}
|
||||
|
||||
record.count++
|
||||
await rateLimitCache.setItem(cacheKey, record, {
|
||||
ttl: (RATE_LIMIT_WINDOW_MS - (now - record.startTime)) / 1000,
|
||||
})
|
||||
|
||||
return false
|
||||
async function markJtiUsed(jti: string, ttlSeconds: number): Promise<boolean> {
|
||||
const redis = getRedis()
|
||||
const key = `jti:${jti}`
|
||||
const wasSet = await redis.set(key, "1", "EX", ttlSeconds, "NX")
|
||||
return wasSet === "OK"
|
||||
}
|
||||
|
||||
export async function createOAuthState(params: {
|
||||
|
|
@ -234,8 +250,9 @@ export async function createOAuthState(params: {
|
|||
type: "oauth_state",
|
||||
}
|
||||
|
||||
const state = jwt.sign(statePayload, JWT_SECRET, {
|
||||
const state = jwt.sign(statePayload, getJwtSecret(), {
|
||||
expiresIn: "2m",
|
||||
algorithm: "HS256",
|
||||
jwtid: crypto.randomBytes(16).toString("hex"),
|
||||
})
|
||||
|
||||
|
|
@ -258,7 +275,9 @@ export async function authorizeOAuth(state: string): Promise<{
|
|||
|
||||
let oauthState: OAuthStatePayload
|
||||
try {
|
||||
oauthState = jwt.verify(state, JWT_SECRET) as unknown as OAuthStatePayload
|
||||
oauthState = jwt.verify(state, getJwtSecret(), {
|
||||
algorithms: ["HS256"],
|
||||
}) as unknown as OAuthStatePayload
|
||||
} catch {
|
||||
return { error: "Invalid or expired state" }
|
||||
}
|
||||
|
|
@ -281,8 +300,9 @@ export async function authorizeOAuth(state: string): Promise<{
|
|||
type: "auth_code",
|
||||
}
|
||||
|
||||
const code = jwt.sign(authCodePayload, JWT_SECRET, {
|
||||
const code = jwt.sign(authCodePayload, getJwtSecret(), {
|
||||
expiresIn: "2m",
|
||||
algorithm: "HS256",
|
||||
jwtid: crypto.randomBytes(16).toString("hex"),
|
||||
})
|
||||
|
||||
|
|
@ -310,7 +330,9 @@ export async function exchangeCodeForToken(
|
|||
try {
|
||||
let codeData: AuthCodePayload
|
||||
try {
|
||||
codeData = jwt.verify(params.code, JWT_SECRET) as unknown as AuthCodePayload
|
||||
codeData = jwt.verify(params.code, getJwtSecret(), {
|
||||
algorithms: ["HS256"],
|
||||
}) as unknown as AuthCodePayload
|
||||
} catch {
|
||||
return { error: "Invalid or expired authorization code" }
|
||||
}
|
||||
|
|
@ -319,6 +341,12 @@ export async function exchangeCodeForToken(
|
|||
return { error: "Invalid authorization code" }
|
||||
}
|
||||
|
||||
// Prevent auth code replay — each jti can only be used once
|
||||
const jti = (codeData as unknown as { jti?: string }).jti
|
||||
if (!jti || !(await markJtiUsed(jti, 120))) {
|
||||
return { error: "Authorization code has already been used" }
|
||||
}
|
||||
|
||||
if (codeData.clientId !== params.clientId) {
|
||||
return { error: "Client ID mismatch" }
|
||||
}
|
||||
|
|
@ -327,11 +355,13 @@ export async function exchangeCodeForToken(
|
|||
return { error: "Redirect URI mismatch" }
|
||||
}
|
||||
|
||||
if (!ALLOWED_CODE_CHALLENGE_METHODS.has(codeData.codeChallengeMethod)) {
|
||||
return { error: "Unsupported code challenge method" }
|
||||
}
|
||||
const computedChallenge = crypto
|
||||
.createHash(codeData.codeChallengeMethod)
|
||||
.createHash("sha256")
|
||||
.update(params.codeVerifier)
|
||||
.digest("base64url")
|
||||
|
||||
if (computedChallenge !== codeData.codeChallenge) {
|
||||
return { error: "Code verifier validation failed" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ export default function CodeFlashAuthContent() {
|
|||
return
|
||||
}
|
||||
|
||||
if (codeChallengeMethod !== "S256" && codeChallengeMethod !== "sha256") {
|
||||
setError("Invalid code challenge method")
|
||||
return
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
setError("Missing request identifier")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,44 +1,27 @@
|
|||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { exchangeCodeForToken } from "../../action"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
console.log("=== Token Exchange Request Started ===")
|
||||
const ALLOWED_CLIENT_IDS = new Set(["cf_vscode_app", "cf-cli-app"])
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
console.log("Request body:", {
|
||||
grant_type: body.grant_type,
|
||||
client_id: body.client_id,
|
||||
redirect_uri: body.redirect_uri,
|
||||
has_code: !!body.code,
|
||||
has_code_verifier: !!body.code_verifier,
|
||||
code_length: body.code?.length,
|
||||
code_verifier_length: body.code_verifier?.length,
|
||||
})
|
||||
|
||||
const { grant_type, code, redirect_uri, code_verifier, client_id } = body
|
||||
|
||||
// Validate grant type
|
||||
if (grant_type !== "authorization_code") {
|
||||
console.error("Invalid grant type:", grant_type)
|
||||
return NextResponse.json({ error: "unsupported_grant_type" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code || !redirect_uri || !code_verifier || !client_id) {
|
||||
console.error("Missing required parameters:", {
|
||||
has_code: !!code,
|
||||
has_redirect_uri: !!redirect_uri,
|
||||
has_code_verifier: !!code_verifier,
|
||||
has_client_id: !!client_id,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: "invalid_request", error_description: "Missing required parameters" },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
console.log("Exchanging code for token...")
|
||||
if (!ALLOWED_CLIENT_IDS.has(client_id)) {
|
||||
return NextResponse.json({ error: "invalid_client" }, { status: 401 })
|
||||
}
|
||||
|
||||
const result = await exchangeCodeForToken({
|
||||
code,
|
||||
|
|
@ -48,26 +31,17 @@ export async function POST(request: NextRequest) {
|
|||
})
|
||||
|
||||
if (result.error) {
|
||||
console.error("Token exchange failed:", result.error)
|
||||
return NextResponse.json(
|
||||
{ error: "invalid_grant", error_description: result.error },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
console.log("=== Token Exchange Request Completed Successfully ===")
|
||||
|
||||
return NextResponse.json({
|
||||
access_token: result.accessToken,
|
||||
token_type: "Bearer",
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("=== Token Exchange Request Failed ===")
|
||||
console.error("Error type:", error instanceof Error ? error.constructor.name : typeof error)
|
||||
console.error("Error message:", error instanceof Error ? error.message : String(error))
|
||||
console.error("Error stack:", error instanceof Error ? error.stack : "No stack trace")
|
||||
console.error("Full error object:", error)
|
||||
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "server_error", error_description: "Internal server error" },
|
||||
{ status: 500 },
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { auth0 } from "@/lib/auth0"
|
|||
export async function upsertReferralSource(
|
||||
referralSource: string,
|
||||
additionalComments?: string,
|
||||
): Promise<any> {
|
||||
): Promise<void> {
|
||||
const session = await auth0.getSession()
|
||||
if (session != null) {
|
||||
setUserReferralData(session.user.sub, referralSource, additionalComments)
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export function CreateApiKeyDialog(): React.JSX.Element {
|
|||
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
|
||||
ownerType === "personal"
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-input hover:bg-accent hover:text-accent-foreground"
|
||||
: "border-input hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
|
|
@ -151,7 +151,7 @@ export function CreateApiKeyDialog(): React.JSX.Element {
|
|||
"flex items-center gap-2 px-3 py-2 rounded-md border text-sm transition-colors",
|
||||
ownerType === "organization"
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-input hover:bg-accent hover:text-accent-foreground"
|
||||
: "border-input hover:bg-accent hover:text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<Building2 className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ export async function reactivateSubscription(userId: string) {
|
|||
try {
|
||||
await reactivateSubscriptionFromCommon(userId)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Error reactivating subscription:", error)
|
||||
Sentry.captureException(error)
|
||||
return { success: false, error: error.message }
|
||||
return { success: false, error: error instanceof Error ? error.message : "Unknown error" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { prisma } from "@codeflash-ai/common"
|
|||
import { getActionAccountContext } from "@/lib/server/get-account-context"
|
||||
|
||||
vi.mock("@/lib/server-action-timing", () => ({
|
||||
withTiming: vi.fn((_name: string, fn: Function) => fn),
|
||||
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
|
||||
}))
|
||||
|
||||
vi.mock("@/lib/analytics/tracking", () => ({
|
||||
|
|
@ -59,8 +59,12 @@ describe("getOrganizationMembers", () => {
|
|||
|
||||
describe("successful retrieval", () => {
|
||||
it("returns members when user has access", async () => {
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
|
||||
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
|
||||
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
|
||||
)
|
||||
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({
|
||||
id: "member-1",
|
||||
} as unknown as Awaited<ReturnType<typeof prisma.organization_members.findUnique>>)
|
||||
|
||||
const result = await getOrganizationMembers("org-1")
|
||||
|
||||
|
|
@ -69,8 +73,12 @@ describe("getOrganizationMembers", () => {
|
|||
})
|
||||
|
||||
it("maps nested organization_members to flat Member structure", async () => {
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
|
||||
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
|
||||
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
|
||||
)
|
||||
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue({
|
||||
id: "member-1",
|
||||
} as unknown as Awaited<ReturnType<typeof prisma.organization_members.findUnique>>)
|
||||
|
||||
const result = await getOrganizationMembers("org-1")
|
||||
const member = result.data![0]
|
||||
|
|
@ -115,7 +123,9 @@ describe("getOrganizationMembers", () => {
|
|||
userId: "unknown-user",
|
||||
username: "testuser",
|
||||
})
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(mockOrg as any)
|
||||
vi.mocked(prisma.organizations.findUnique).mockResolvedValue(
|
||||
mockOrg as unknown as Awaited<ReturnType<typeof prisma.organizations.findUnique>>,
|
||||
)
|
||||
vi.mocked(prisma.organization_members.findUnique).mockResolvedValue(null)
|
||||
|
||||
const result = await getOrganizationMembers("org-1")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default async function OrganizationMembersPage() {
|
|||
<MembersClient
|
||||
initialUserId={initData.userId}
|
||||
initialOrgId={initData.orgId}
|
||||
initialMembers={initData.members as any}
|
||||
initialMembers={initData.members}
|
||||
initialUserRole={initData.currentUserRole}
|
||||
/>
|
||||
</DashboardErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { prisma } from "@codeflash-ai/common"
|
||||
import type { AccountPayload } from "@codeflash-ai/common"
|
||||
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
|
||||
import { trackRepositoryConnected } from "@/lib/analytics/tracking"
|
||||
import { getActionAccountContext } from "@/lib/server/get-account-context"
|
||||
|
||||
vi.mock("@/lib/server-action-timing", () => ({
|
||||
withTiming: vi.fn((_name: string, fn: Function) => fn),
|
||||
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
|
||||
}))
|
||||
|
||||
vi.mock("@/lib/services/repository-utils", () => ({
|
||||
|
|
@ -50,14 +51,16 @@ describe("getRepositoryById", () => {
|
|||
|
||||
describe("parallel fetch", () => {
|
||||
it("fetches repo and authorized repoIds concurrently", async () => {
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
|
||||
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
|
||||
)
|
||||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["repo-1"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(5)
|
||||
|
||||
await getRepositoryById(mockPayload as any, "repo-1")
|
||||
await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
|
||||
expect(prisma.repositories.findUnique).toHaveBeenCalledTimes(1)
|
||||
expect(getRepositoriesForAccountCached).toHaveBeenCalledWith(mockPayload)
|
||||
|
|
@ -68,37 +71,41 @@ describe("getRepositoryById", () => {
|
|||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["repo-1"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null when repo is not in authorized list", async () => {
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
|
||||
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
|
||||
)
|
||||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["other-repo"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("successful retrieval", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
|
||||
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
|
||||
)
|
||||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["repo-1"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
})
|
||||
|
||||
it("returns RepositoryWithUsage with all required fields", async () => {
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(3)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
|
||||
expect(result).toEqual({
|
||||
id: "repo-1",
|
||||
|
|
@ -121,30 +128,32 @@ describe("getRepositoryById", () => {
|
|||
it("sets is_active to false when no recent events", async () => {
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(0)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
expect(result!.is_active).toBe(false)
|
||||
})
|
||||
|
||||
it("sets is_active to true when recent events exist", async () => {
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(10)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
expect(result!.is_active).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("analytics tracking", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(mockRepo as any)
|
||||
vi.mocked(prisma.repositories.findUnique).mockResolvedValue(
|
||||
mockRepo as unknown as Awaited<ReturnType<typeof prisma.repositories.findUnique>>,
|
||||
)
|
||||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["repo-1"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(1)
|
||||
})
|
||||
|
||||
it("calls trackRepositoryConnected for user payloads", async () => {
|
||||
await getRepositoryById(mockPayload as any, "repo-1")
|
||||
await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
|
||||
expect(trackRepositoryConnected).toHaveBeenCalledWith("user-1", {
|
||||
repositoryId: "repo-1",
|
||||
|
|
@ -160,9 +169,9 @@ describe("getRepositoryById", () => {
|
|||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: ["repo-1"],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
|
||||
const result = await getRepositoryById(mockPayload as any, "repo-1")
|
||||
const result = await getRepositoryById(mockPayload as AccountPayload, "repo-1")
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -178,14 +187,16 @@ describe("getRepositoryMembers", () => {
|
|||
userId: "user-1",
|
||||
username: "testuser",
|
||||
})
|
||||
;(prisma.repository_members as any).findMany = vi.fn()
|
||||
;(prisma.repository_members as unknown as Record<string, unknown>).findMany = vi.fn()
|
||||
|
||||
const mod = await import("../action")
|
||||
getRepositoryMembers = mod.getRepositoryMembers
|
||||
})
|
||||
|
||||
it("uses the authenticated session user for the access check", async () => {
|
||||
vi.mocked(prisma.repository_members.findUnique).mockResolvedValue({ id: "member-1" } as any)
|
||||
vi.mocked(prisma.repository_members.findUnique).mockResolvedValue({
|
||||
id: "member-1",
|
||||
} as unknown as Awaited<ReturnType<typeof prisma.repository_members.findUnique>>)
|
||||
vi.mocked(prisma.repository_members.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "member-1",
|
||||
|
|
@ -194,7 +205,7 @@ describe("getRepositoryMembers", () => {
|
|||
added_at: new Date("2024-01-01"),
|
||||
user: { github_username: "alice" },
|
||||
},
|
||||
] as any)
|
||||
] as unknown as Awaited<ReturnType<typeof prisma.repository_members.findMany>>)
|
||||
|
||||
const result = await getRepositoryMembers("repo-1")
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default async function RepositoryDetailPage({
|
|||
repositoryId={repositoryId}
|
||||
initialUserId={initData.userId}
|
||||
initialOrgId={initData.orgId ?? null}
|
||||
initialRepository={initData.repository as any}
|
||||
initialRepository={initData.repository}
|
||||
initialStats={initData.stats}
|
||||
/>
|
||||
</DashboardErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import { ActionResponse, createErrorResponse, createSuccessResponse } from "@/li
|
|||
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
|
||||
import { auth0 } from "@/lib/auth0"
|
||||
import { AccountPayload, buildOptimizationOrCondition, prisma } from "@codeflash-ai/common"
|
||||
import { Prisma } from "@prisma/client"
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { trackOptimizationReviewed } from "@/lib/analytics/tracking"
|
||||
import { PrCommentFields } from "@/lib/types"
|
||||
import { getActionAccountContext } from "@/lib/server/get-account-context"
|
||||
|
||||
export interface DiffContent {
|
||||
|
|
@ -35,10 +37,10 @@ export interface GetStagingCodeParams {
|
|||
async function findAuthorizedOptimizationEvent(
|
||||
payload: AccountPayload,
|
||||
identifiers: { id?: string; trace_id?: string },
|
||||
queryOptions: Record<string, unknown> = {},
|
||||
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
|
||||
) {
|
||||
const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds
|
||||
const where = {
|
||||
const where: Prisma.optimization_eventsWhereInput = {
|
||||
...identifiers,
|
||||
...buildOptimizationOrCondition(payload, repoIds),
|
||||
}
|
||||
|
|
@ -46,14 +48,17 @@ async function findAuthorizedOptimizationEvent(
|
|||
return prisma.optimization_events.findFirst({
|
||||
where,
|
||||
...queryOptions,
|
||||
} as any)
|
||||
})
|
||||
}
|
||||
|
||||
async function getAuthorizedActionContext() {
|
||||
return getActionAccountContext()
|
||||
}
|
||||
|
||||
async function getAuthorizedEventById(eventId: string, queryOptions: Record<string, unknown> = {}) {
|
||||
async function getAuthorizedEventById(
|
||||
eventId: string,
|
||||
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
|
||||
) {
|
||||
const accountContext = await getAuthorizedActionContext()
|
||||
if (!accountContext) {
|
||||
return null
|
||||
|
|
@ -73,7 +78,7 @@ async function getAuthorizedEventById(eventId: string, queryOptions: Record<stri
|
|||
|
||||
async function getAuthorizedEventByTraceId(
|
||||
traceId: string,
|
||||
queryOptions: Record<string, unknown> = {},
|
||||
queryOptions: Omit<Prisma.optimization_eventsFindFirstArgs, "where"> = {},
|
||||
) {
|
||||
const accountContext = await getAuthorizedActionContext()
|
||||
if (!accountContext) {
|
||||
|
|
@ -137,10 +142,12 @@ async function getStagingCodeFromApi(
|
|||
|
||||
const data = await response.json()
|
||||
return createSuccessResponse(data as StagingCodeResponse)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("[getStagingCodeFromApi] Error:", error)
|
||||
Sentry.captureException(error)
|
||||
return createErrorResponse(error?.message || "Failed to fetch staging code")
|
||||
return createErrorResponse(
|
||||
error instanceof Error ? error.message : "Failed to fetch staging code",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -211,10 +218,10 @@ export async function commitStagingCode(
|
|||
|
||||
const data = await response.json()
|
||||
return createSuccessResponse(data as CommitStagingCodeResponse)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("[commitStagingCode] Error:", error)
|
||||
Sentry.captureException(error)
|
||||
return createErrorResponse(error?.message || "Failed to commit changes")
|
||||
return createErrorResponse(error instanceof Error ? error.message : "Failed to commit changes")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +233,7 @@ async function getOptimizationEventById({
|
|||
trace_id: string
|
||||
}) {
|
||||
const repoIds = (await getRepositoriesForAccountCached(payload)).repoIds
|
||||
const where: any = {
|
||||
const where: Prisma.optimization_eventsWhereInput = {
|
||||
trace_id,
|
||||
...buildOptimizationOrCondition(payload, repoIds),
|
||||
}
|
||||
|
|
@ -298,20 +305,21 @@ export async function saveOptimizationChanges({
|
|||
|
||||
try {
|
||||
// Get the current metadata
|
||||
const currentMetadata = (authorizedEvent.event.metadata as any) || {}
|
||||
const currentDiffContents = currentMetadata.diffContents || {}
|
||||
const currentMetadata = (authorizedEvent.event.metadata as Prisma.JsonObject | null) ?? {}
|
||||
const currentDiffContents =
|
||||
(currentMetadata.diffContents as Record<string, Prisma.JsonObject> | undefined) ?? {}
|
||||
|
||||
// Update only the specific file's content
|
||||
const updatedDiffContents = {
|
||||
const updatedDiffContents: Record<string, Prisma.JsonObject> = {
|
||||
...currentDiffContents,
|
||||
[filePath]: {
|
||||
...currentDiffContents[filePath],
|
||||
...(currentDiffContents[filePath] ?? {}),
|
||||
newContent: newContent,
|
||||
},
|
||||
}
|
||||
|
||||
// Update the metadata with new diff contents
|
||||
const updatedMetadata = {
|
||||
const updatedMetadata: Prisma.JsonObject = {
|
||||
...currentMetadata,
|
||||
diffContents: updatedDiffContents,
|
||||
lastModified: new Date().toISOString(),
|
||||
|
|
@ -355,7 +363,7 @@ export async function createPullRequest({
|
|||
}: {
|
||||
traceId: string
|
||||
diffContents: Record<string, { oldContent: string; newContent: string }>
|
||||
prCommentFields?: any
|
||||
prCommentFields?: PrCommentFields
|
||||
generatedTests?: string
|
||||
existingTests?: string
|
||||
functionName?: string
|
||||
|
|
@ -490,8 +498,9 @@ export async function createPullRequest({
|
|||
})
|
||||
|
||||
return createErrorResponse(errorMessage)
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || "Something went wrong. Please try again."
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Something went wrong. Please try again."
|
||||
Sentry.captureException(error)
|
||||
return createErrorResponse(errorMessage)
|
||||
}
|
||||
|
|
@ -620,9 +629,9 @@ export async function getReviewPageInitData(traceId: string) {
|
|||
}
|
||||
|
||||
// If git_branch storage, fetch staging code + comments in parallel
|
||||
const metadata = (event.metadata as any) || {}
|
||||
const metadata = (event.metadata as Record<string, unknown> | null) ?? {}
|
||||
if (event.staging_storage_type === "git_branch") {
|
||||
const stagingBranchName = metadata.staging_branch_name
|
||||
const stagingBranchName = metadata.staging_branch_name as string | undefined
|
||||
const repository = event.repository
|
||||
|
||||
if (stagingBranchName && repository?.full_name && repository?.installation_id) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { notFound } from "next/navigation"
|
||||
import { getReviewPageInitData } from "./action"
|
||||
import { OptimizationReviewClient } from "./review-client"
|
||||
import { OptimizationReviewClient, type ReviewClientProps } from "./review-client"
|
||||
|
||||
interface ReviewPageProps {
|
||||
params: Promise<{ traceId: string }>
|
||||
|
|
@ -24,9 +24,9 @@ export default async function OptimizationReviewPage({ params }: ReviewPageProps
|
|||
return (
|
||||
<OptimizationReviewClient
|
||||
traceId={traceId}
|
||||
initialEvent={initData.event as any}
|
||||
initialComments={initData.comments as any}
|
||||
initialStagingCode={initData.stagingCode as any}
|
||||
initialEvent={initData.event as ReviewClientProps["initialEvent"]}
|
||||
initialComments={initData.comments as ReviewClientProps["initialComments"]}
|
||||
initialStagingCode={initData.stagingCode as ReviewClientProps["initialStagingCode"]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export default async function LineProfilerPage({ params }: ProfilerPageProps) {
|
|||
notFound()
|
||||
}
|
||||
|
||||
const metadata = (initData.event.metadata as any) || {}
|
||||
const metadata = (initData.event.metadata as Record<string, unknown> | null) ?? {}
|
||||
const prCommentFields = (metadata.prCommentFields as Record<string, unknown> | undefined) ?? {}
|
||||
|
||||
return (
|
||||
<ProfilerClient
|
||||
|
|
@ -27,10 +28,10 @@ export default async function LineProfilerPage({ params }: ProfilerPageProps) {
|
|||
functionName={initData.event.function_name ?? null}
|
||||
filePath={initData.event.file_path ?? null}
|
||||
speedupX={initData.event.speedup_x ?? null}
|
||||
originalLineProfiler={metadata.originalLineProfiler}
|
||||
optimizedLineProfiler={metadata.optimizedLineProfiler}
|
||||
originalRuntime={metadata.prCommentFields?.original_runtime}
|
||||
bestRuntime={metadata.prCommentFields?.best_runtime}
|
||||
originalLineProfiler={metadata.originalLineProfiler as string | undefined}
|
||||
optimizedLineProfiler={metadata.optimizedLineProfiler as string | undefined}
|
||||
originalRuntime={prCommentFields.original_runtime as string | undefined}
|
||||
bestRuntime={prCommentFields.best_runtime as string | undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { prisma } from "@codeflash-ai/common"
|
||||
import type { AccountPayload } from "@codeflash-ai/common"
|
||||
import { getRepositoriesForAccountCached } from "@/lib/services/repository-utils"
|
||||
|
||||
const prismaWithRaw = prisma as unknown as Record<string, ReturnType<typeof vi.fn>>
|
||||
|
||||
vi.mock("@/lib/server-action-timing", () => ({
|
||||
withTiming: vi.fn((_name: string, fn: Function) => fn),
|
||||
withTiming: vi.fn((_name: string, fn: (...args: unknown[]) => unknown) => fn),
|
||||
}))
|
||||
|
||||
vi.mock("@/lib/services/repository-utils", () => ({
|
||||
|
|
@ -49,7 +52,7 @@ const mockFeatures = [
|
|||
]
|
||||
|
||||
/** Helper: extract SQL pattern from a $queryRaw tagged template mock call */
|
||||
function getTaggedSql(mockFn: any, callIndex: number): string {
|
||||
function getTaggedSql(mockFn: ReturnType<typeof vi.fn>, callIndex: number): string {
|
||||
const args = mockFn.mock.calls[callIndex]
|
||||
const strings = args[0] as string[]
|
||||
return strings.join("$?")
|
||||
|
|
@ -62,10 +65,10 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: mockRepoIds,
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
|
||||
// $queryRaw is used as a tagged template literal — auto-mock doesn't create it
|
||||
;(prisma as any).$queryRaw = vi.fn()
|
||||
prismaWithRaw.$queryRaw = vi.fn()
|
||||
|
||||
const mod = await import("../action")
|
||||
getAllOptimizationEvents = mod.getAllOptimizationEvents
|
||||
|
|
@ -74,22 +77,30 @@ describe("getAllOptimizationEvents", () => {
|
|||
describe("Path B: standard Prisma query (org account)", () => {
|
||||
// Org accounts use Prisma findMany/count (not raw SQL) when not sorting by review_quality
|
||||
it("calls findMany and count in parallel", async () => {
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
|
||||
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
|
||||
)
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
|
||||
|
||||
expect(prisma.optimization_events.findMany).toHaveBeenCalledTimes(1)
|
||||
expect(prisma.optimization_events.count).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("batch-fetches optimization_features by trace_id array (not N+1)", async () => {
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
|
||||
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
|
||||
)
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(
|
||||
mockFeatures as unknown as Awaited<
|
||||
ReturnType<typeof prisma.optimization_features.findMany>
|
||||
>,
|
||||
)
|
||||
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
|
||||
|
||||
// Single batch query with all trace IDs — NOT one per event
|
||||
expect(prisma.optimization_features.findMany).toHaveBeenCalledTimes(1)
|
||||
|
|
@ -104,15 +115,23 @@ describe("getAllOptimizationEvents", () => {
|
|||
})
|
||||
|
||||
it("merges review_quality into events", async () => {
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(mockEvents as any)
|
||||
vi.mocked(prisma.optimization_events.findMany).mockResolvedValue(
|
||||
mockEvents as unknown as Awaited<ReturnType<typeof prisma.optimization_events.findMany>>,
|
||||
)
|
||||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(2)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(mockFeatures as any)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue(
|
||||
mockFeatures as unknown as Awaited<
|
||||
ReturnType<typeof prisma.optimization_features.findMany>
|
||||
>,
|
||||
)
|
||||
|
||||
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as any })
|
||||
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
|
||||
|
||||
expect((result.events[0] as any).review_quality).toBe("high")
|
||||
expect((result.events[0] as any).review_explanation).toBe("Great optimization")
|
||||
expect((result.events[1] as any).review_quality).toBeNull()
|
||||
const event0 = result.events[0] as Record<string, unknown>
|
||||
const event1 = result.events[1] as Record<string, unknown>
|
||||
expect(event0.review_quality).toBe("high")
|
||||
expect(event0.review_explanation).toBe("Great optimization")
|
||||
expect(event1.review_quality).toBeNull()
|
||||
})
|
||||
|
||||
it("returns totalCount from count query", async () => {
|
||||
|
|
@ -120,7 +139,7 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(42)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as any })
|
||||
const result = await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
|
||||
expect(result.totalCount).toBe(42)
|
||||
})
|
||||
|
||||
|
|
@ -130,7 +149,7 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
page: 3,
|
||||
pageSize: 25,
|
||||
})
|
||||
|
|
@ -148,7 +167,7 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(prisma.optimization_events.count).mockResolvedValue(0)
|
||||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as any })
|
||||
await getAllOptimizationEvents({ payload: mockOrgPayload as AccountPayload })
|
||||
|
||||
expect(prisma.optimization_events.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -163,17 +182,21 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
search: "calc",
|
||||
})
|
||||
|
||||
// Check findMany was called with a search-containing where clause
|
||||
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any
|
||||
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
// Should have AND with OR containing the search fields
|
||||
expect(call.where.AND).toBeDefined()
|
||||
const orClause = call.where.AND.find((c: any) => c.OR)
|
||||
const andClauses = call.where.AND as Array<Record<string, unknown>>
|
||||
const orClause = andClauses.find(c => c.OR)
|
||||
expect(orClause).toBeDefined()
|
||||
expect(orClause.OR).toHaveLength(3) // function_name, file_path, repository.full_name
|
||||
expect(orClause!.OR).toHaveLength(3) // function_name, file_path, repository.full_name
|
||||
})
|
||||
|
||||
it("applies repository_id filter", async () => {
|
||||
|
|
@ -182,54 +205,58 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(prisma.optimization_features.findMany).mockResolvedValue([])
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
filter: { repository_id: mockRepoIds[0] },
|
||||
})
|
||||
|
||||
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as any
|
||||
const call = vi.mocked(prisma.optimization_events.findMany).mock.calls[0][0] as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>
|
||||
// The repository_id filter should be in the AND clause
|
||||
const repoFilter = call.where.AND.find((c: any) => c.repository_id !== undefined)
|
||||
const andClauses = call.where.AND as Array<Record<string, unknown>>
|
||||
const repoFilter = andClauses.find(c => c.repository_id !== undefined)
|
||||
expect(repoFilter).toBeDefined()
|
||||
expect(repoFilter.repository_id).toBe(mockRepoIds[0])
|
||||
expect(repoFilter!.repository_id).toBe(mockRepoIds[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Path A: raw SQL query (review_quality sort/filter)", () => {
|
||||
it("triggers when sort includes review_quality", async () => {
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce([]) // events
|
||||
.mockResolvedValueOnce([{ count: BigInt(0) }]) // count
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
sort: { review_quality: "desc" },
|
||||
})
|
||||
|
||||
expect((prisma as any).$queryRaw).toHaveBeenCalledTimes(2)
|
||||
expect(prismaWithRaw.$queryRaw).toHaveBeenCalledTimes(2)
|
||||
// Should NOT use standard Prisma findMany
|
||||
expect(prisma.optimization_events.findMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("triggers when filter includes review_quality", async () => {
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ count: BigInt(0) }])
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
filter: { review_quality: "high" },
|
||||
})
|
||||
|
||||
expect((prisma as any).$queryRaw).toHaveBeenCalledTimes(2)
|
||||
expect(prismaWithRaw.$queryRaw).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("returns correct totalCount from BigInt conversion", async () => {
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ count: BigInt(99) }])
|
||||
|
||||
const result = await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
sort: { review_quality: "asc" },
|
||||
})
|
||||
|
||||
|
|
@ -248,16 +275,16 @@ describe("getAllOptimizationEvents", () => {
|
|||
repo_id: mockRepoIds[0],
|
||||
},
|
||||
]
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce(rawEvents)
|
||||
.mockResolvedValueOnce([{ count: BigInt(1) }])
|
||||
|
||||
const result = await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
sort: { review_quality: "desc" },
|
||||
})
|
||||
|
||||
expect((result.events[0] as any).repository).toEqual({
|
||||
expect((result.events[0] as Record<string, unknown>).repository).toEqual({
|
||||
id: mockRepoIds[0],
|
||||
full_name: "org/repo",
|
||||
name: "repo",
|
||||
|
|
@ -276,30 +303,30 @@ describe("getAllOptimizationEvents", () => {
|
|||
repo_id: null,
|
||||
},
|
||||
]
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce(rawEvents)
|
||||
.mockResolvedValueOnce([{ count: BigInt(1) }])
|
||||
|
||||
const result = await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
sort: { review_quality: "desc" },
|
||||
})
|
||||
|
||||
expect((result.events[0] as any).repository).toBeNull()
|
||||
expect((result.events[0] as Record<string, unknown>).repository).toBeNull()
|
||||
})
|
||||
|
||||
it("includes LEFT JOIN in raw SQL queries", async () => {
|
||||
;(prisma as any).$queryRaw
|
||||
prismaWithRaw.$queryRaw
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ count: BigInt(0) }])
|
||||
|
||||
await getAllOptimizationEvents({
|
||||
payload: mockOrgPayload as any,
|
||||
payload: mockOrgPayload as AccountPayload,
|
||||
sort: { review_quality: "desc" },
|
||||
})
|
||||
|
||||
// $queryRaw is a tagged template — first arg is TemplateStringsArray
|
||||
const sql = getTaggedSql((prisma as any).$queryRaw, 0)
|
||||
const sql = getTaggedSql(prismaWithRaw.$queryRaw, 0)
|
||||
expect(sql).toContain("LEFT JOIN optimization_features")
|
||||
expect(sql).toContain("LEFT JOIN repositories")
|
||||
})
|
||||
|
|
@ -310,9 +337,11 @@ describe("getAllOptimizationEvents", () => {
|
|||
vi.mocked(getRepositoriesForAccountCached).mockResolvedValue({
|
||||
repoIds: [],
|
||||
repos: [],
|
||||
} as any)
|
||||
} as Awaited<ReturnType<typeof getRepositoriesForAccountCached>>)
|
||||
|
||||
const result = await getAllOptimizationEvents({ payload: mockPersonalPayload as any })
|
||||
const result = await getAllOptimizationEvents({
|
||||
payload: mockPersonalPayload as AccountPayload,
|
||||
})
|
||||
expect(result.events).toEqual([])
|
||||
expect(result.totalCount).toBe(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -305,7 +305,6 @@ export function OptimizationsTable({
|
|||
}
|
||||
}
|
||||
// Flatten filter properties as deps to avoid object-reference churn
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
filters.page,
|
||||
filters.search,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@ import { withTiming } from "@/lib/server-action-timing"
|
|||
import { AccountPayload, prisma } from "@codeflash-ai/common"
|
||||
import { Prisma } from "@prisma/client"
|
||||
|
||||
/** Row shape returned by raw SQL joins on optimization_events + optimization_features + repositories */
|
||||
interface RawOptimizationEventRow extends Record<string, unknown> {
|
||||
repo_id?: string
|
||||
repo_full_name?: string
|
||||
repo_name?: string
|
||||
review_quality?: string | null
|
||||
review_explanation?: string | null
|
||||
}
|
||||
|
||||
/** Filter values accepted for optimization event queries */
|
||||
type FilterValue = string | null | { not: null } | undefined
|
||||
|
||||
// Cached implementation for getRepositoriesWithStagingEvents
|
||||
// React cache() ensures this is only executed once per unique payload within a single request
|
||||
const getRepositoriesWithStagingEventsImpl = cache(
|
||||
|
|
@ -91,7 +103,7 @@ const getAllOptimizationEventsImpl = async ({
|
|||
}: {
|
||||
payload: AccountPayload
|
||||
search?: string
|
||||
filter?: Record<string, any>
|
||||
filter?: Record<string, FilterValue>
|
||||
sort?: { [key: string]: "asc" | "desc" }
|
||||
page?: number
|
||||
pageSize?: number
|
||||
|
|
@ -144,12 +156,18 @@ const getAllOptimizationEventsImpl = async ({
|
|||
whereFragments.push(Prisma.sql`of.review_quality = ${filter.review_quality}`)
|
||||
}
|
||||
if (filter.repository_id !== undefined) {
|
||||
if (filter.repository_id === null) {
|
||||
const repoFilter = filter.repository_id
|
||||
if (repoFilter === null) {
|
||||
whereFragments.push(Prisma.sql`oe.repository_id IS NULL`)
|
||||
} else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
|
||||
} else if (
|
||||
typeof repoFilter === "object" &&
|
||||
repoFilter !== null &&
|
||||
"not" in repoFilter &&
|
||||
repoFilter.not === null
|
||||
) {
|
||||
whereFragments.push(Prisma.sql`oe.repository_id IS NOT NULL`)
|
||||
} else if (typeof filter.repository_id === "string") {
|
||||
whereFragments.push(Prisma.sql`oe.repository_id = ${filter.repository_id}`)
|
||||
} else if (typeof repoFilter === "string") {
|
||||
whereFragments.push(Prisma.sql`oe.repository_id = ${repoFilter}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,7 +198,7 @@ const getAllOptimizationEventsImpl = async ({
|
|||
const paginationLimit = pageSize
|
||||
const paginationOffset = (page - 1) * pageSize
|
||||
const [events, countResult] = await Promise.all([
|
||||
prisma.$queryRaw<any[]>`
|
||||
prisma.$queryRaw<RawOptimizationEventRow[]>`
|
||||
SELECT
|
||||
oe.*,
|
||||
of.review_quality,
|
||||
|
|
@ -205,20 +223,12 @@ const getAllOptimizationEventsImpl = async ({
|
|||
])
|
||||
const totalCount = Number(countResult[0].count)
|
||||
// Repository data is already included from the JOIN
|
||||
const eventsWithRepo = events.map(
|
||||
(
|
||||
event: Record<string, unknown> & {
|
||||
repo_id?: string
|
||||
repo_full_name?: string
|
||||
repo_name?: string
|
||||
},
|
||||
) => ({
|
||||
...event,
|
||||
repository: event.repo_id
|
||||
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
|
||||
: null,
|
||||
}),
|
||||
)
|
||||
const eventsWithRepo = events.map((event: RawOptimizationEventRow) => ({
|
||||
...event,
|
||||
repository: event.repo_id
|
||||
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
|
||||
: null,
|
||||
}))
|
||||
return { events: eventsWithRepo, totalCount }
|
||||
} else {
|
||||
// Standard Prisma query with native orderBy (optimized with UNION for personal accounts)
|
||||
|
|
@ -229,10 +239,12 @@ const getAllOptimizationEventsImpl = async ({
|
|||
|
||||
if ("orgId" in payload) {
|
||||
// Organization account: simple IN clause
|
||||
const where = {
|
||||
const where: Prisma.optimization_eventsWhereInput & {
|
||||
AND?: Prisma.optimization_eventsWhereInput[]
|
||||
} = {
|
||||
is_staging: true,
|
||||
repository_id: { in: repoIds },
|
||||
} as any
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.AND = where.AND || []
|
||||
|
|
@ -266,9 +278,9 @@ const getAllOptimizationEventsImpl = async ({
|
|||
Object.keys(filter).forEach(key => {
|
||||
if (key === "repository_id") {
|
||||
where.AND = where.AND || []
|
||||
where.AND.push({ [key]: filter[key] })
|
||||
where.AND.push({ [key]: filter[key] } as Prisma.optimization_eventsWhereInput)
|
||||
} else if (key !== "review_quality") {
|
||||
where[key] = filter[key]
|
||||
;(where as Record<string, FilterValue>)[key] = filter[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -305,7 +317,12 @@ const getAllOptimizationEventsImpl = async ({
|
|||
} else if (key === "repository_id") {
|
||||
if (value === null) {
|
||||
filterFragments.push(Prisma.sql`AND oe.repository_id IS NULL`)
|
||||
} else if (value?.not === null) {
|
||||
} else if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"not" in value &&
|
||||
value.not === null
|
||||
) {
|
||||
filterFragments.push(Prisma.sql`AND oe.repository_id IS NOT NULL`)
|
||||
} else if (typeof value === "string") {
|
||||
filterFragments.push(Prisma.sql`AND oe.repository_id = ${value}`)
|
||||
|
|
@ -339,7 +356,7 @@ const getAllOptimizationEventsImpl = async ({
|
|||
`
|
||||
|
||||
const [eventsResult, countResult] = await Promise.all([
|
||||
prisma.$queryRaw<any[]>`
|
||||
prisma.$queryRaw<RawOptimizationEventRow[]>`
|
||||
WITH base_events AS (
|
||||
SELECT oe.*, r.id as repo_id, r.full_name as repo_full_name, r.name as repo_name
|
||||
FROM optimization_events oe
|
||||
|
|
@ -363,20 +380,12 @@ const getAllOptimizationEventsImpl = async ({
|
|||
])
|
||||
|
||||
totalCount = Number(countResult[0].count)
|
||||
events = eventsResult.map(
|
||||
(
|
||||
event: Record<string, unknown> & {
|
||||
repo_id?: string
|
||||
repo_full_name?: string
|
||||
repo_name?: string
|
||||
},
|
||||
) => ({
|
||||
...event,
|
||||
repository: event.repo_id
|
||||
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
|
||||
: null,
|
||||
}),
|
||||
)
|
||||
events = eventsResult.map((event: RawOptimizationEventRow) => ({
|
||||
...event,
|
||||
repository: event.repo_id
|
||||
? { id: event.repo_id, full_name: event.repo_full_name, name: event.repo_name }
|
||||
: null,
|
||||
}))
|
||||
}
|
||||
|
||||
// Batch-fetch review data for all events in a single query
|
||||
|
|
|
|||
|
|
@ -11,17 +11,39 @@ export default async function ReviewOptimizationsPage() {
|
|||
getCachedRepositories(accountKey, accountPayload),
|
||||
])
|
||||
|
||||
const initialEvents = (initialData?.events || []).map((event: any) => ({
|
||||
...event,
|
||||
created_at:
|
||||
event.created_at instanceof Date ? event.created_at.toISOString() : event.created_at,
|
||||
repository: event.repository
|
||||
? {
|
||||
id: event.repository.id,
|
||||
full_name: event.repository.full_name || event.repository.name,
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
interface OptimizationEventSerialized {
|
||||
id: string
|
||||
function_name?: string
|
||||
file_path?: string
|
||||
repository?: { id: string; full_name?: string } | null
|
||||
speedup_x?: number
|
||||
speedup_pct?: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
created_at: string
|
||||
status?: string
|
||||
event_type?: string
|
||||
trace_id: string
|
||||
review_quality: string
|
||||
}
|
||||
|
||||
const initialEvents: OptimizationEventSerialized[] = (initialData?.events || []).map(
|
||||
(
|
||||
event: Record<string, unknown> & {
|
||||
repository?: { id: string; full_name?: string; name?: string } | null
|
||||
},
|
||||
) =>
|
||||
({
|
||||
...event,
|
||||
created_at:
|
||||
event.created_at instanceof Date ? event.created_at.toISOString() : event.created_at,
|
||||
repository: event.repository
|
||||
? {
|
||||
id: event.repository.id,
|
||||
full_name: event.repository.full_name || event.repository.name,
|
||||
}
|
||||
: null,
|
||||
}) as OptimizationEventSerialized,
|
||||
)
|
||||
|
||||
return (
|
||||
<OptimizationsTable
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { Prisma } from "@prisma/client"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(request: NextRequest, props: { params: Promise<{ trace_id: string }> }) {
|
||||
|
|
@ -31,34 +32,39 @@ export async function POST(request: NextRequest, props: { params: Promise<{ trac
|
|||
}
|
||||
|
||||
// Get existing metadata and experiment_metadata
|
||||
const existingMetadata = (optimizationFeature.metadata as any) || {}
|
||||
const experimentMetadata = (optimizationFeature.experiment_metadata as any) || {}
|
||||
const existingMetadata = (optimizationFeature.metadata as Prisma.JsonObject | null) ?? {}
|
||||
const experimentMetadata =
|
||||
(optimizationFeature.experiment_metadata as Prisma.JsonObject | null) ?? {}
|
||||
|
||||
// Check if diffContents exists and has the fileKey
|
||||
if (!experimentMetadata.diffContents || !experimentMetadata.diffContents[fileKey]) {
|
||||
const diffContents = experimentMetadata.diffContents as
|
||||
| Record<string, Prisma.JsonObject>
|
||||
| undefined
|
||||
if (!diffContents || !diffContents[fileKey]) {
|
||||
return NextResponse.json({ error: "File not found in experiment metadata" }, { status: 404 })
|
||||
}
|
||||
|
||||
// Backup original code if not already backed up
|
||||
const originalCode = existingMetadata.originalCode || {}
|
||||
const originalCode: Prisma.JsonObject =
|
||||
(existingMetadata.originalCode as Prisma.JsonObject | undefined) ?? {}
|
||||
if (!originalCode[fileKey]) {
|
||||
originalCode[fileKey] = experimentMetadata.diffContents[fileKey].newContent
|
||||
originalCode[fileKey] = diffContents[fileKey].newContent
|
||||
}
|
||||
|
||||
// Update experiment_metadata with modified code
|
||||
const updatedExperimentMetadata = {
|
||||
const updatedExperimentMetadata: Prisma.JsonObject = {
|
||||
...experimentMetadata,
|
||||
diffContents: {
|
||||
...experimentMetadata.diffContents,
|
||||
...diffContents,
|
||||
[fileKey]: {
|
||||
...experimentMetadata.diffContents[fileKey],
|
||||
...diffContents[fileKey],
|
||||
newContent: modifiedCode,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Update metadata with backup and tracking info
|
||||
const updatedMetadata = {
|
||||
const updatedMetadata: Prisma.JsonObject = {
|
||||
...existingMetadata,
|
||||
originalCode,
|
||||
lastModified: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect, useMemo, memo } from "react"
|
||||
import {
|
||||
FileText,
|
||||
Code,
|
||||
GitCompare,
|
||||
Columns2,
|
||||
} from "lucide-react"
|
||||
import { FileText, Code, GitCompare, Columns2 } from "lucide-react"
|
||||
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
|
||||
import { parseAllCodeBlocks, findMatchingFile } from "./timeline-helpers"
|
||||
import { DiffView, SideBySideDiffView } from "./diff-views"
|
||||
|
|
@ -26,18 +21,17 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
const [unifiedDiff, setUnifiedDiff] = useState<string | null>(null)
|
||||
const [diffLoading, setDiffLoading] = useState(false)
|
||||
|
||||
const originalCode =
|
||||
content.type === "refinement" ? content.parentCode : content.originalCode
|
||||
const originalCode = content.type === "refinement" ? content.parentCode : content.originalCode
|
||||
|
||||
const candidateFiles = useMemo(() => parseAllCodeBlocks(content.code), [content.code])
|
||||
const originalFiles = useMemo(
|
||||
() => (originalCode ? parseAllCodeBlocks(originalCode) : []),
|
||||
[originalCode]
|
||||
[originalCode],
|
||||
)
|
||||
|
||||
const selectedCandidateFile = useMemo(
|
||||
() => candidateFiles[selectedFileIndex] || candidateFiles[0],
|
||||
[candidateFiles, selectedFileIndex]
|
||||
[candidateFiles, selectedFileIndex],
|
||||
)
|
||||
|
||||
const matchingOriginalFile = useMemo(() => {
|
||||
|
|
@ -61,9 +55,7 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
if (cancelled) return
|
||||
|
||||
const filename =
|
||||
selectedCandidateFile.filename ||
|
||||
matchingOriginalFile.filename ||
|
||||
"code.py"
|
||||
selectedCandidateFile.filename || matchingOriginalFile.filename || "code.py"
|
||||
|
||||
const diff = createTwoFilesPatch(
|
||||
`a/${filename}`,
|
||||
|
|
@ -72,14 +64,12 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
selectedCandidateFile.code,
|
||||
"",
|
||||
"",
|
||||
{ context: 3 }
|
||||
{ context: 3 },
|
||||
)
|
||||
|
||||
const lines = diff.split("\n")
|
||||
const hunkStartIndex = lines.findIndex(line => line.startsWith("@@"))
|
||||
const processedDiff = hunkStartIndex > 0
|
||||
? lines.slice(hunkStartIndex).join("\n")
|
||||
: diff
|
||||
const processedDiff = hunkStartIndex > 0 ? lines.slice(hunkStartIndex).join("\n") : diff
|
||||
|
||||
setUnifiedDiff(processedDiff)
|
||||
setDiffLoading(false)
|
||||
|
|
@ -90,16 +80,15 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
setDiffLoading(false)
|
||||
})
|
||||
|
||||
return () => { cancelled = true }
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [viewMode, matchingOriginalFile, selectedCandidateFile])
|
||||
|
||||
const hasDiff = matchingOriginalFile !== null
|
||||
const hasMultipleFiles = candidateFiles.length > 1
|
||||
|
||||
const codeContainerStyle = useMemo(
|
||||
() => ({ maxHeight: isActive ? "80vh" : "200px" }),
|
||||
[isActive]
|
||||
)
|
||||
const codeContainerStyle = useMemo(() => ({ maxHeight: isActive ? "80vh" : "200px" }), [isActive])
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -164,7 +153,7 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
{hasMultipleFiles && (
|
||||
<select
|
||||
value={selectedFileIndex}
|
||||
onChange={(e) => setSelectedFileIndex(Number(e.target.value))}
|
||||
onChange={e => setSelectedFileIndex(Number(e.target.value))}
|
||||
className="px-2 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700"
|
||||
>
|
||||
{candidateFiles.map((file, index) => (
|
||||
|
|
@ -185,21 +174,21 @@ export const CandidateContent = memo(function CandidateContent({
|
|||
<span className="text-sm font-mono font-medium text-zinc-700 dark:text-zinc-300">
|
||||
{selectedCandidateFile.filename || "Code"}
|
||||
</span>
|
||||
{selectedCandidateFile.path && selectedCandidateFile.path !== selectedCandidateFile.filename && (
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
({selectedCandidateFile.path})
|
||||
</span>
|
||||
)}
|
||||
{selectedCandidateFile.path &&
|
||||
selectedCandidateFile.path !== selectedCandidateFile.filename && (
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
({selectedCandidateFile.path})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-zinc-500">
|
||||
{selectedCandidateFile.lineCount} lines
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500">{selectedCandidateFile.lineCount} lines</span>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
style={codeContainerStyle}
|
||||
>
|
||||
<CodeHighlighter language={selectedCandidateFile.language} code={selectedCandidateFile.code} customStyle={CODE_STYLE} />
|
||||
<div className="overflow-y-auto" style={codeContainerStyle}>
|
||||
<CodeHighlighter
|
||||
language={selectedCandidateFile.language}
|
||||
code={selectedCandidateFile.code}
|
||||
customStyle={CODE_STYLE}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ export const CodeContextSection = memo(function CodeContextSection({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded)
|
||||
}}
|
||||
className="w-full p-6 flex items-center justify-between hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors rounded-sm cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -134,7 +136,9 @@ export const CodeContextSection = memo(function CodeContextSection({
|
|||
{metrics.totalFiles} files
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -153,7 +157,10 @@ export const CodeContextSection = memo(function CodeContextSection({
|
|||
{filePath && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400 mb-1">File</span>
|
||||
<code className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit truncate max-w-full" title={filePath}>
|
||||
<code
|
||||
className="text-sm font-mono text-zinc-900 dark:text-white bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-sm w-fit truncate max-w-full"
|
||||
title={filePath}
|
||||
>
|
||||
{filePath}
|
||||
</code>
|
||||
</div>
|
||||
|
|
@ -242,7 +249,11 @@ interface CodeGroupSectionProps {
|
|||
sectionKey: string
|
||||
}
|
||||
|
||||
function getAccentColorClasses(accentColor: "emerald" | "slate"): { border: string; bg: string; icon: string } {
|
||||
function getAccentColorClasses(accentColor: "emerald" | "slate"): {
|
||||
border: string
|
||||
bg: string
|
||||
icon: string
|
||||
} {
|
||||
switch (accentColor) {
|
||||
case "emerald":
|
||||
return {
|
||||
|
|
@ -280,7 +291,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={onToggle}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggle() }}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") onToggle()
|
||||
}}
|
||||
className={`w-full p-4 flex items-center justify-between hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer ${bgColor}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -292,9 +305,12 @@ const CodeGroupSection = memo(function CodeGroupSection({
|
|||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars · {files.length} files
|
||||
{tokenCount.toLocaleString()} tokens · {charCount.toLocaleString()} chars ·{" "}
|
||||
{files.length} files
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -310,7 +326,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onToggleFile(fileKey)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") onToggleFile(fileKey)
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer hover:opacity-80 flex-1"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 text-zinc-400" />
|
||||
|
|
@ -320,7 +338,9 @@ const CodeGroupSection = memo(function CodeGroupSection({
|
|||
<span className="text-xs text-zinc-500 dark:text-zinc-400" title={file.path}>
|
||||
{file.path !== file.filename && `(${file.path})`}
|
||||
</span>
|
||||
<ChevronDown className={`h-3.5 w-3.5 text-zinc-400 transition-transform duration-200 ${isFileExpanded ? '' : '-rotate-90'}`} />
|
||||
<ChevronDown
|
||||
className={`h-3.5 w-3.5 text-zinc-400 transition-transform duration-200 ${isFileExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
|
|
@ -378,4 +398,4 @@ const TokenDistributionBar = memo(function TokenDistributionBar({
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -119,13 +119,8 @@ export const DiffView = memo(function DiffView({ diff }: DiffViewProps) {
|
|||
const { bgClass, textClass, lineContent, indicator, borderClass } = getDiffLineStyle(line)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${bgClass} border-l-2 ${borderClass}`}
|
||||
>
|
||||
<div className="w-8 flex-shrink-0 text-right pr-2 select-none">
|
||||
{indicator}
|
||||
</div>
|
||||
<div key={index} className={`flex ${bgClass} border-l-2 ${borderClass}`}>
|
||||
<div className="w-8 flex-shrink-0 text-right pr-2 select-none">{indicator}</div>
|
||||
<pre className={`flex-1 px-2 py-0.5 ${textClass} whitespace-pre`}>
|
||||
{lineContent || " "}
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useCallback, memo } from "react"
|
||||
import {
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
} from "lucide-react"
|
||||
import { XCircle, AlertCircle, AlertTriangle, ChevronDown } from "lucide-react"
|
||||
import { CopyButton } from "./copy-button"
|
||||
|
||||
interface ErrorContext {
|
||||
|
|
@ -89,7 +84,9 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleError(error.id)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") toggleError(error.id) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") toggleError(error.id)
|
||||
}}
|
||||
className="flex items-start gap-3 cursor-pointer hover:opacity-80 flex-1 transition-opacity duration-150"
|
||||
>
|
||||
<SeverityIcon className="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
|
|
@ -110,7 +107,9 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
|
|||
</p>
|
||||
</div>
|
||||
{(hasContext || isTestFailure) && (
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -201,4 +200,4 @@ export const ErrorsSection = memo(function ErrorsSection({ errors }: ErrorsSecti
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -132,7 +132,9 @@ export function formatTimelineForLLM(input: LLMExportInput): string {
|
|||
lines.push("## Errors")
|
||||
lines.push("")
|
||||
for (const error of errors) {
|
||||
lines.push(`### ${error.error_type || "Error"}${error.severity ? ` (${error.severity})` : ""}`)
|
||||
lines.push(
|
||||
`### ${error.error_type || "Error"}${error.severity ? ` (${error.severity})` : ""}`,
|
||||
)
|
||||
if (error.error_message) lines.push(error.error_message)
|
||||
if (error.context) {
|
||||
const ctx = error.context
|
||||
|
|
@ -153,7 +155,11 @@ export function formatTimelineForLLM(input: LLMExportInput): string {
|
|||
const totalCost = sections.reduce((sum, s) => sum + (s.cost ?? 0), 0)
|
||||
const totalTokens = sections.reduce((sum, s) => sum + (s.tokens ?? 0), 0)
|
||||
const candidatesCount = sections.filter(
|
||||
s => s.type === "optimization" || s.type === "line_profiler" || s.type === "refinement" || s.type === "adaptive",
|
||||
s =>
|
||||
s.type === "optimization" ||
|
||||
s.type === "line_profiler" ||
|
||||
s.type === "refinement" ||
|
||||
s.type === "adaptive",
|
||||
).length
|
||||
|
||||
lines.push("---")
|
||||
|
|
|
|||
|
|
@ -47,16 +47,17 @@ function parseMarkdownCodeBlocks(markdown: string): ParsedFile[] {
|
|||
function findTargetFile(files: ParsedFile[], filePath: string | null): ParsedFile | null {
|
||||
if (!filePath || files.length === 0) return null
|
||||
|
||||
return files.find(file =>
|
||||
filePath.endsWith(file.path) ||
|
||||
file.path.endsWith(filePath) ||
|
||||
file.path === filePath
|
||||
) || null
|
||||
return (
|
||||
files.find(
|
||||
file =>
|
||||
filePath.endsWith(file.path) || file.path.endsWith(filePath) || file.path === filePath,
|
||||
) || null
|
||||
)
|
||||
}
|
||||
|
||||
async function searchAllFiles(
|
||||
files: ParsedFile[],
|
||||
functionName: string
|
||||
functionName: string,
|
||||
): Promise<Array<{ file: ParsedFile; location: FunctionLocation } | null>> {
|
||||
const searchPromises = files.map(async file => {
|
||||
const location = await findFunctionInCode(file.code, functionName)
|
||||
|
|
@ -119,7 +120,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
}
|
||||
|
||||
searchForFunction(functionName)
|
||||
return () => { cancelled = true }
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [functionName, filePath, allFiles])
|
||||
|
||||
const functionFile = actualFile ?? allFiles[0] ?? null
|
||||
|
|
@ -143,11 +146,11 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
const lineHeight = 22.4
|
||||
const paddingTop = 16
|
||||
const targetLine = functionLocation.startLine - 1
|
||||
const scrollPosition = paddingTop + (targetLine * lineHeight) - (container.clientHeight / 3)
|
||||
const scrollPosition = paddingTop + targetLine * lineHeight - container.clientHeight / 3
|
||||
|
||||
container.scrollTo({
|
||||
top: Math.max(0, scrollPosition),
|
||||
behavior: 'smooth'
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -168,16 +171,16 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded) }}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") setIsExpanded(!isExpanded)
|
||||
}}
|
||||
className="flex items-center gap-3 cursor-pointer hover:opacity-80 flex-1"
|
||||
>
|
||||
<div className="p-2 bg-zinc-800 rounded-sm">
|
||||
<Code className="h-4 w-4 text-zinc-400" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h2 className="text-lg font-semibold text-zinc-50">
|
||||
Function to Optimize
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-zinc-50">Function to Optimize</h2>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{functionName && (
|
||||
<code className="text-sm font-mono text-zinc-300 bg-zinc-800 px-2 py-0.5 rounded-sm">
|
||||
|
|
@ -191,7 +194,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-zinc-500 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CopyButton text={functionFile.code} label="function code" />
|
||||
|
|
@ -206,13 +211,9 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
{functionFile.filename}
|
||||
</span>
|
||||
{functionFile.path !== functionFile.filename && (
|
||||
<span className="text-xs font-mono text-zinc-500">
|
||||
({functionFile.path})
|
||||
</span>
|
||||
<span className="text-xs font-mono text-zinc-500">({functionFile.path})</span>
|
||||
)}
|
||||
<span className="text-xs text-zinc-500 ml-auto">
|
||||
{functionFile.lineCount} lines
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500 ml-auto">{functionFile.lineCount} lines</span>
|
||||
</div>
|
||||
|
||||
<div ref={codeContainerRef} className="max-h-[500px] overflow-y-auto">
|
||||
|
|
@ -227,4 +228,4 @@ export const FunctionToOptimizeSection = memo(function FunctionToOptimizeSection
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ export { transformToTimelineSections } from "./timeline-types"
|
|||
export { ErrorsSection } from "./errors-section"
|
||||
export { FunctionToOptimizeSection } from "./function-to-optimize-section"
|
||||
export { CodeContextSection } from "./code-context-section"
|
||||
export { CodeHighlighter, CODE_STYLE, CODE_STYLE_RELAXED, CODE_STYLE_SMALL } from "./code-highlighter"
|
||||
export {
|
||||
CodeHighlighter,
|
||||
CODE_STYLE,
|
||||
CODE_STYLE_RELAXED,
|
||||
CODE_STYLE_SMALL,
|
||||
} from "./code-highlighter"
|
||||
export { findFunctionInCode } from "./python-parser"
|
||||
export type { FunctionLocation } from "./python-parser"
|
||||
export { CopyButton } from "./copy-button"
|
||||
export { InfoIcon } from "./info-icon"
|
||||
export { getTraceSource } from "./utils"
|
||||
export { getTraceSource } from "./utils"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, memo, useMemo } from "react"
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Code,
|
||||
Bug,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { ChevronDown, ChevronUp, Code, Bug, Search, X } from "lucide-react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -97,7 +90,10 @@ const PromptContent = memo(function PromptContent({ content }: { content: string
|
|||
{parts.map((part, index) => {
|
||||
if (part.type === "code") {
|
||||
return (
|
||||
<div key={index} className="rounded border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
<div
|
||||
key={index}
|
||||
className="rounded border border-zinc-200 dark:border-zinc-700 overflow-hidden"
|
||||
>
|
||||
<CodeHighlighter
|
||||
language={part.language || "python"}
|
||||
code={part.content}
|
||||
|
|
@ -194,12 +190,13 @@ function applySearchHighlights(container: HTMLElement, query: string): number {
|
|||
function scrollToSearchMatch(container: HTMLElement, index: number) {
|
||||
const marks = container.querySelectorAll("mark[data-search-highlight]")
|
||||
marks.forEach(m => {
|
||||
(m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]"
|
||||
;(m as HTMLElement).className = "bg-yellow-200 dark:bg-yellow-700/70 text-inherit rounded-[2px]"
|
||||
})
|
||||
|
||||
const target = marks[index] as HTMLElement | undefined
|
||||
if (target) {
|
||||
target.className = "bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500"
|
||||
target.className =
|
||||
"bg-orange-300 dark:bg-orange-600/70 text-inherit rounded-[2px] ring-2 ring-orange-400 dark:ring-orange-500"
|
||||
target.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||
}
|
||||
}
|
||||
|
|
@ -307,9 +304,10 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
|
|||
function navigateMatch(direction: "next" | "prev") {
|
||||
if (matchCount === 0) return
|
||||
|
||||
const next = direction === "next"
|
||||
? (currentMatch + 1) % matchCount
|
||||
: (currentMatch - 1 + matchCount) % matchCount
|
||||
const next =
|
||||
direction === "next"
|
||||
? (currentMatch + 1) % matchCount
|
||||
: (currentMatch - 1 + matchCount) % matchCount
|
||||
|
||||
setCurrentMatch(next)
|
||||
if (contentRef.current) {
|
||||
|
|
@ -411,7 +409,7 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
|
|||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="w-[95vw] max-w-[95vw] h-[90vh] max-h-[90vh] flex flex-col overflow-hidden"
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown={e => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
||||
e.preventDefault()
|
||||
setSearchOpen(true)
|
||||
|
|
@ -457,12 +455,13 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
|
|||
<p className="text-sm text-red-500">Failed to load debug data: {fetchError}</p>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="flex-1 flex flex-col min-h-0 mt-3 p-4">
|
||||
{loadingSkeleton}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col min-h-0 mt-3 p-4">{loadingSkeleton}</div>
|
||||
) : showResponse ? (
|
||||
<div className="flex-1 flex flex-col min-h-0 mt-3">
|
||||
<div ref={contentRef} className="flex-1 overflow-y-auto p-4 bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700">
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="flex-1 overflow-y-auto p-4 bg-white dark:bg-zinc-900 rounded-sm border border-zinc-200 dark:border-zinc-700"
|
||||
>
|
||||
{fetchedData.rawResponse ? (
|
||||
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
|
||||
{fetchedData.rawResponse}
|
||||
|
|
@ -473,7 +472,11 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs value={activeTab} onValueChange={v => setActiveTab(v as "user" | "system")} className="flex-1 flex flex-col min-h-0 mt-3">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={v => setActiveTab(v as "user" | "system")}
|
||||
className="flex-1 flex flex-col min-h-0 mt-3"
|
||||
>
|
||||
<TabsList className="flex-shrink-0 w-fit mx-auto">
|
||||
<TabsTrigger value="user">
|
||||
User Prompt
|
||||
|
|
@ -500,14 +503,12 @@ export const LLMCallDebugDialog = memo(function LLMCallDebugDialog({
|
|||
) : (
|
||||
<span className="text-zinc-400">No user prompt</span>
|
||||
)
|
||||
) : fetchedData.systemPrompt ? (
|
||||
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
|
||||
{fetchedData.systemPrompt}
|
||||
</pre>
|
||||
) : (
|
||||
fetchedData.systemPrompt ? (
|
||||
<pre className="text-sm whitespace-pre-wrap break-words text-zinc-800 dark:text-zinc-200 leading-relaxed font-mono">
|
||||
{fetchedData.systemPrompt}
|
||||
</pre>
|
||||
) : (
|
||||
<span className="text-zinc-400">No system prompt</span>
|
||||
)
|
||||
<span className="text-zinc-400">No system prompt</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async function getParser(): Promise<ParserType | null> {
|
|||
|
||||
export async function findFunctionInCode(
|
||||
code: string,
|
||||
functionName: string
|
||||
functionName: string,
|
||||
): Promise<FunctionLocation | null> {
|
||||
const { className, methodName } = parseQualifiedName(functionName)
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ export async function findFunctionInCode(
|
|||
async function findWithTreeSitter(
|
||||
code: string,
|
||||
className: string | undefined,
|
||||
methodName: string
|
||||
methodName: string,
|
||||
): Promise<FunctionLocation | null> {
|
||||
try {
|
||||
const parser = await getParser()
|
||||
|
|
@ -80,7 +80,7 @@ async function findWithTreeSitter(
|
|||
function findFunctionWithRegex(
|
||||
code: string,
|
||||
functionName: string,
|
||||
className?: string
|
||||
className?: string,
|
||||
): FunctionLocation | null {
|
||||
const lines = code.split("\n")
|
||||
|
||||
|
|
@ -92,14 +92,10 @@ function findFunctionWithRegex(
|
|||
function findMethodInClassWithRegex(
|
||||
lines: string[],
|
||||
className: string,
|
||||
methodName: string
|
||||
methodName: string,
|
||||
): FunctionLocation | null {
|
||||
const classPattern = new RegExp(
|
||||
`^(\\s*)class\\s+${escapeRegex(className)}\\s*[:(]`
|
||||
)
|
||||
const methodPattern = new RegExp(
|
||||
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(methodName)}\\s*\\(`
|
||||
)
|
||||
const classPattern = new RegExp(`^(\\s*)class\\s+${escapeRegex(className)}\\s*[:(]`)
|
||||
const methodPattern = new RegExp(`^(\\s*)(async\\s+)?def\\s+${escapeRegex(methodName)}\\s*\\(`)
|
||||
|
||||
let classIndent = -1
|
||||
let inClass = false
|
||||
|
|
@ -142,10 +138,10 @@ function findMethodInClassWithRegex(
|
|||
|
||||
function findStandaloneFunctionWithRegex(
|
||||
lines: string[],
|
||||
functionName: string
|
||||
functionName: string,
|
||||
): FunctionLocation | null {
|
||||
const functionPattern = new RegExp(
|
||||
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(`
|
||||
`^(\\s*)(async\\s+)?def\\s+${escapeRegex(functionName)}\\s*\\(`,
|
||||
)
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
|
|
@ -159,11 +155,7 @@ function findStandaloneFunctionWithRegex(
|
|||
return null
|
||||
}
|
||||
|
||||
function findBlockEnd(
|
||||
lines: string[],
|
||||
startLine: number,
|
||||
startIndent: number
|
||||
): FunctionLocation {
|
||||
function findBlockEnd(lines: string[], startLine: number, startIndent: number): FunctionLocation {
|
||||
for (let i = startLine; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim()
|
||||
if (!trimmed || trimmed.startsWith("#")) continue
|
||||
|
|
@ -199,11 +191,7 @@ function parseQualifiedName(functionName: string): {
|
|||
return { className: undefined, methodName: functionName }
|
||||
}
|
||||
|
||||
function findMethodInClass(
|
||||
rootNode: Node,
|
||||
className: string,
|
||||
methodName: string
|
||||
): Node | null {
|
||||
function findMethodInClass(rootNode: Node, className: string, methodName: string): Node | null {
|
||||
// Find all class definitions matching the class name
|
||||
function searchClasses(node: Node): Node | null {
|
||||
if (node.type === "class_definition") {
|
||||
|
|
@ -238,10 +226,7 @@ function findMethodInClass(
|
|||
}
|
||||
|
||||
function findFunctionNode(node: Node, functionName: string): Node | null {
|
||||
if (
|
||||
node.type === "function_definition" ||
|
||||
node.type === "async_function_definition"
|
||||
) {
|
||||
if (node.type === "function_definition" || node.type === "async_function_definition") {
|
||||
const nameNode = node.childForFieldName("name")
|
||||
if (nameNode && nameNode.text === functionName) {
|
||||
return node
|
||||
|
|
@ -264,4 +249,4 @@ function findFunctionNode(node: Node, functionName: string): Node | null {
|
|||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ interface RankingContentProps {
|
|||
export const RankingContent = memo(function RankingContent({ content }: RankingContentProps) {
|
||||
const strippedCodes = useMemo(
|
||||
() => new Map(content.rankings.map(item => [item.id, stripCodeHeader(item.code)])),
|
||||
[content.rankings]
|
||||
[content.rankings],
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
@ -27,7 +27,7 @@ export const RankingContent = memo(function RankingContent({ content }: RankingC
|
|||
|
||||
{content.rankings.length >= 1 && (
|
||||
<div className="space-y-4">
|
||||
{content.rankings.map((item) => (
|
||||
{content.rankings.map(item => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`rounded border ${
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import { useState, memo } from "react"
|
||||
import {
|
||||
ChevronDown,
|
||||
FlaskConical,
|
||||
CheckCircle2,
|
||||
} from "lucide-react"
|
||||
import { ChevronDown, FlaskConical, CheckCircle2 } from "lucide-react"
|
||||
import { CodeHighlighter, CODE_STYLE } from "./code-highlighter"
|
||||
import type { TimelineSectionContent } from "./timeline-types"
|
||||
|
||||
function getEmptyMessage(
|
||||
variant: "generated" | "instrumented" | "instrumentedPerf"
|
||||
): string {
|
||||
function getEmptyMessage(variant: "generated" | "instrumented" | "instrumentedPerf"): string {
|
||||
switch (variant) {
|
||||
case "generated":
|
||||
return "No generated test available"
|
||||
|
|
@ -31,7 +25,9 @@ interface TestContentProps {
|
|||
export const TestContent = memo(function TestContent({ content }: TestContentProps) {
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [expandedTest, setExpandedTest] = useState<number | null>(null)
|
||||
const [activeVariant, setActiveVariant] = useState<"generated" | "instrumented" | "instrumentedPerf">("generated")
|
||||
const [activeVariant, setActiveVariant] = useState<
|
||||
"generated" | "instrumented" | "instrumentedPerf"
|
||||
>("generated")
|
||||
|
||||
const testCount = content.testGroups.length
|
||||
const hasInstrumented = content.testGroups.some(g => g.instrumented)
|
||||
|
|
@ -70,15 +66,19 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
|
|||
className="px-3 py-1.5 text-xs rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
{showDetails ? "Hide Details" : "View Details"}
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${showDetails ? "" : "-rotate-90"}`} />
|
||||
<ChevronDown
|
||||
className={`h-3.5 w-3.5 transition-transform duration-200 ${showDetails ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className="space-y-3 pt-2 border-t border-zinc-200 dark:border-zinc-700">
|
||||
{content.testGroups.map((group) => {
|
||||
{content.testGroups.map(group => {
|
||||
const isExpanded = expandedTest === group.index
|
||||
const hasMultipleVariants = [group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length > 1
|
||||
const hasMultipleVariants =
|
||||
[group.generated, group.instrumented, group.instrumentedPerf].filter(Boolean).length >
|
||||
1
|
||||
|
||||
const currentCode = (() => {
|
||||
switch (activeVariant) {
|
||||
|
|
@ -94,7 +94,10 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
|
|||
})()
|
||||
|
||||
return (
|
||||
<div key={group.index} className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
<div
|
||||
key={group.index}
|
||||
className="rounded-md border border-zinc-200 dark:border-zinc-700 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => setExpandedTest(isExpanded ? null : group.index)}
|
||||
className="w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-800 flex items-center justify-between hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors"
|
||||
|
|
@ -124,9 +127,14 @@ export const TestContent = memo(function TestContent({ content }: TestContentPro
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-zinc-500">
|
||||
{group.generated?.lines ?? group.instrumented?.lines ?? group.instrumentedPerf?.lines} lines
|
||||
{group.generated?.lines ??
|
||||
group.instrumented?.lines ??
|
||||
group.instrumentedPerf?.lines}{" "}
|
||||
lines
|
||||
</span>
|
||||
<ChevronDown className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`} />
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 text-zinc-400 transition-transform duration-200 ${isExpanded ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,7 @@ interface TimelineChatProps {
|
|||
onClose: () => void
|
||||
}
|
||||
|
||||
export const TimelineChat = memo(function TimelineChat({
|
||||
traceId,
|
||||
onClose,
|
||||
}: TimelineChatProps) {
|
||||
export const TimelineChat = memo(function TimelineChat({ traceId, onClose }: TimelineChatProps) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||||
const [input, setInput] = useState("")
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
|
|
@ -63,7 +60,7 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
const controller = new AbortController()
|
||||
abortRef.current = controller
|
||||
|
||||
setMessages((prev) => [...prev, { role: "assistant", content: "" }])
|
||||
setMessages(prev => [...prev, { role: "assistant", content: "" }])
|
||||
setCompletedRounds([])
|
||||
setActiveSteps([])
|
||||
|
||||
|
|
@ -110,15 +107,17 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
// Handle typed events (new protocol)
|
||||
if (parsed.type === "tool_start") {
|
||||
setStatusMessage(null)
|
||||
setActiveSteps((prev) => {
|
||||
setActiveSteps(prev => {
|
||||
// If all previous steps are done, this is a new round — commit previous steps
|
||||
if (prev.length > 0 && prev.every((s) => s.status === "done")) {
|
||||
setCompletedRounds((rounds) => [...rounds, prev])
|
||||
return [{
|
||||
tool: parsed.tool,
|
||||
displayName: parsed.displayName ?? parsed.tool,
|
||||
status: "running",
|
||||
}]
|
||||
if (prev.length > 0 && prev.every(s => s.status === "done")) {
|
||||
setCompletedRounds(rounds => [...rounds, prev])
|
||||
return [
|
||||
{
|
||||
tool: parsed.tool,
|
||||
displayName: parsed.displayName ?? parsed.tool,
|
||||
status: "running",
|
||||
},
|
||||
]
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
|
|
@ -133,8 +132,8 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
}
|
||||
|
||||
if (parsed.type === "tool_result") {
|
||||
setActiveSteps((prev) =>
|
||||
prev.map((step) =>
|
||||
setActiveSteps(prev =>
|
||||
prev.map(step =>
|
||||
step.tool === parsed.tool && step.status === "running"
|
||||
? { ...step, status: "done", summary: parsed.summary, content: parsed.content }
|
||||
: step,
|
||||
|
|
@ -147,7 +146,7 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
const textContent = parsed.type === "text" ? parsed.text : parsed.text
|
||||
if (textContent) {
|
||||
setStatusMessage(null)
|
||||
setMessages((prev) => {
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
const last = updated[updated.length - 1]
|
||||
if (last?.role === "assistant") {
|
||||
|
|
@ -171,7 +170,7 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") return
|
||||
|
||||
setMessages((prev) => {
|
||||
setMessages(prev => {
|
||||
const updated = [...prev]
|
||||
const last = updated[updated.length - 1]
|
||||
if (last?.role === "assistant" && !last.content) {
|
||||
|
|
@ -184,9 +183,9 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
})
|
||||
} finally {
|
||||
// Commit any remaining active steps as a final completed round
|
||||
setActiveSteps((prev) => {
|
||||
setActiveSteps(prev => {
|
||||
if (prev.length > 0) {
|
||||
setCompletedRounds((rounds) => [...rounds, prev])
|
||||
setCompletedRounds(rounds => [...rounds, prev])
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
|
@ -203,7 +202,7 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
sendMessage()
|
||||
}
|
||||
},
|
||||
[sendMessage]
|
||||
[sendMessage],
|
||||
)
|
||||
|
||||
const stopStreaming = useCallback(() => {
|
||||
|
|
@ -213,8 +212,8 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
const [copied, setCopied] = useState(false)
|
||||
const exportChat = useCallback(() => {
|
||||
const text = messages
|
||||
.filter((m) => m.content)
|
||||
.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
|
||||
.filter(m => m.content)
|
||||
.map(m => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
|
||||
.join("\n\n")
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopied(true)
|
||||
|
|
@ -227,9 +226,7 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="h-4 w-4 text-zinc-500" />
|
||||
<span className="text-sm font-medium text-zinc-900 dark:text-white">
|
||||
Chat with Trace
|
||||
</span>
|
||||
<span className="text-sm font-medium text-zinc-900 dark:text-white">Chat with Trace</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{messages.length > 0 && (
|
||||
|
|
@ -260,7 +257,8 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
Ask about this optimization trace
|
||||
</p>
|
||||
<p className="text-xs text-zinc-400 dark:text-zinc-500">
|
||||
e.g. "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>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -297,13 +295,13 @@ export const TimelineChat = memo(function TimelineChat({
|
|||
<textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about this trace..."
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent max-h-32 min-h-[38px]"
|
||||
style={{ height: "38px" }}
|
||||
onInput={(e) => {
|
||||
onInput={e => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = "38px"
|
||||
target.style.height = `${Math.min(target.scrollHeight, 128)}px`
|
||||
|
|
@ -341,11 +339,11 @@ const ToolRoundBubble = memo(function ToolRoundBubble({
|
|||
steps: ToolStep[]
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const allDone = steps.every((s) => s.status === "done")
|
||||
const allDone = steps.every(s => s.status === "done")
|
||||
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set())
|
||||
|
||||
const toggleStep = useCallback((index: number) => {
|
||||
setExpandedSteps((prev) => {
|
||||
setExpandedSteps(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(index)) {
|
||||
next.delete(index)
|
||||
|
|
@ -397,7 +395,10 @@ const ToolRoundBubble = memo(function ToolRoundBubble({
|
|||
<span>
|
||||
{step.displayName}
|
||||
{step.summary && (
|
||||
<span className="text-zinc-400 dark:text-zinc-500"> — {step.summary}</span>
|
||||
<span className="text-zinc-400 dark:text-zinc-500">
|
||||
{" "}
|
||||
— {step.summary}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -458,7 +459,11 @@ const ToolResultContent = memo(function ToolResultContent({ content }: { content
|
|||
},
|
||||
// Render plain text content as preformatted when no markdown structure detected
|
||||
p({ children }) {
|
||||
return <p className="whitespace-pre-wrap break-words text-xs text-zinc-600 dark:text-zinc-300">{children}</p>
|
||||
return (
|
||||
<p className="whitespace-pre-wrap break-words text-xs text-zinc-600 dark:text-zinc-300">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export function parseAllCodeBlocks(markdown: string): ParsedCodeBlock[] {
|
|||
|
||||
export function findMatchingFile(
|
||||
files: ParsedCodeBlock[],
|
||||
targetPath: string | null
|
||||
targetPath: string | null,
|
||||
): ParsedCodeBlock | null {
|
||||
if (!targetPath || files.length === 0) {
|
||||
return files[0] || null
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import { MessageSquare } from "lucide-react"
|
|||
import { formatTime } from "./timeline-helpers"
|
||||
import { TimelineSectionCard } from "./timeline-section-card"
|
||||
import { TimelineChat } from "./timeline-chat"
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
ResizableHandle,
|
||||
} from "@/components/ui/resizable"
|
||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"
|
||||
import type { Layout } from "react-resizable-panels"
|
||||
import type { TimelineSection } from "./timeline-types"
|
||||
|
||||
|
|
@ -40,17 +36,25 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
try {
|
||||
const stored = localStorage.getItem("obs-chat-layout")
|
||||
if (stored) setSavedLayout(JSON.parse(stored))
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLayoutChanged = useCallback((layout: Layout) => {
|
||||
try { localStorage.setItem("obs-chat-layout", JSON.stringify(layout)) } catch { /* ignore */ }
|
||||
try {
|
||||
localStorage.setItem("obs-chat-layout", JSON.stringify(layout))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [])
|
||||
|
||||
function getSectionRef(index: number) {
|
||||
let cb = refCallbacks.current.get(index)
|
||||
if (!cb) {
|
||||
cb = (el: HTMLDivElement | null) => { sectionRefs.current[index] = el }
|
||||
cb = (el: HTMLDivElement | null) => {
|
||||
sectionRefs.current[index] = el
|
||||
}
|
||||
refCallbacks.current.set(index, cb)
|
||||
}
|
||||
return cb
|
||||
|
|
@ -62,7 +66,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
const root = chatOpen ? scrollContainerRef.current : null
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries => {
|
||||
let bestIndex = -1
|
||||
let bestDistance = Infinity
|
||||
|
||||
|
|
@ -76,7 +80,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
const sectionMiddle = rect.top + rect.height / 2
|
||||
const viewportH = root ? root.clientHeight : window.innerHeight
|
||||
const rootTop = root ? root.getBoundingClientRect().top : 0
|
||||
const distance = Math.abs((sectionMiddle - rootTop) - viewportH * 0.35)
|
||||
const distance = Math.abs(sectionMiddle - rootTop - viewportH * 0.35)
|
||||
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance
|
||||
|
|
@ -92,7 +96,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
root,
|
||||
threshold: [0.5],
|
||||
rootMargin: "-10% 0px -55% 0px",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
sectionRefs.current.forEach((ref, index) => {
|
||||
|
|
@ -105,7 +109,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
return () => observer.disconnect()
|
||||
}, [sections.length, chatOpen])
|
||||
|
||||
const toggleChat = useCallback(() => setChatOpen((prev) => !prev), [])
|
||||
const toggleChat = useCallback(() => setChatOpen(prev => !prev), [])
|
||||
const closeChat = useCallback(() => setChatOpen(false), [])
|
||||
|
||||
if (sections.length === 0) {
|
||||
|
|
@ -118,8 +122,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
|
||||
const activeSection = sections[activeIndex]
|
||||
const shouldExpandContainer =
|
||||
activeSection?.content.type === "candidate" ||
|
||||
activeSection?.content.type === "refinement"
|
||||
activeSection?.content.type === "candidate" || activeSection?.content.type === "refinement"
|
||||
|
||||
const timelineSections = (
|
||||
<>
|
||||
|
|
@ -130,11 +133,7 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
</div>
|
||||
|
||||
{sections.map((section, index) => (
|
||||
<div
|
||||
key={section.id}
|
||||
ref={getSectionRef(index)}
|
||||
className="scroll-mt-24"
|
||||
>
|
||||
<div key={section.id} ref={getSectionRef(index)} className="scroll-mt-24">
|
||||
<TimelineSectionCard
|
||||
section={section}
|
||||
isActive={index === activeIndex}
|
||||
|
|
@ -148,16 +147,16 @@ export const TimelinePageView = memo(function TimelinePageView({
|
|||
<div className="absolute right-6 top-0 h-6 w-px bg-zinc-200 dark:bg-zinc-700" />
|
||||
<div className="absolute right-4 top-6 w-4 h-4 rounded-full bg-zinc-300 dark:bg-zinc-600 border-2 border-white dark:border-zinc-900 z-10" />
|
||||
<div className="mr-14 py-4 text-right">
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
End
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500 dark:text-zinc-400">End</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
const header = (
|
||||
<div className={`${chatOpen ? "flex-shrink-0" : "sticky top-0"} z-30 bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700`}>
|
||||
<div
|
||||
className={`${chatOpen ? "flex-shrink-0" : "sticky top-0"} z-30 bg-white dark:bg-zinc-900 border-b border-zinc-200 dark:border-zinc-700`}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import React, { memo } from "react"
|
||||
import {
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
} from "lucide-react"
|
||||
import { Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react"
|
||||
import { TYPE_CONFIG, formatTime } from "./timeline-helpers"
|
||||
import { LLMCallDebugDialog } from "./llm-call-debug-dialog"
|
||||
import { TestContent } from "./test-content"
|
||||
|
|
@ -42,11 +37,7 @@ export const TimelineSectionCard = memo(function TimelineSectionCard({
|
|||
const Icon = config.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${
|
||||
isActive ? "opacity-100" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className={`relative ${isActive ? "opacity-100" : "opacity-60"}`}>
|
||||
<div className="absolute right-6 top-0 bottom-0 w-px bg-zinc-200 dark:bg-zinc-700" />
|
||||
|
||||
<div className="mr-14 mb-6">
|
||||
|
|
@ -112,19 +103,12 @@ export const TimelineSectionCard = memo(function TimelineSectionCard({
|
|||
</div>
|
||||
|
||||
<div className="p-4 bg-white dark:bg-zinc-800">
|
||||
{section.content.type === "tests" && (
|
||||
<TestContent content={section.content} />
|
||||
)}
|
||||
{(section.content.type === "candidate" ||
|
||||
section.content.type === "refinement") && (
|
||||
{section.content.type === "tests" && <TestContent content={section.content} />}
|
||||
{(section.content.type === "candidate" || section.content.type === "refinement") && (
|
||||
<CandidateContent content={section.content} isActive={isActive} />
|
||||
)}
|
||||
{section.content.type === "ranking" && (
|
||||
<RankingContent content={section.content} />
|
||||
)}
|
||||
{section.content.type === "summary" && (
|
||||
<SummaryContent content={section.content} />
|
||||
)}
|
||||
{section.content.type === "ranking" && <RankingContent content={section.content} />}
|
||||
{section.content.type === "summary" && <SummaryContent content={section.content} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@ export interface LLMCallDebugData {
|
|||
|
||||
export interface TimelineSection {
|
||||
id: string
|
||||
type: "test_generation" | "optimization" | "line_profiler" | "refinement" | "adaptive" | "ranking" | "summary"
|
||||
type:
|
||||
| "test_generation"
|
||||
| "optimization"
|
||||
| "line_profiler"
|
||||
| "refinement"
|
||||
| "adaptive"
|
||||
| "ranking"
|
||||
| "summary"
|
||||
title: string
|
||||
subtitle?: string
|
||||
timestamp: number
|
||||
|
|
@ -26,10 +33,37 @@ export interface TestGroup {
|
|||
|
||||
export type TimelineSectionContent =
|
||||
| { type: "tests"; testGroups: TestGroup[]; testFramework?: string }
|
||||
| { type: "candidate"; code: string; originalCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
|
||||
| { type: "refinement"; code: string; parentCode: string | null; explanation?: string; rank?: number; isBest?: boolean }
|
||||
| { type: "ranking"; explanation: string; rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>; usedForPr: boolean }
|
||||
| { type: "summary"; metrics: { totalCost: number; totalTokens: number; totalDuration: number; candidatesCount: number } }
|
||||
| {
|
||||
type: "candidate"
|
||||
code: string
|
||||
originalCode: string | null
|
||||
explanation?: string
|
||||
rank?: number
|
||||
isBest?: boolean
|
||||
}
|
||||
| {
|
||||
type: "refinement"
|
||||
code: string
|
||||
parentCode: string | null
|
||||
explanation?: string
|
||||
rank?: number
|
||||
isBest?: boolean
|
||||
}
|
||||
| {
|
||||
type: "ranking"
|
||||
explanation: string
|
||||
rankings: Array<{ id: string; rank: number; label: string; code: string; isBest: boolean }>
|
||||
usedForPr: boolean
|
||||
}
|
||||
| {
|
||||
type: "summary"
|
||||
metrics: {
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
totalDuration: number
|
||||
candidatesCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransformInput {
|
||||
calls: Array<{
|
||||
|
|
@ -80,9 +114,10 @@ export interface TransformInput {
|
|||
usedForPr: boolean
|
||||
}
|
||||
|
||||
export function transformToTimelineSections(
|
||||
input: TransformInput
|
||||
): { sections: TimelineSection[]; totalDuration: number } {
|
||||
export function transformToTimelineSections(input: TransformInput): {
|
||||
sections: TimelineSection[]
|
||||
totalDuration: number
|
||||
} {
|
||||
const {
|
||||
calls,
|
||||
optimizationCandidates,
|
||||
|
|
@ -132,7 +167,7 @@ export function transformToTimelineSections(
|
|||
const maxTestIndex = Math.max(
|
||||
generatedTests.length,
|
||||
instrumentedTests.length,
|
||||
instrumentedPerfTests.length
|
||||
instrumentedPerfTests.length,
|
||||
)
|
||||
|
||||
const genMap = new Map(generatedTests.map(t => [t.index, t]))
|
||||
|
|
@ -166,9 +201,7 @@ export function transformToTimelineSections(
|
|||
|
||||
if (testCalls.length > 0 || testGroups.length > 0) {
|
||||
const firstTestCall = testCalls[0]
|
||||
const firstTimestamp = firstTestCall
|
||||
? timestampMap.get(firstTestCall.id)! - minTime
|
||||
: 0
|
||||
const firstTimestamp = firstTestCall ? timestampMap.get(firstTestCall.id)! - minTime : 0
|
||||
|
||||
const totalTestDuration = testCalls.reduce((sum, c) => sum + (c.latency_ms ?? 0), 0)
|
||||
const totalTestCost = testCalls.reduce((sum, c) => sum + (c.llm_cost ?? 0), 0)
|
||||
|
|
@ -201,7 +234,12 @@ export function transformToTimelineSections(
|
|||
})
|
||||
}
|
||||
|
||||
const allCandidates = [...optimizationCandidates, ...lineProfilerCandidates, ...refinementCandidates, ...adaptiveCandidates]
|
||||
const allCandidates = [
|
||||
...optimizationCandidates,
|
||||
...lineProfilerCandidates,
|
||||
...refinementCandidates,
|
||||
...adaptiveCandidates,
|
||||
]
|
||||
const allCandidatesById = new Map(allCandidates.map(c => [c.id, c]))
|
||||
|
||||
// Pre-compute ranking data (identical for all ranking calls)
|
||||
|
|
@ -252,7 +290,8 @@ export function transformToTimelineSections(
|
|||
title: `Optimization Candidate ${candidate.index}`,
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
status:
|
||||
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
|
|
@ -279,7 +318,8 @@ export function transformToTimelineSections(
|
|||
subtitle: "Guided by profiling data",
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
status:
|
||||
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
|
|
@ -306,9 +346,10 @@ export function transformToTimelineSections(
|
|||
let parentLabel: string | undefined
|
||||
if (parentCandidate) {
|
||||
const source = (parentCandidate as { source?: string }).source
|
||||
parentLabel = source === "REFINE"
|
||||
? `From Refinement ${parentCandidate.index}`
|
||||
: `From Candidate ${parentCandidate.index}`
|
||||
parentLabel =
|
||||
source === "REFINE"
|
||||
? `From Refinement ${parentCandidate.index}`
|
||||
: `From Candidate ${parentCandidate.index}`
|
||||
}
|
||||
sections.push({
|
||||
id: call.id,
|
||||
|
|
@ -317,7 +358,8 @@ export function transformToTimelineSections(
|
|||
subtitle: parentLabel,
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
status:
|
||||
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
|
|
@ -359,7 +401,8 @@ export function transformToTimelineSections(
|
|||
subtitle: parentLabel ?? "Informed by full optimization history",
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
status:
|
||||
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
|
|
@ -382,7 +425,8 @@ export function transformToTimelineSections(
|
|||
subtitle: "Selecting the best optimization",
|
||||
timestamp,
|
||||
duration: call.latency_ms ?? undefined,
|
||||
status: call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
status:
|
||||
call.status === "success" ? "success" : call.status === "failed" ? "failed" : "partial",
|
||||
model: call.model_name,
|
||||
cost: call.llm_cost,
|
||||
tokens: call.total_tokens,
|
||||
|
|
@ -399,7 +443,7 @@ export function transformToTimelineSections(
|
|||
|
||||
// Fallback: create sections for adaptive candidates not matched to an LLM call
|
||||
// (the backend currently logs adaptive LLM calls with an empty call_type)
|
||||
const matchedAdaptiveCount = (callIndexByType.get("adaptive_optimize") ?? 0)
|
||||
const matchedAdaptiveCount = callIndexByType.get("adaptive_optimize") ?? 0
|
||||
for (let i = matchedAdaptiveCount; i < adaptiveCandidates.length; i++) {
|
||||
const candidate = adaptiveCandidates[i]
|
||||
const rank = candidateRankMap[candidate.id]
|
||||
|
|
@ -451,7 +495,7 @@ export function transformToTimelineSections(
|
|||
|
||||
const candidateTypeSet = new Set(["optimization", "line_profiler", "refinement", "adaptive"])
|
||||
const sectionSortIndex = new Map(
|
||||
sections.map(s => [s, parseInt(s.title.match(/\d+$/)?.[0] ?? "0", 10)])
|
||||
sections.map(s => [s, parseInt(s.title.match(/\d+$/)?.[0] ?? "0", 10)]),
|
||||
)
|
||||
|
||||
sections.sort((a, b) => {
|
||||
|
|
@ -465,4 +509,4 @@ export function transformToTimelineSections(
|
|||
})
|
||||
|
||||
return { sections, totalDuration }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ interface TraceSearchProps {
|
|||
hasResults?: boolean
|
||||
}
|
||||
|
||||
export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults = false }: TraceSearchProps) {
|
||||
export function TraceSearch({
|
||||
initialTraceId = "",
|
||||
isLoading = false,
|
||||
hasResults = false,
|
||||
}: TraceSearchProps) {
|
||||
const [traceId, setTraceId] = useState(initialTraceId)
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -95,4 +99,4 @@ export function TraceSearch({ initialTraceId = "", isLoading = false, hasResults
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@ export function getTraceSource(eventType: string | null): string {
|
|||
}
|
||||
|
||||
return eventType
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default function ObservabilityLoading() {
|
|||
|
||||
{/* Timeline items skeleton */}
|
||||
<div className="space-y-8">
|
||||
{[1, 2, 3].map((i) => (
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<div className="w-5 h-5 rounded-full bg-zinc-200 dark:bg-zinc-700 animate-pulse" />
|
||||
<div className="flex-1">
|
||||
|
|
@ -36,4 +36,4 @@ export default function ObservabilityLoading() {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,7 @@ export function Breadcrumb({
|
|||
}
|
||||
|
||||
const breadcrumbs = generateBreadcrumbs()
|
||||
const visibleBreadcrumbs =
|
||||
hideRoot && breadcrumbs.length > 1 ? breadcrumbs.slice(1) : breadcrumbs
|
||||
const visibleBreadcrumbs = hideRoot && breadcrumbs.length > 1 ? breadcrumbs.slice(1) : breadcrumbs
|
||||
|
||||
// Don't show breadcrumbs if we're on the root
|
||||
if (visibleBreadcrumbs.length <= 1) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ export function ObservabilityNav() {
|
|||
if (pathname === "/observability") {
|
||||
setViewMode("timeline")
|
||||
localStorage.setItem("observability-view-mode", "timeline")
|
||||
} else if (pathname.startsWith("/observability/traces") || pathname.startsWith("/observability/llm-calls")) {
|
||||
} else if (
|
||||
pathname.startsWith("/observability/traces") ||
|
||||
pathname.startsWith("/observability/llm-calls")
|
||||
) {
|
||||
setViewMode("classic")
|
||||
localStorage.setItem("observability-view-mode", "classic")
|
||||
} else {
|
||||
|
|
@ -95,7 +98,7 @@ export function ObservabilityNav() {
|
|||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
viewMode === "classic"
|
||||
? "bg-white dark:bg-gray-800 text-blue-700 dark:text-blue-300 shadow-sm"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Layers className="h-4 w-4" />
|
||||
|
|
@ -107,7 +110,7 @@ export function ObservabilityNav() {
|
|||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
viewMode === "timeline"
|
||||
? "bg-white dark:bg-gray-800 text-blue-700 dark:text-blue-300 shadow-sm"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -41,7 +41,14 @@ interface StatCardProps {
|
|||
className?: string
|
||||
}
|
||||
|
||||
export function StatCard({ label, value, helpText, icon, variant = "default", className }: StatCardProps) {
|
||||
export function StatCard({
|
||||
label,
|
||||
value,
|
||||
helpText,
|
||||
icon,
|
||||
variant = "default",
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
const IconComponent = icon ? iconMap[icon] : null
|
||||
|
||||
return (
|
||||
|
|
@ -56,15 +63,15 @@ export function StatCard({ label, value, helpText, icon, variant = "default", cl
|
|||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{IconComponent && <IconComponent className="h-4 w-4 text-gray-500 dark:text-gray-400" />}
|
||||
{IconComponent && (
|
||||
<IconComponent className="h-4 w-4 text-gray-500 dark:text-gray-400" />
|
||||
)}
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 font-medium flex items-center gap-1.5">
|
||||
{label}
|
||||
{helpText && <InfoIcon content={helpText} side="top" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{value}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -610,7 +610,8 @@ export function RenderStepContent({
|
|||
</h4>
|
||||
<ApiKeyCard label="GitHub Actions Setup" value="codeflash init-actions" />
|
||||
<p className="mt-2 text-sm text-muted-foreground font-medium">
|
||||
Automate optimization of your future code by setting up Codeflash Github Actions{" "}
|
||||
Automate optimization of your future code by setting up Codeflash Github
|
||||
Actions{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { type JSX } from "react"
|
|||
|
||||
export function SignOut(): JSX.Element {
|
||||
return (
|
||||
|
||||
<a href="/auth/logout">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -23,10 +23,8 @@ const badgeVariants = cva(
|
|||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ const buttonVariants = cva(
|
|||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ interface FormFieldContextValue<
|
|||
> {
|
||||
name: TName
|
||||
}
|
||||
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
|
|
@ -37,7 +37,6 @@ const FormField = <
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
|
|
@ -45,7 +44,6 @@ const useFormField = () => {
|
|||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
|
@ -65,7 +63,7 @@ const useFormField = () => {
|
|||
interface FormItemContextValue {
|
||||
id: string
|
||||
}
|
||||
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
|
||||
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
|
|
@ -142,7 +140,6 @@ const FormMessage = React.forwardRef<
|
|||
const { error, formMessageId } = useFormField()
|
||||
const body = error != null ? String(error?.message) : children
|
||||
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,9 @@ import {
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: GroupProps) => (
|
||||
const ResizablePanelGroup = ({ className, ...props }: GroupProps) => (
|
||||
<Group
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayNam
|
|||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { cn } from "@/lib/utils"
|
|||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
|
||||
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,6 @@ function dispatch(action: Action): void {
|
|||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import type { Span } from "@sentry/core"
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { withTiming } from "../server-action-timing"
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ describe("withTiming", () => {
|
|||
beforeEach(() => {
|
||||
mockSetAttribute = vi.fn()
|
||||
vi.mocked(Sentry.startSpan).mockImplementation((_opts, callback) =>
|
||||
callback({ setAttribute: mockSetAttribute } as any),
|
||||
callback({ setAttribute: mockSetAttribute } as unknown as Span),
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -50,10 +51,7 @@ describe("withTiming", () => {
|
|||
const wrapped = withTiming("test", vi.fn().mockResolvedValue(null))
|
||||
await wrapped()
|
||||
|
||||
expect(mockSetAttribute).toHaveBeenCalledWith(
|
||||
"server_action.duration_ms",
|
||||
250,
|
||||
)
|
||||
expect(mockSetAttribute).toHaveBeenCalledWith("server_action.duration_ms", 250)
|
||||
nowSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
|
@ -67,12 +65,8 @@ describe("withTiming", () => {
|
|||
const wrapped = withTiming("slowAction", vi.fn().mockResolvedValue(null))
|
||||
await wrapped()
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("slowAction"),
|
||||
)
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("1500"),
|
||||
)
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("slowAction"))
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("1500"))
|
||||
|
||||
warnSpy.mockRestore()
|
||||
nowSpy.mockRestore()
|
||||
|
|
@ -136,10 +130,7 @@ describe("withTiming", () => {
|
|||
const wrapped = withTiming("errAction", vi.fn().mockRejectedValue(new Error("oops")))
|
||||
await expect(wrapped()).rejects.toThrow()
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("errAction"),
|
||||
expect.any(Error),
|
||||
)
|
||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("errAction"), expect.any(Error))
|
||||
|
||||
errorSpy.mockRestore()
|
||||
nowSpy.mockRestore()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type ActionResponse<T = any> = {
|
||||
export type ActionResponse<T = unknown> = {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
|
|
@ -12,7 +12,7 @@ export function createSuccessResponse<T>(data: T): ActionResponse<T> {
|
|||
}
|
||||
}
|
||||
|
||||
export function createErrorResponse(error: string): ActionResponse {
|
||||
export function createErrorResponse<T = unknown>(error: string): ActionResponse<T> {
|
||||
return {
|
||||
success: false,
|
||||
data: undefined,
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@ let _auth0: Auth0Client | undefined
|
|||
function getAuth0Client(): Auth0Client {
|
||||
if (!_auth0) {
|
||||
if (!auth0Domain) {
|
||||
// During build/CI prerendering, Auth0 env vars aren't set.
|
||||
// Return a stub — getSession() returning null is correct (no user).
|
||||
return { getSession: async () => null } as unknown as Auth0Client
|
||||
throw new Error("AUTH0_DOMAIN (or AUTH0_ISSUER_BASE_URL) environment variable is required")
|
||||
}
|
||||
_auth0 = new Auth0Client({
|
||||
domain: auth0Domain,
|
||||
|
|
@ -64,7 +62,7 @@ function getAuth0Client(): Auth0Client {
|
|||
const match = errorMessage.match(re)
|
||||
if (match != null) {
|
||||
const userNickname = match[2]
|
||||
return redirectTo(`/waitlist?username=${userNickname}`)
|
||||
return redirectTo(`/waitlist?username=${encodeURIComponent(userNickname)}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +122,7 @@ function getAuth0Client(): Auth0Client {
|
|||
export const auth0: Auth0Client = new Proxy({} as Auth0Client, {
|
||||
get(_, prop) {
|
||||
const client = getAuth0Client()
|
||||
const value = (client as any)[prop]
|
||||
const value = client[prop as keyof Auth0Client]
|
||||
return typeof value === "function" ? value.bind(client) : value
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -64,7 +64,10 @@ export function parseLineProfilerResults(rawResults: string): LineProfilerReport
|
|||
// Detect format: JS format starts with "Line Profile Results:" or has space-separated columns
|
||||
const isJsFormat =
|
||||
rawResults.includes("Line Profile Results:") ||
|
||||
(rawResults.includes("Line") && rawResults.includes("Hits") && rawResults.includes("Content") && !rawResults.includes("|"))
|
||||
(rawResults.includes("Line") &&
|
||||
rawResults.includes("Hits") &&
|
||||
rawResults.includes("Content") &&
|
||||
!rawResults.includes("|"))
|
||||
|
||||
if (isJsFormat) {
|
||||
return parseJsLineProfilerResults(rawResults)
|
||||
|
|
@ -193,7 +196,11 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
|
|||
|
||||
// Detect table header - handle variable whitespace in cells
|
||||
// Tabulate may produce "| Hits |" with extra spaces
|
||||
if (trimmedLine.includes("Hits") && trimmedLine.includes("Line Contents") && trimmedLine.startsWith("|")) {
|
||||
if (
|
||||
trimmedLine.includes("Hits") &&
|
||||
trimmedLine.includes("Line Contents") &&
|
||||
trimmedLine.startsWith("|")
|
||||
) {
|
||||
inTable = true
|
||||
headerPassed = false
|
||||
continue
|
||||
|
|
@ -210,7 +217,7 @@ function parsePythonLineProfilerResults(rawResults: string): LineProfilerReport
|
|||
// Split by | but preserve the original line for extracting code with whitespace
|
||||
const rawParts = trimmedLine.split("|")
|
||||
// Trim stats columns but NOT the code column
|
||||
const statParts = rawParts.slice(1, 5).map((p) => p.trim())
|
||||
const statParts = rawParts.slice(1, 5).map(p => p.trim())
|
||||
// Keep code with original whitespace - join remaining parts (code may contain pipes)
|
||||
// Use " " for empty lines to preserve blank lines in display
|
||||
const codePart = rawParts.slice(5, -1).join("|") || " "
|
||||
|
|
@ -252,9 +259,7 @@ export const HEAT_THRESHOLDS = {
|
|||
* Get the heat level for a given percent time
|
||||
* Returns a class suffix for CSS styling
|
||||
*/
|
||||
export function getHeatLevel(
|
||||
percentTime: number,
|
||||
): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
|
||||
export function getHeatLevel(percentTime: number): "cold" | "hot-1" | "hot-2" | "hot-3" | "hot-4" {
|
||||
if (percentTime >= HEAT_THRESHOLDS.HOT_4) return "hot-4"
|
||||
if (percentTime >= HEAT_THRESHOLDS.HOT_3) return "hot-3"
|
||||
if (percentTime >= HEAT_THRESHOLDS.HOT_2) return "hot-2"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function getModifiedCodeForTrace(
|
|||
}
|
||||
|
||||
const experimentMetadata = optimizationFeature.experiment_metadata as ExperimentMetadata | null
|
||||
const additionalMetadata = (optimizationFeature.metadata as any) || {}
|
||||
const additionalMetadata = (optimizationFeature.metadata as Record<string, unknown>) || {}
|
||||
|
||||
// Get modified code from metadata if available
|
||||
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
|
||||
|
|
@ -99,7 +99,7 @@ export async function hasModifiedCode(traceId: string): Promise<boolean> {
|
|||
return false
|
||||
}
|
||||
|
||||
const additionalMetadata = (optimizationFeature.metadata as any) || {}
|
||||
const additionalMetadata = (optimizationFeature.metadata as Record<string, unknown>) || {}
|
||||
const modifiedCode = additionalMetadata.modifiedCode as { [key: string]: string } | undefined
|
||||
|
||||
return !!(modifiedCode && Object.keys(modifiedCode).length > 0)
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ export function getCallSource(
|
|||
eventType: string | null,
|
||||
context: Record<string, unknown> | null,
|
||||
): string {
|
||||
if (
|
||||
context &&
|
||||
typeof context === "object" &&
|
||||
!Array.isArray(context) &&
|
||||
"source" in context
|
||||
) {
|
||||
if (context && typeof context === "object" && !Array.isArray(context) && "source" in context) {
|
||||
return String(context.source)
|
||||
}
|
||||
if (eventType) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,31 @@
|
|||
import { prisma } from "@codeflash-ai/common"
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
|
||||
interface PrismaQueryEvent {
|
||||
timestamp: Date
|
||||
query: string
|
||||
params: string
|
||||
duration: number
|
||||
target: string
|
||||
}
|
||||
|
||||
interface PrismaLogEvent {
|
||||
timestamp: Date
|
||||
message: string
|
||||
target: string
|
||||
}
|
||||
|
||||
interface PrismaClientWithEvents {
|
||||
$on(event: "query", listener: (e: PrismaQueryEvent) => void): void
|
||||
$on(event: "warn" | "error", listener: (e: PrismaLogEvent) => void): void
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production"
|
||||
const SLOW_QUERY_THRESHOLD_MS = 500
|
||||
|
||||
// Log slow queries in development
|
||||
if (!isProduction) {
|
||||
;(prisma as any).$on("query", (e: any) => {
|
||||
;(prisma as unknown as PrismaClientWithEvents).$on("query", (e: PrismaQueryEvent) => {
|
||||
if (e.duration > SLOW_QUERY_THRESHOLD_MS) {
|
||||
console.warn(`[Prisma] Slow query (${e.duration}ms): ${e.query}`)
|
||||
}
|
||||
|
|
@ -14,10 +33,10 @@ if (!isProduction) {
|
|||
}
|
||||
|
||||
// Forward Prisma warnings and errors to Sentry
|
||||
;(prisma as any).$on("warn", (e: any) => {
|
||||
;(prisma as unknown as PrismaClientWithEvents).$on("warn", (e: PrismaLogEvent) => {
|
||||
console.warn("[Prisma] Warning:", e.message)
|
||||
})
|
||||
;(prisma as any).$on("error", (e: any) => {
|
||||
;(prisma as unknown as PrismaClientWithEvents).$on("error", (e: PrismaLogEvent) => {
|
||||
console.error("[Prisma] Error:", e.message)
|
||||
Sentry.captureException(new Error(`Prisma error: ${e.message}`))
|
||||
})
|
||||
|
|
|
|||
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,
|
||||
},
|
||||
},
|
||||
async (span) => {
|
||||
async span => {
|
||||
const res = await fn(...args)
|
||||
const durationMs = performance.now() - start
|
||||
|
||||
|
|
@ -43,10 +43,7 @@ export function withTiming<TArgs extends unknown[], TReturn>(
|
|||
return result
|
||||
} catch (error) {
|
||||
const durationMs = performance.now() - start
|
||||
console.error(
|
||||
`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`,
|
||||
error,
|
||||
)
|
||||
console.error(`[ServerAction] ${actionName} failed after ${durationMs.toFixed(0)}ms:`, error)
|
||||
Sentry.captureException(error, {
|
||||
tags: { server_action: actionName },
|
||||
extra: { duration_ms: durationMs },
|
||||
|
|
|
|||
|
|
@ -37,8 +37,10 @@ export class GithubService {
|
|||
)
|
||||
}
|
||||
}
|
||||
private mapGitHubUserSearchResult(githubUsers: any): GitHubUserSearchResult[] {
|
||||
return githubUsers.map((user: any) => ({
|
||||
private mapGitHubUserSearchResult(
|
||||
githubUsers: { login: string; id: number; avatar_url: string }[],
|
||||
): GitHubUserSearchResult[] {
|
||||
return githubUsers.map(user => ({
|
||||
username: user.login,
|
||||
githubUserId: user.id,
|
||||
avatarUrl: user.avatar_url,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ export async function getRepositoriesForAccountCached(
|
|||
return dedup(`repos:${cacheKey}`, async () => {
|
||||
const repoIdsCacheKey = `${cacheKey}_ids`
|
||||
|
||||
const cachedRepos = await memoryCache.getItem<any>(cacheKey)
|
||||
const cachedRepos =
|
||||
await memoryCache.getItem<Awaited<ReturnType<typeof getRepositoriesForAccount>>>(cacheKey)
|
||||
const cachedRepoIds = await memoryCache.getItem<string[]>(repoIdsCacheKey)
|
||||
if (cachedRepos && cachedRepoIds) {
|
||||
return { repoIds: cachedRepoIds, repos: cachedRepos }
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ SyntaxHighlighter.registerLanguage("markup", markup)
|
|||
SyntaxHighlighter.registerLanguage("bash", bash)
|
||||
SyntaxHighlighter.registerLanguage("jsx", jsx)
|
||||
SyntaxHighlighter.registerLanguage("tsx", tsx)
|
||||
const mockPlaintext = (prism: any) => {
|
||||
const mockPlaintext = (prism: { languages: Record<string, Record<string, unknown>> }) => {
|
||||
prism.languages.plaintext = {}
|
||||
prism.languages.text = {}
|
||||
}
|
||||
mockPlaintext.displayName = "plaintext"
|
||||
mockPlaintext.aliases = ["text"]
|
||||
|
||||
SyntaxHighlighter.registerLanguage("plaintext", mockPlaintext as any)
|
||||
SyntaxHighlighter.registerLanguage("text", mockPlaintext as any)
|
||||
SyntaxHighlighter.registerLanguage("plaintext", mockPlaintext as typeof python)
|
||||
SyntaxHighlighter.registerLanguage("text", mockPlaintext as typeof python)
|
||||
|
||||
export { SyntaxHighlighter }
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ export interface PrCommentFields {
|
|||
report_table?: Record<string, { failed: number; passed: number }>
|
||||
function_name?: string // The Python function optimized
|
||||
original_runtime?: string
|
||||
benchmark_details?: any
|
||||
benchmark_details?: {
|
||||
benchmark_name: string
|
||||
test_function: string
|
||||
original_timing: number
|
||||
expected_new_timing: number
|
||||
speedup_percent: number
|
||||
}[]
|
||||
optimization_explanation?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ vi.mock("@codeflash-ai/common", () => {
|
|||
// Mock: @sentry/nextjs
|
||||
// ---------------------------------------------------------------------------
|
||||
vi.mock("@sentry/nextjs", () => ({
|
||||
startSpan: vi.fn((_opts: any, callback: any) => callback({ setAttribute: vi.fn() })),
|
||||
startSpan: vi.fn(
|
||||
(
|
||||
_opts: Record<string, unknown>,
|
||||
callback: (span: { setAttribute: ReturnType<typeof vi.fn> }) => unknown,
|
||||
) => callback({ setAttribute: vi.fn() }),
|
||||
),
|
||||
captureException: vi.fn(),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
|
@ -23,9 +19,7 @@
|
|||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
|
@ -36,7 +30,5 @@
|
|||
"*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import path from "path"
|
||||
import type { PluginOption } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()] as any,
|
||||
plugins: [react()] as PluginOption[],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ importers:
|
|||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@sentry/nextjs':
|
||||
specifier: ^10.38.0
|
||||
version: 10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1)
|
||||
version: 10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1(esbuild@0.27.7))
|
||||
'@sentry/opentelemetry':
|
||||
specifier: ^10.47.0
|
||||
version: 10.48.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
|
|
@ -273,6 +273,9 @@ importers:
|
|||
dompurify:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
ioredis:
|
||||
specifier: ^5.10.1
|
||||
version: 5.10.1
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.3
|
||||
|
|
@ -361,6 +364,9 @@ importers:
|
|||
'@next/bundle-analyzer':
|
||||
specifier: ^16.2.2
|
||||
version: 16.2.3
|
||||
'@sentry/core':
|
||||
specifier: ^10.48.0
|
||||
version: 10.48.0
|
||||
'@testing-library/react':
|
||||
specifier: ^16.0.0
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
|
|
@ -375,7 +381,7 @@ importers:
|
|||
version: 5.5.2
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.3.1
|
||||
version: 4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.7.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
autoprefixer:
|
||||
specifier: ^10.0.1
|
||||
version: 10.4.27(postcss@8.5.9)
|
||||
|
|
@ -409,9 +415,12 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^8.0.8
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
optionalDependencies:
|
||||
tree-sitter-cli:
|
||||
specifier: ^0.26.3
|
||||
|
|
@ -1300,6 +1309,9 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@ioredis/commands@1.5.1':
|
||||
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
|
||||
|
||||
'@istanbuljs/load-nyc-config@1.1.0':
|
||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1415,6 +1427,12 @@ packages:
|
|||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.3':
|
||||
resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==}
|
||||
peerDependencies:
|
||||
'@emnapi/core': ^1.7.1
|
||||
'@emnapi/runtime': ^1.7.1
|
||||
|
||||
'@next/bundle-analyzer@16.2.3':
|
||||
resolution: {integrity: sha512-aDwW4f4SVqbQDWzSBHQJ1KI6H+lx8oX/vS3xGqzLajUu+KQb7uakK88AIMvRIf7TlIonce67g594rzpxvBuJIw==}
|
||||
|
||||
|
|
@ -2204,6 +2222,9 @@ packages:
|
|||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
|
||||
'@oxc-project/types@0.124.0':
|
||||
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
|
||||
|
||||
'@panva/hkdf@1.2.1':
|
||||
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
|
||||
|
||||
|
|
@ -2751,9 +2772,107 @@ packages:
|
|||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.1':
|
||||
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
|
||||
engines: {node: '>=16.0.0 || 14 >= 14.17'}
|
||||
|
|
@ -4028,6 +4147,10 @@ packages:
|
|||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
|
|
@ -5143,6 +5266,10 @@ packages:
|
|||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ioredis@5.10.1:
|
||||
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
|
|
@ -5609,6 +5736,80 @@ packages:
|
|||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.32.0:
|
||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -5640,9 +5841,15 @@ packages:
|
|||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
|
||||
lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
|
||||
|
|
@ -6744,6 +6951,11 @@ packages:
|
|||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rolldown@1.0.0-rc.15:
|
||||
resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rollup@4.60.1:
|
||||
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
|
|
@ -6932,6 +7144,9 @@ packages:
|
|||
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
|
||||
standardwebhooks@1.0.0:
|
||||
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
|
||||
|
||||
|
|
@ -7481,15 +7696,16 @@ packages:
|
|||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite@7.3.2:
|
||||
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
|
||||
vite@8.0.8:
|
||||
resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
'@vitejs/devtools': ^0.1.0
|
||||
esbuild: ^0.27.0 || ^0.28.0
|
||||
jiti: '>=1.21.0'
|
||||
less: ^4.0.0
|
||||
lightningcss: ^1.21.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
|
|
@ -7500,12 +7716,14 @@ packages:
|
|||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitejs/devtools':
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
|
|
@ -8555,6 +8773,8 @@ snapshots:
|
|||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
'@istanbuljs/load-nyc-config@1.1.0':
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
|
|
@ -8778,6 +8998,13 @@ snapshots:
|
|||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.9.2
|
||||
'@emnapi/runtime': 1.9.2
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next/bundle-analyzer@16.2.3':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
|
|
@ -9826,6 +10053,8 @@ snapshots:
|
|||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1)
|
||||
|
||||
'@oxc-project/types@0.124.0': {}
|
||||
|
||||
'@panva/hkdf@1.2.1': {}
|
||||
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
|
|
@ -10382,8 +10611,59 @@ snapshots:
|
|||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.9.2
|
||||
'@emnapi/runtime': 1.9.2
|
||||
'@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.15': {}
|
||||
|
||||
'@rollup/plugin-commonjs@28.0.1(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
|
|
@ -10573,7 +10853,7 @@ snapshots:
|
|||
|
||||
'@sentry/core@10.48.0': {}
|
||||
|
||||
'@sentry/nextjs@10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1)':
|
||||
'@sentry/nextjs@10.48.0(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(webpack@5.106.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@opentelemetry/semantic-conventions': 1.40.0
|
||||
|
|
@ -10585,7 +10865,7 @@ snapshots:
|
|||
'@sentry/opentelemetry': 10.48.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)
|
||||
'@sentry/react': 10.48.0(react@19.2.5)
|
||||
'@sentry/vercel-edge': 10.48.0
|
||||
'@sentry/webpack-plugin': 5.2.0(webpack@5.106.1)
|
||||
'@sentry/webpack-plugin': 5.2.0(webpack@5.106.1(esbuild@0.27.7))
|
||||
next: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
rollup: 4.60.1
|
||||
stacktrace-parser: 0.1.11
|
||||
|
|
@ -10684,10 +10964,10 @@ snapshots:
|
|||
'@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1)
|
||||
'@sentry/core': 10.48.0
|
||||
|
||||
'@sentry/webpack-plugin@5.2.0(webpack@5.106.1)':
|
||||
'@sentry/webpack-plugin@5.2.0(webpack@5.106.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
'@sentry/bundler-plugin-core': 5.2.0
|
||||
webpack: 5.106.1
|
||||
webpack: 5.106.1(esbuild@0.27.7)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
|
@ -11280,7 +11560,7 @@ snapshots:
|
|||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||
optional: true
|
||||
|
||||
'@vitejs/plugin-react@4.7.0(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
'@vitejs/plugin-react@4.7.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||
|
|
@ -11288,7 +11568,7 @@ snapshots:
|
|||
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.17.0
|
||||
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -11301,13 +11581,13 @@ snapshots:
|
|||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
'@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@vitest/pretty-format@4.1.4':
|
||||
dependencies:
|
||||
|
|
@ -11895,6 +12175,8 @@ snapshots:
|
|||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
co@4.6.0: {}
|
||||
|
||||
collect-v8-coverage@1.0.3: {}
|
||||
|
|
@ -12355,7 +12637,7 @@ snapshots:
|
|||
'@next/eslint-plugin-next': 16.2.3
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@1.21.7))
|
||||
|
|
@ -12390,7 +12672,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -12405,13 +12687,13 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)):
|
||||
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -12442,7 +12724,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.4(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))
|
||||
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -13320,6 +13602,20 @@ snapshots:
|
|||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
ioredis@5.10.1:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.5.1
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.4.3
|
||||
denque: 2.1.0
|
||||
lodash.defaults: 4.2.0
|
||||
lodash.isarguments: 3.1.0
|
||||
redis-errors: 1.2.0
|
||||
redis-parser: 3.0.0
|
||||
standard-as-callback: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
|
@ -13986,6 +14282,55 @@ snapshots:
|
|||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-arm64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.32.0:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.32.0:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optionalDependencies:
|
||||
lightningcss-android-arm64: 1.32.0
|
||||
lightningcss-darwin-arm64: 1.32.0
|
||||
lightningcss-darwin-x64: 1.32.0
|
||||
lightningcss-freebsd-x64: 1.32.0
|
||||
lightningcss-linux-arm-gnueabihf: 1.32.0
|
||||
lightningcss-linux-arm64-gnu: 1.32.0
|
||||
lightningcss-linux-arm64-musl: 1.32.0
|
||||
lightningcss-linux-x64-gnu: 1.32.0
|
||||
lightningcss-linux-x64-musl: 1.32.0
|
||||
lightningcss-win32-arm64-msvc: 1.32.0
|
||||
lightningcss-win32-x64-msvc: 1.32.0
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
|
@ -14020,8 +14365,12 @@ snapshots:
|
|||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
|
||||
lodash.includes@4.3.0: {}
|
||||
|
||||
lodash.isarguments@3.1.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
|
||||
lodash.isinteger@4.0.4: {}
|
||||
|
|
@ -15343,6 +15692,27 @@ snapshots:
|
|||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
rolldown@1.0.0-rc.15:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.124.0
|
||||
'@rolldown/pluginutils': 1.0.0-rc.15
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.0.0-rc.15
|
||||
'@rolldown/binding-darwin-arm64': 1.0.0-rc.15
|
||||
'@rolldown/binding-darwin-x64': 1.0.0-rc.15
|
||||
'@rolldown/binding-freebsd-x64': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
|
||||
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
|
||||
|
||||
rollup@4.60.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
|
@ -15607,6 +15977,8 @@ snapshots:
|
|||
dependencies:
|
||||
type-fest: 0.7.1
|
||||
|
||||
standard-as-callback@2.1.0: {}
|
||||
|
||||
standardwebhooks@1.0.0:
|
||||
dependencies:
|
||||
'@stablelib/base64': 1.0.1
|
||||
|
|
@ -15838,13 +16210,15 @@ snapshots:
|
|||
|
||||
tapable@2.3.2: {}
|
||||
|
||||
terser-webpack-plugin@5.4.0(webpack@5.106.1):
|
||||
terser-webpack-plugin@5.4.0(esbuild@0.27.7)(webpack@5.106.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
terser: 5.46.1
|
||||
webpack: 5.106.1
|
||||
webpack: 5.106.1(esbuild@0.27.7)
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.7
|
||||
|
||||
terser@5.46.1:
|
||||
dependencies:
|
||||
|
|
@ -16228,26 +16602,26 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.7
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.9
|
||||
rollup: 4.60.1
|
||||
rolldown: 1.0.0-rc.15
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
esbuild: 0.27.7
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
terser: 5.46.1
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.3
|
||||
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.4
|
||||
'@vitest/mocker': 4.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 4.1.4
|
||||
'@vitest/runner': 4.1.4
|
||||
'@vitest/snapshot': 4.1.4
|
||||
|
|
@ -16264,7 +16638,7 @@ snapshots:
|
|||
tinyexec: 1.1.1
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 7.3.2(@types/node@25.6.0)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@1.21.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
|
|
@ -16317,7 +16691,7 @@ snapshots:
|
|||
|
||||
webpack-sources@3.3.4: {}
|
||||
|
||||
webpack@5.106.1:
|
||||
webpack@5.106.1(esbuild@0.27.7):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
|
|
@ -16341,7 +16715,7 @@ snapshots:
|
|||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.2
|
||||
terser-webpack-plugin: 5.4.0(webpack@5.106.1)
|
||||
terser-webpack-plugin: 5.4.0(esbuild@0.27.7)(webpack@5.106.1(esbuild@0.27.7))
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue