dev3000 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +11 -2
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/dev-environment.d.ts +1 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +29 -3
- package/dist/dev-environment.js.map +1 -1
- package/dist/screencast-manager.d.ts +76 -0
- package/dist/screencast-manager.d.ts.map +1 -0
- package/dist/screencast-manager.js +410 -0
- package/dist/screencast-manager.js.map +1 -0
- package/dist/src/tui-interface-impl.tsx +15 -7
- package/dist/tui-interface-impl.d.ts.map +1 -1
- package/dist/tui-interface-impl.js +7 -5
- package/dist/tui-interface-impl.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/app-path-routes-manifest.json +4 -1
- package/mcp-server/.next/build-manifest.json +5 -5
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/routes-manifest.json +16 -0
- package/mcp-server/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error/page.js +1 -1
- package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_global-error.html +2 -2
- package/mcp-server/.next/server/app/_global-error.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/_not-found/page.js +1 -1
- package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +2 -2
- package/mcp-server/.next/server/app/api/jank/[session]/route/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route/build-manifest.json +11 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js +9 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.map +5 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/list/route/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route/build-manifest.json +11 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js +7 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.map +5 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app/api/tools/route.js +1 -1
- package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +3 -3
- package/mcp-server/.next/server/app/logs/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/logs/page.js +1 -1
- package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/mcp/route.js +3 -2
- package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/page.js +1 -1
- package/mcp-server/.next/server/app/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/video/[session]/page/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/video/[session]/page/build-manifest.json +18 -0
- package/mcp-server/.next/server/app/video/[session]/page/next-font-manifest.json +6 -0
- package/mcp-server/.next/server/app/video/[session]/page/react-loadable-manifest.json +1 -0
- package/mcp-server/.next/server/app/video/[session]/page/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/video/[session]/page.js +15 -0
- package/mcp-server/.next/server/app/video/[session]/page.js.map +5 -0
- package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -0
- package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app-paths-manifest.json +4 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js +34 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__5580d2ea._.js → [root-of-the-server]__177c72c6._.js} +3 -3
- package/mcp-server/.next/server/chunks/[root-of-the-server]__177c72c6._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js → [root-of-the-server]__2056c8b5._.js} +2 -2
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js → [root-of-the-server]__55c04517._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js.map +1 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js → [root-of-the-server]__bc773251._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js.map +1 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js.map +1 -0
- package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js +3 -0
- package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js.map +1 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js +3 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js.map +1 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js +3 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__bf771f7e._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__8db775f9._.js.map → [root-of-the-server]__bf771f7e._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__c4e78a20._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__e5dec879._.js.map → [root-of-the-server]__c4e78a20._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js +1 -1
- package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js.map +1 -1
- package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js +2 -2
- package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js.map +1 -1
- package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js +4 -0
- package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js.map +1 -0
- package/mcp-server/.next/server/middleware-build-manifest.js +3 -3
- package/mcp-server/.next/server/server-reference-manifest.js +1 -1
- package/mcp-server/.next/server/server-reference-manifest.json +1 -1
- package/mcp-server/.next/static/chunks/274a8d03fad7f819.js +1 -0
- package/mcp-server/.next/static/chunks/3d37ed424c6aaf63.css +1 -0
- package/mcp-server/.next/static/chunks/543e14c771a22442.js +1 -0
- package/mcp-server/.next/static/chunks/58fdd5192b305065.js +1 -0
- package/mcp-server/.next/static/chunks/6bd684c2018a357c.js +1 -0
- package/mcp-server/.next/static/chunks/6d59e588420330ca.js +1 -0
- package/mcp-server/.next/static/chunks/9625e4da85a132f3.js +1 -0
- package/mcp-server/.next/static/chunks/c36bc797d535a4dc.js +1 -0
- package/mcp-server/.next/static/chunks/{turbopack-7cd5a898ed038e26.js → turbopack-9656e7304584cab2.js} +2 -2
- package/mcp-server/app/api/jank/[session]/route.ts +344 -0
- package/mcp-server/app/api/screenshots/list/route.ts +22 -0
- package/mcp-server/app/logs/LogsClient.tsx +33 -0
- package/mcp-server/app/logs/utils.ts +2 -0
- package/mcp-server/app/mcp/route.ts +20 -0
- package/mcp-server/app/mcp/tools.ts +288 -9
- package/mcp-server/app/video/[session]/page.tsx +237 -0
- package/mcp-server/package.json +4 -0
- package/package.json +1 -1
- package/src/tui-interface-impl.tsx +15 -7
- package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js +0 -34
- package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js.map +0 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__5580d2ea._.js.map +0 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__8db775f9._.js +0 -3
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e5dec879._.js +0 -3
- package/mcp-server/.next/static/chunks/11ac0d0e69696c72.js +0 -1
- package/mcp-server/.next/static/chunks/172f6179d608c15f.js +0 -1
- package/mcp-server/.next/static/chunks/65b18bf1ede9811a.css +0 -1
- package/mcp-server/.next/static/chunks/bb8a4e5f381a85ec.js +0 -1
- package/mcp-server/.next/static/chunks/e09be78bba2194fd.js +0 -1
- package/mcp-server/.next/static/chunks/fec3a6ddaef02b8b.js +0 -1
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js.map → [root-of-the-server]__2056c8b5._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js.map → [root-of-the-server]__55c04517._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js.map → [root-of-the-server]__bc773251._.js.map} +0 -0
- /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_clientMiddlewareManifest.json +0 -0
- /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "fs"
|
|
2
|
+
import { type NextRequest, NextResponse } from "next/server"
|
|
3
|
+
import { tmpdir } from "os"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import pixelmatch from "pixelmatch"
|
|
6
|
+
import { PNG } from "pngjs"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetch real CLS data from screencast metadata
|
|
10
|
+
* ScreencastManager now injects a PerformanceObserver to capture real layout shifts
|
|
11
|
+
*/
|
|
12
|
+
async function getRealCLSData(
|
|
13
|
+
screenshotDir: string,
|
|
14
|
+
session: string
|
|
15
|
+
): Promise<{
|
|
16
|
+
shifts: Array<{ score: number; timestamp: number; sources?: unknown[] }>
|
|
17
|
+
totalCLS: number
|
|
18
|
+
grade: string
|
|
19
|
+
cssViewport?: { width: number; height: number; devicePixelRatio: number }
|
|
20
|
+
} | null> {
|
|
21
|
+
try {
|
|
22
|
+
const metadataPath = join(screenshotDir, `${session}-metadata.json`)
|
|
23
|
+
if (!existsSync(metadataPath)) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"))
|
|
28
|
+
if (!metadata.layoutShifts || metadata.layoutShifts.length === 0) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
shifts: metadata.layoutShifts,
|
|
34
|
+
totalCLS: metadata.totalCLS || 0,
|
|
35
|
+
grade: metadata.clsGrade || "unknown",
|
|
36
|
+
cssViewport: metadata.cssViewport
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Failed to fetch real CLS data:", error)
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function GET(_request: NextRequest, { params }: { params: Promise<{ session: string }> }) {
|
|
45
|
+
const { session } = await params
|
|
46
|
+
const screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots")
|
|
47
|
+
|
|
48
|
+
// Try to get real CLS data from screencast metadata
|
|
49
|
+
const realCLS = await getRealCLSData(screenshotDir, session)
|
|
50
|
+
|
|
51
|
+
if (realCLS && realCLS.shifts.length === 0) {
|
|
52
|
+
// No layout shifts detected by PerformanceObserver
|
|
53
|
+
return NextResponse.json({
|
|
54
|
+
clsMarkers: [],
|
|
55
|
+
source: "performance-observer",
|
|
56
|
+
actualCLS: 0,
|
|
57
|
+
grade: "good",
|
|
58
|
+
message: "No layout shifts detected by PerformanceObserver"
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!existsSync(screenshotDir)) {
|
|
63
|
+
return NextResponse.json({ clsMarkers: [] })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Find files for this session
|
|
67
|
+
const files = readdirSync(screenshotDir)
|
|
68
|
+
const sessionFiles = files
|
|
69
|
+
.filter((f) => f.startsWith(session) && f.includes("-jank-") && f.endsWith(".png"))
|
|
70
|
+
.sort((a, b) => {
|
|
71
|
+
const aTime = parseInt(a.match(/-(\d+)ms\.png$/)?.[1] || "0", 10)
|
|
72
|
+
const bTime = parseInt(b.match(/-(\d+)ms\.png$/)?.[1] || "0", 10)
|
|
73
|
+
return aTime - bTime
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (sessionFiles.length < 2) {
|
|
77
|
+
return NextResponse.json({ clsMarkers: [] })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const clsMarkers: Array<{
|
|
81
|
+
timestamp: number
|
|
82
|
+
boundingBox: { x: number; y: number; width: number; height: number } | null
|
|
83
|
+
}> = []
|
|
84
|
+
|
|
85
|
+
const debugInfo: Array<{
|
|
86
|
+
comparison: string
|
|
87
|
+
diffPercentage: number
|
|
88
|
+
shiftRegions?: number
|
|
89
|
+
isValid: boolean
|
|
90
|
+
}> = []
|
|
91
|
+
|
|
92
|
+
// Compare consecutive frames
|
|
93
|
+
for (let i = 1; i < sessionFiles.length; i++) {
|
|
94
|
+
const prevFile = join(screenshotDir, sessionFiles[i - 1])
|
|
95
|
+
const currFile = join(screenshotDir, sessionFiles[i])
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const prevPng = PNG.sync.read(readFileSync(prevFile))
|
|
99
|
+
const currPng = PNG.sync.read(readFileSync(currFile))
|
|
100
|
+
|
|
101
|
+
if (prevPng.width !== currPng.width || prevPng.height !== currPng.height) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const diff = new PNG({ width: prevPng.width, height: prevPng.height })
|
|
106
|
+
const numDiffPixels = pixelmatch(prevPng.data, currPng.data, diff.data, prevPng.width, prevPng.height, {
|
|
107
|
+
threshold: 0.1
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const totalPixels = prevPng.width * prevPng.height
|
|
111
|
+
const diffPercentage = (numDiffPixels / totalPixels) * 100
|
|
112
|
+
|
|
113
|
+
// Skip if almost no changes (< 0.05% = likely just anti-aliasing)
|
|
114
|
+
if (diffPercentage < 0.05) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Detect LAYOUT SHIFTS by looking for horizontal bands of changes
|
|
119
|
+
// (content moving down/up will create horizontal bands of differences)
|
|
120
|
+
const rowChangeCounts = new Array(prevPng.height).fill(0)
|
|
121
|
+
|
|
122
|
+
for (let y = 0; y < prevPng.height; y++) {
|
|
123
|
+
for (let x = 0; x < prevPng.width; x++) {
|
|
124
|
+
const idx = (prevPng.width * y + x) * 4
|
|
125
|
+
if (diff.data[idx] > 0 || diff.data[idx + 1] > 0 || diff.data[idx + 2] > 0) {
|
|
126
|
+
rowChangeCounts[y]++
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Find regions with significant row changes (indicating vertical shifts)
|
|
132
|
+
let shiftRegions: Array<{ startY: number; endY: number; intensity: number }> = []
|
|
133
|
+
let currentRegionStart = -1
|
|
134
|
+
let currentRegionIntensity = 0
|
|
135
|
+
|
|
136
|
+
for (let y = 0; y < prevPng.height; y++) {
|
|
137
|
+
const rowChangePercentage = (rowChangeCounts[y] / prevPng.width) * 100
|
|
138
|
+
|
|
139
|
+
// If this row has significant changes (>10% of pixels in the row changed)
|
|
140
|
+
if (rowChangePercentage > 10) {
|
|
141
|
+
if (currentRegionStart === -1) {
|
|
142
|
+
currentRegionStart = y
|
|
143
|
+
currentRegionIntensity = rowChangePercentage
|
|
144
|
+
} else {
|
|
145
|
+
currentRegionIntensity = Math.max(currentRegionIntensity, rowChangePercentage)
|
|
146
|
+
}
|
|
147
|
+
} else if (currentRegionStart !== -1) {
|
|
148
|
+
// End of a shift region
|
|
149
|
+
shiftRegions.push({
|
|
150
|
+
startY: currentRegionStart,
|
|
151
|
+
endY: y - 1,
|
|
152
|
+
intensity: currentRegionIntensity
|
|
153
|
+
})
|
|
154
|
+
currentRegionStart = -1
|
|
155
|
+
currentRegionIntensity = 0
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Close final region if needed
|
|
160
|
+
if (currentRegionStart !== -1) {
|
|
161
|
+
shiftRegions.push({
|
|
162
|
+
startY: currentRegionStart,
|
|
163
|
+
endY: prevPng.height - 1,
|
|
164
|
+
intensity: currentRegionIntensity
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Filter shift regions: must be at least 5px tall and have high intensity
|
|
169
|
+
shiftRegions = shiftRegions.filter((region) => {
|
|
170
|
+
const height = region.endY - region.startY
|
|
171
|
+
return height >= 5 && region.intensity > 20
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// If we found layout shift regions, calculate bounding box
|
|
175
|
+
if (shiftRegions.length > 0) {
|
|
176
|
+
let minX = prevPng.width
|
|
177
|
+
let minY = prevPng.height
|
|
178
|
+
let maxX = 0
|
|
179
|
+
let maxY = 0
|
|
180
|
+
|
|
181
|
+
for (const region of shiftRegions) {
|
|
182
|
+
for (let y = region.startY; y <= region.endY; y++) {
|
|
183
|
+
for (let x = 0; x < prevPng.width; x++) {
|
|
184
|
+
const idx = (prevPng.width * y + x) * 4
|
|
185
|
+
if (diff.data[idx] > 0 || diff.data[idx + 1] > 0 || diff.data[idx + 2] > 0) {
|
|
186
|
+
minX = Math.min(minX, x)
|
|
187
|
+
minY = Math.min(minY, y)
|
|
188
|
+
maxX = Math.max(maxX, x)
|
|
189
|
+
maxY = Math.max(maxY, y)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Collect debug info
|
|
196
|
+
debugInfo.push({
|
|
197
|
+
comparison: `${sessionFiles[i - 1]} vs ${sessionFiles[i]}`,
|
|
198
|
+
diffPercentage: Number.parseFloat(diffPercentage.toFixed(2)),
|
|
199
|
+
shiftRegions: shiftRegions.length,
|
|
200
|
+
isValid: true
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (minX < maxX && minY < maxY) {
|
|
204
|
+
const timeMatch = sessionFiles[i].match(/-(\d+)ms\.png$/)
|
|
205
|
+
const timeSinceStart = timeMatch ? parseInt(timeMatch[1], 10) : 0
|
|
206
|
+
|
|
207
|
+
// Add padding to bounding box (10px)
|
|
208
|
+
const padding = 10
|
|
209
|
+
const boundingBox = {
|
|
210
|
+
x: Math.max(0, minX - padding),
|
|
211
|
+
y: Math.max(0, minY - padding),
|
|
212
|
+
width: Math.min(prevPng.width, maxX - minX + padding * 2),
|
|
213
|
+
height: Math.min(prevPng.height, maxY - minY + padding * 2)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Only include bounding box if it's reasonable (< 50% of screen)
|
|
217
|
+
// If it's too large, the detection is likely inaccurate
|
|
218
|
+
const boxArea = boundingBox.width * boundingBox.height
|
|
219
|
+
const screenArea = prevPng.width * prevPng.height
|
|
220
|
+
const areaPercentage = (boxArea / screenArea) * 100
|
|
221
|
+
const boundingBoxToUse = boxArea < screenArea * 0.5 ? boundingBox : null
|
|
222
|
+
|
|
223
|
+
console.log("[JANK API] Pixel-diff bounding box:", {
|
|
224
|
+
boundingBox,
|
|
225
|
+
areaPercentage: areaPercentage.toFixed(2) + "%",
|
|
226
|
+
rejected: !boundingBoxToUse,
|
|
227
|
+
reason: !boundingBoxToUse ? "too large (>50% of screen)" : "accepted"
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
clsMarkers.push({ timestamp: timeSinceStart, boundingBox: boundingBoxToUse })
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
// No shift regions found - likely just content changes (images loading)
|
|
234
|
+
debugInfo.push({
|
|
235
|
+
comparison: `${sessionFiles[i - 1]} vs ${sessionFiles[i]}`,
|
|
236
|
+
diffPercentage: Number.parseFloat(diffPercentage.toFixed(2)),
|
|
237
|
+
shiftRegions: 0,
|
|
238
|
+
isValid: false
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Skip frames that can't be compared
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get screenshot dimensions to calculate coordinate scaling
|
|
247
|
+
let screenshotWidth = 1920 // default
|
|
248
|
+
let screenshotHeight = 1080 // default
|
|
249
|
+
if (sessionFiles.length > 0) {
|
|
250
|
+
try {
|
|
251
|
+
const firstScreenshot = PNG.sync.read(readFileSync(join(screenshotDir, sessionFiles[0])))
|
|
252
|
+
screenshotWidth = firstScreenshot.width
|
|
253
|
+
screenshotHeight = firstScreenshot.height
|
|
254
|
+
} catch {
|
|
255
|
+
// Use defaults
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// If we have real CLS data, cross-reference with pixel-diff to filter false positives
|
|
260
|
+
if (realCLS) {
|
|
261
|
+
// Match each real CLS shift to the closest pixel-diff marker
|
|
262
|
+
// This avoids duplicate markers for the same shift
|
|
263
|
+
const filteredMarkers = realCLS.shifts
|
|
264
|
+
.map((shift) => {
|
|
265
|
+
// Find closest pixel-diff marker within 50ms
|
|
266
|
+
let closestMarker = null
|
|
267
|
+
let closestDistance = Infinity
|
|
268
|
+
|
|
269
|
+
for (const marker of clsMarkers) {
|
|
270
|
+
const distance = Math.abs(shift.timestamp - marker.timestamp)
|
|
271
|
+
if (distance < 50 && distance < closestDistance) {
|
|
272
|
+
closestMarker = marker
|
|
273
|
+
closestDistance = distance
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!closestMarker) return null
|
|
278
|
+
|
|
279
|
+
// Try to use real CLS bounding box if available
|
|
280
|
+
const source = shift.sources?.[0] as {
|
|
281
|
+
node?: string
|
|
282
|
+
previousRect?: { x: number; y: number; width: number; height: number }
|
|
283
|
+
currentRect?: { x: number; y: number; width: number; height: number }
|
|
284
|
+
actualRect?: { x: number; y: number; width: number; height: number }
|
|
285
|
+
}
|
|
286
|
+
let boundingBox = closestMarker.boundingBox
|
|
287
|
+
|
|
288
|
+
// Prefer actualRect (queried via querySelector) - it's in CSS pixels relative to viewport
|
|
289
|
+
// Convert to screenshot pixels using CSS viewport dimensions
|
|
290
|
+
if (source?.actualRect && realCLS?.cssViewport) {
|
|
291
|
+
const cssRect = source.actualRect
|
|
292
|
+
const cssViewportWidth = realCLS.cssViewport.width
|
|
293
|
+
const cssViewportHeight = realCLS.cssViewport.height
|
|
294
|
+
|
|
295
|
+
console.log("[JANK API] actualRect (CSS pixels):", cssRect)
|
|
296
|
+
console.log("[JANK API] CSS viewport:", { width: cssViewportWidth, height: cssViewportHeight })
|
|
297
|
+
console.log("[JANK API] Screenshot dimensions:", { width: screenshotWidth, height: screenshotHeight })
|
|
298
|
+
|
|
299
|
+
// Calculate scale factor: screenshot pixels / CSS pixels
|
|
300
|
+
const scaleX = screenshotWidth / cssViewportWidth
|
|
301
|
+
const scaleY = screenshotHeight / cssViewportHeight
|
|
302
|
+
|
|
303
|
+
console.log("[JANK API] Calculated scale:", { x: scaleX.toFixed(3), y: scaleY.toFixed(3) })
|
|
304
|
+
|
|
305
|
+
// Convert CSS pixels to screenshot pixels
|
|
306
|
+
boundingBox = {
|
|
307
|
+
x: Math.round(cssRect.x * scaleX),
|
|
308
|
+
y: Math.round(cssRect.y * scaleY),
|
|
309
|
+
width: Math.round(cssRect.width * scaleX),
|
|
310
|
+
height: Math.round(cssRect.height * scaleY)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log("[JANK API] Bounding box (screenshot pixels):", boundingBox)
|
|
314
|
+
} else {
|
|
315
|
+
console.log("[JANK API] No actualRect or cssViewport available, cannot determine bounding box")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...closestMarker,
|
|
320
|
+
boundingBox,
|
|
321
|
+
clsScore: shift.score,
|
|
322
|
+
element: source?.node || "unknown"
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
.filter(Boolean)
|
|
326
|
+
|
|
327
|
+
return NextResponse.json({
|
|
328
|
+
clsMarkers: filteredMarkers,
|
|
329
|
+
realLayoutShifts: realCLS.shifts,
|
|
330
|
+
debug: debugInfo,
|
|
331
|
+
source: "performance-observer-validated",
|
|
332
|
+
actualCLS: realCLS.totalCLS,
|
|
333
|
+
grade: realCLS.grade,
|
|
334
|
+
note: `Real CLS: ${realCLS.totalCLS.toFixed(4)} (${realCLS.grade}). Filtered ${clsMarkers.length - filteredMarkers.length} false positives from pixel-diff.`
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return NextResponse.json({
|
|
339
|
+
clsMarkers,
|
|
340
|
+
debug: debugInfo,
|
|
341
|
+
source: "pixel-diff-fallback",
|
|
342
|
+
note: "PerformanceObserver data not available, using pixel-diff analysis (may include false positives)"
|
|
343
|
+
})
|
|
344
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readdirSync } from "fs"
|
|
2
|
+
import { type NextRequest, NextResponse } from "next/server"
|
|
3
|
+
import { tmpdir } from "os"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const searchParams = request.nextUrl.searchParams
|
|
9
|
+
const pattern = searchParams.get("pattern") || ""
|
|
10
|
+
|
|
11
|
+
const screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots")
|
|
12
|
+
|
|
13
|
+
const files = readdirSync(screenshotDir)
|
|
14
|
+
.filter((f) => f.endsWith(".png"))
|
|
15
|
+
.filter((f) => (pattern ? f.includes(pattern) : true))
|
|
16
|
+
|
|
17
|
+
return NextResponse.json({ files })
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("Error listing screenshots:", error)
|
|
20
|
+
return NextResponse.json({ files: [] }, { status: 500 })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -784,6 +784,39 @@ function LogEntryComponent({ entry, darkMode }: { entry: LogEntry; darkMode: boo
|
|
|
784
784
|
/>
|
|
785
785
|
</div>
|
|
786
786
|
)}
|
|
787
|
+
|
|
788
|
+
{entry.screencast && (
|
|
789
|
+
<div className="mt-2">
|
|
790
|
+
<a
|
|
791
|
+
href={entry.screencast}
|
|
792
|
+
target="_blank"
|
|
793
|
+
rel="noopener noreferrer"
|
|
794
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
|
|
795
|
+
>
|
|
796
|
+
<svg
|
|
797
|
+
className="w-5 h-5"
|
|
798
|
+
fill="none"
|
|
799
|
+
stroke="currentColor"
|
|
800
|
+
viewBox="0 0 24 24"
|
|
801
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
802
|
+
>
|
|
803
|
+
<path
|
|
804
|
+
strokeLinecap="round"
|
|
805
|
+
strokeLinejoin="round"
|
|
806
|
+
strokeWidth={2}
|
|
807
|
+
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
|
808
|
+
/>
|
|
809
|
+
<path
|
|
810
|
+
strokeLinecap="round"
|
|
811
|
+
strokeLinejoin="round"
|
|
812
|
+
strokeWidth={2}
|
|
813
|
+
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
814
|
+
/>
|
|
815
|
+
</svg>
|
|
816
|
+
View Jank Analysis Video
|
|
817
|
+
</a>
|
|
818
|
+
</div>
|
|
819
|
+
)}
|
|
787
820
|
</div>
|
|
788
821
|
)
|
|
789
822
|
}
|
|
@@ -89,6 +89,7 @@ export function parseLogEntries(logContent: string): LogEntry[] {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const screenshot = message.match(/\[SCREENSHOT\] ([^\s[]+)/)?.[1]
|
|
92
|
+
const screencast = message.match(/\[SCREENCAST\] View jank analysis: ([^\s]+)/)?.[1]
|
|
92
93
|
|
|
93
94
|
// Clean up CSS formatting directives in console log messages
|
|
94
95
|
let cleanedMessage = cleanConsoleFormatting(message)
|
|
@@ -129,6 +130,7 @@ export function parseLogEntries(logContent: string): LogEntry[] {
|
|
|
129
130
|
source,
|
|
130
131
|
message: cleanedMessage,
|
|
131
132
|
screenshot,
|
|
133
|
+
screencast,
|
|
132
134
|
original: line,
|
|
133
135
|
tabIdentifier,
|
|
134
136
|
userAgent
|
|
@@ -55,6 +55,26 @@ const handler = createMcpHandler(
|
|
|
55
55
|
}
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
+
// Alias: fix_my_jank -> fix_my_app with performance focus
|
|
59
|
+
server.tool(
|
|
60
|
+
"fix_my_jank",
|
|
61
|
+
"🎯 **JANK & PERFORMANCE FIXER** - Specialized alias for detecting and fixing layout shifts, CLS issues, and performance problems. Automatically focuses on performance analysis and jank detection from passive screencast captures.\n\n💡 This is an alias for fix_my_app with focusArea='performance', perfect for 'fix my jank' or 'why is my page janky' requests!",
|
|
62
|
+
{
|
|
63
|
+
projectName: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Project name to debug (if multiple dev3000 instances are running)"),
|
|
67
|
+
timeRangeMinutes: z.number().optional().describe("Minutes to analyze back from now (default: 10)")
|
|
68
|
+
},
|
|
69
|
+
async (params) => {
|
|
70
|
+
// Call fix_my_app with performance focus
|
|
71
|
+
return fixMyApp({
|
|
72
|
+
...params,
|
|
73
|
+
focusArea: "performance"
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
58
78
|
// Integrated workflow orchestration tool
|
|
59
79
|
server.tool(
|
|
60
80
|
"create_integrated_workflow",
|