dev3000 0.0.78 → 0.0.79

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.
Files changed (151) hide show
  1. package/dist/dev-environment.d.ts +1 -0
  2. package/dist/dev-environment.d.ts.map +1 -1
  3. package/dist/dev-environment.js +29 -3
  4. package/dist/dev-environment.js.map +1 -1
  5. package/dist/screencast-manager.d.ts +76 -0
  6. package/dist/screencast-manager.d.ts.map +1 -0
  7. package/dist/screencast-manager.js +410 -0
  8. package/dist/screencast-manager.js.map +1 -0
  9. package/dist/src/tui-interface-impl.tsx +15 -7
  10. package/dist/tui-interface-impl.d.ts.map +1 -1
  11. package/dist/tui-interface-impl.js +7 -5
  12. package/dist/tui-interface-impl.js.map +1 -1
  13. package/mcp-server/.next/BUILD_ID +1 -1
  14. package/mcp-server/.next/app-path-routes-manifest.json +4 -1
  15. package/mcp-server/.next/build-manifest.json +5 -5
  16. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  17. package/mcp-server/.next/prerender-manifest.json +3 -3
  18. package/mcp-server/.next/routes-manifest.json +16 -0
  19. package/mcp-server/.next/server/app/_global-error/page/build-manifest.json +3 -3
  20. package/mcp-server/.next/server/app/_global-error/page.js +1 -1
  21. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  22. package/mcp-server/.next/server/app/_global-error.html +2 -2
  23. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  24. package/mcp-server/.next/server/app/_not-found/page/build-manifest.json +3 -3
  25. package/mcp-server/.next/server/app/_not-found/page.js +1 -1
  26. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  27. package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  28. package/mcp-server/.next/server/app/_not-found.html +1 -1
  29. package/mcp-server/.next/server/app/_not-found.rsc +2 -2
  30. package/mcp-server/.next/server/app/api/jank/[session]/route/app-paths-manifest.json +3 -0
  31. package/mcp-server/.next/server/app/api/jank/[session]/route/build-manifest.json +11 -0
  32. package/mcp-server/.next/server/app/api/jank/[session]/route/server-reference-manifest.json +4 -0
  33. package/mcp-server/.next/server/app/api/jank/[session]/route.js +9 -0
  34. package/mcp-server/.next/server/app/api/jank/[session]/route.js.map +5 -0
  35. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -0
  36. package/mcp-server/.next/server/app/api/jank/[session]/route_client-reference-manifest.js +2 -0
  37. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  38. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  39. package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
  40. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  41. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  42. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  43. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
  44. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  45. package/mcp-server/.next/server/app/api/screenshots/list/route/app-paths-manifest.json +3 -0
  46. package/mcp-server/.next/server/app/api/screenshots/list/route/build-manifest.json +11 -0
  47. package/mcp-server/.next/server/app/api/screenshots/list/route/server-reference-manifest.json +4 -0
  48. package/mcp-server/.next/server/app/api/screenshots/list/route.js +7 -0
  49. package/mcp-server/.next/server/app/api/screenshots/list/route.js.map +5 -0
  50. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -0
  51. package/mcp-server/.next/server/app/api/screenshots/list/route_client-reference-manifest.js +2 -0
  52. package/mcp-server/.next/server/app/api/tools/route.js +1 -1
  53. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  54. package/mcp-server/.next/server/app/index.html +1 -1
  55. package/mcp-server/.next/server/app/index.rsc +3 -3
  56. package/mcp-server/.next/server/app/logs/page/build-manifest.json +3 -3
  57. package/mcp-server/.next/server/app/logs/page.js +1 -1
  58. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  59. package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  60. package/mcp-server/.next/server/app/mcp/route.js +3 -2
  61. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  62. package/mcp-server/.next/server/app/page/build-manifest.json +3 -3
  63. package/mcp-server/.next/server/app/page.js +1 -1
  64. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  65. package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
  66. package/mcp-server/.next/server/app/video/[session]/page/app-paths-manifest.json +3 -0
  67. package/mcp-server/.next/server/app/video/[session]/page/build-manifest.json +18 -0
  68. package/mcp-server/.next/server/app/video/[session]/page/next-font-manifest.json +6 -0
  69. package/mcp-server/.next/server/app/video/[session]/page/react-loadable-manifest.json +1 -0
  70. package/mcp-server/.next/server/app/video/[session]/page/server-reference-manifest.json +4 -0
  71. package/mcp-server/.next/server/app/video/[session]/page.js +15 -0
  72. package/mcp-server/.next/server/app/video/[session]/page.js.map +5 -0
  73. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -0
  74. package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +2 -0
  75. package/mcp-server/.next/server/app-paths-manifest.json +4 -1
  76. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js +34 -0
  77. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js.map +1 -0
  78. package/mcp-server/.next/server/chunks/{[root-of-the-server]__5580d2ea._.js → [root-of-the-server]__177c72c6._.js} +3 -3
  79. package/mcp-server/.next/server/chunks/[root-of-the-server]__177c72c6._.js.map +1 -0
  80. package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js → [root-of-the-server]__2056c8b5._.js} +2 -2
  81. package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js → [root-of-the-server]__55c04517._.js} +2 -2
  82. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js +3 -0
  83. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js.map +1 -0
  84. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js +3 -0
  85. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js.map +1 -0
  86. package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js → [root-of-the-server]__bc773251._.js} +2 -2
  87. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js +3 -0
  88. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js.map +1 -0
  89. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js +3 -0
  90. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js.map +1 -0
  91. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js +3 -0
  92. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js.map +1 -0
  93. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js +3 -0
  94. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js.map +1 -0
  95. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js +3 -0
  96. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js.map +1 -0
  97. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__bf771f7e._.js +3 -0
  98. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__8db775f9._.js.map → [root-of-the-server]__bf771f7e._.js.map} +1 -1
  99. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__c4e78a20._.js +3 -0
  100. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__e5dec879._.js.map → [root-of-the-server]__c4e78a20._.js.map} +1 -1
  101. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js +3 -0
  102. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js.map +1 -0
  103. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js +3 -0
  104. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js.map +1 -0
  105. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js +1 -1
  106. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js.map +1 -1
  107. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js +2 -2
  108. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js.map +1 -1
  109. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js +4 -0
  110. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js.map +1 -0
  111. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js +3 -0
  112. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js.map +1 -0
  113. package/mcp-server/.next/server/middleware-build-manifest.js +3 -3
  114. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  115. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  116. package/mcp-server/.next/static/chunks/274a8d03fad7f819.js +1 -0
  117. package/mcp-server/.next/static/chunks/3d37ed424c6aaf63.css +1 -0
  118. package/mcp-server/.next/static/chunks/543e14c771a22442.js +1 -0
  119. package/mcp-server/.next/static/chunks/58fdd5192b305065.js +1 -0
  120. package/mcp-server/.next/static/chunks/6bd684c2018a357c.js +1 -0
  121. package/mcp-server/.next/static/chunks/6d59e588420330ca.js +1 -0
  122. package/mcp-server/.next/static/chunks/9625e4da85a132f3.js +1 -0
  123. package/mcp-server/.next/static/chunks/c36bc797d535a4dc.js +1 -0
  124. package/mcp-server/.next/static/chunks/{turbopack-7cd5a898ed038e26.js → turbopack-9656e7304584cab2.js} +2 -2
  125. package/mcp-server/app/api/jank/[session]/route.ts +344 -0
  126. package/mcp-server/app/api/screenshots/list/route.ts +22 -0
  127. package/mcp-server/app/logs/LogsClient.tsx +33 -0
  128. package/mcp-server/app/logs/utils.ts +2 -0
  129. package/mcp-server/app/mcp/route.ts +20 -0
  130. package/mcp-server/app/mcp/tools.ts +288 -9
  131. package/mcp-server/app/video/[session]/page.tsx +237 -0
  132. package/mcp-server/package.json +4 -0
  133. package/package.json +1 -1
  134. package/src/tui-interface-impl.tsx +15 -7
  135. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js +0 -34
  136. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js.map +0 -1
  137. package/mcp-server/.next/server/chunks/[root-of-the-server]__5580d2ea._.js.map +0 -1
  138. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__8db775f9._.js +0 -3
  139. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e5dec879._.js +0 -3
  140. package/mcp-server/.next/static/chunks/11ac0d0e69696c72.js +0 -1
  141. package/mcp-server/.next/static/chunks/172f6179d608c15f.js +0 -1
  142. package/mcp-server/.next/static/chunks/65b18bf1ede9811a.css +0 -1
  143. package/mcp-server/.next/static/chunks/bb8a4e5f381a85ec.js +0 -1
  144. package/mcp-server/.next/static/chunks/e09be78bba2194fd.js +0 -1
  145. package/mcp-server/.next/static/chunks/fec3a6ddaef02b8b.js +0 -1
  146. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js.map → [root-of-the-server]__2056c8b5._.js.map} +0 -0
  147. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js.map → [root-of-the-server]__55c04517._.js.map} +0 -0
  148. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js.map → [root-of-the-server]__bc773251._.js.map} +0 -0
  149. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → T9qI0it_H-9Z-TE5_ROut}/_buildManifest.js +0 -0
  150. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → T9qI0it_H-9Z-TE5_ROut}/_clientMiddlewareManifest.json +0 -0
  151. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → T9qI0it_H-9Z-TE5_ROut}/_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",