dev3000 0.0.91 → 0.0.93

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 (55) hide show
  1. package/dist/screencast-manager.d.ts +1 -0
  2. package/dist/screencast-manager.d.ts.map +1 -1
  3. package/dist/screencast-manager.js +29 -4
  4. package/dist/screencast-manager.js.map +1 -1
  5. package/dist/src/tui-interface-impl.tsx +28 -13
  6. package/dist/tui-interface-impl.d.ts.map +1 -1
  7. package/dist/tui-interface-impl.js +23 -11
  8. package/dist/tui-interface-impl.js.map +1 -1
  9. package/mcp-server/.next/BUILD_ID +1 -1
  10. package/mcp-server/.next/build-manifest.json +2 -2
  11. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  12. package/mcp-server/.next/next-minimal-server.js.nft.json +1 -1
  13. package/mcp-server/.next/next-server.js.nft.json +1 -1
  14. package/mcp-server/.next/prerender-manifest.json +3 -3
  15. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  16. package/mcp-server/.next/server/app/_global-error.html +2 -2
  17. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  18. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/mcp-server/.next/server/app/_not-found.html +1 -1
  20. package/mcp-server/.next/server/app/_not-found.rsc +1 -1
  21. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -1
  22. package/mcp-server/.next/server/app/api/logs/append/route.js.nft.json +1 -1
  23. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  24. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  25. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  26. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  27. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  28. package/mcp-server/.next/server/app/api/orchestrator/route.js.nft.json +1 -1
  29. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  30. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -1
  31. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  32. package/mcp-server/.next/server/app/index.html +1 -1
  33. package/mcp-server/.next/server/app/index.rsc +1 -1
  34. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  35. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  36. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  37. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -1
  38. package/mcp-server/.next/server/chunks/[root-of-the-server]__94037b23._.js +2 -2
  39. package/mcp-server/.next/server/chunks/[root-of-the-server]__94037b23._.js.map +1 -1
  40. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js +6 -6
  41. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js.map +1 -1
  42. package/mcp-server/.next/server/chunks/[root-of-the-server]__cc74dbef._.js +2 -2
  43. package/mcp-server/.next/server/chunks/[root-of-the-server]__cc74dbef._.js.map +1 -1
  44. package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js +1 -1
  45. package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js.map +1 -1
  46. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  47. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  48. package/mcp-server/app/mcp/client-manager.ts +76 -23
  49. package/mcp-server/app/mcp/route.ts +140 -15
  50. package/mcp-server/app/mcp/tools.ts +151 -24
  51. package/package.json +1 -1
  52. package/src/tui-interface-impl.tsx +28 -13
  53. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → JCjxP4rkK3FXLwJCSQET6}/_buildManifest.js +0 -0
  54. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → JCjxP4rkK3FXLwJCSQET6}/_clientMiddlewareManifest.json +0 -0
  55. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → JCjxP4rkK3FXLwJCSQET6}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"mkpbqeA8hbQBwumcGof8mtazY/m9j2lVb+4otV6eSo0=\"\n}"
1
+ self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"rm261543LMD/+EU2F6XJHyupyv2BrageVvOxdXx78XI=\"\n}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "node": {},
3
3
  "edge": {},
4
- "encryptionKey": "mkpbqeA8hbQBwumcGof8mtazY/m9j2lVb+4otV6eSo0="
4
+ "encryptionKey": "rm261543LMD/+EU2F6XJHyupyv2BrageVvOxdXx78XI="
5
5
  }
@@ -32,6 +32,8 @@ export class MCPClientManager {
32
32
  private tools: Map<string, Tool[]> = new Map()
33
33
  private reconnectTimers: Map<string, NodeJS.Timeout> = new Map()
34
34
  private lastConfigs: Map<string, MCPClientConfig> = new Map()
35
+ private toolDiscoveryListeners: Array<() => void> = []
36
+ private toolUpdateSubscribers: Array<(info: { mcpName: string; tools: Tool[] }) => void> = []
35
37
 
36
38
  /**
37
39
  * Initialize MCP clients for available downstream servers
@@ -118,26 +120,23 @@ export class MCPClientManager {
118
120
 
119
121
  // Discover available tools (non-fatal - connection already succeeded)
120
122
  try {
121
- const toolsResult = await client.request(
122
- {
123
- method: "tools/list",
124
- params: {}
125
- },
126
- undefined as any // Type inference will work at runtime
127
- )
128
-
129
- if (toolsResult && "tools" in toolsResult) {
130
- this.tools.set(config.name, toolsResult.tools as Tool[])
131
- console.log(`[MCP Orchestrator] Discovered ${toolsResult.tools.length} tools from ${config.name}`)
123
+ const toolsResult = await client.listTools()
124
+ const discoveredTools = Array.isArray(toolsResult?.tools) ? (toolsResult.tools as Tool[]) : []
125
+
126
+ this.tools.set(config.name, discoveredTools)
127
+
128
+ if (discoveredTools.length > 0) {
129
+ console.log(`[MCP Orchestrator] Discovered ${discoveredTools.length} tools from ${config.name}`)
130
+ this.notifyToolDiscovery()
132
131
  } else {
133
- // Set empty array if no tools found
134
- this.tools.set(config.name, [])
135
132
  console.log(`[MCP Orchestrator] No tools discovered from ${config.name} (will retry on first use)`)
136
133
  }
134
+ this.notifyToolsUpdated(config.name)
137
135
  } catch (_error) {
138
136
  // Tool discovery failed but connection succeeded - tools will be discovered on first use
139
137
  this.tools.set(config.name, [])
140
138
  console.log(`[MCP Orchestrator] Tool discovery deferred for ${config.name} (will discover on first tool call)`)
139
+ this.notifyToolsUpdated(config.name)
141
140
  }
142
141
  }
143
142
 
@@ -167,16 +166,10 @@ export class MCPClientManager {
167
166
  }
168
167
 
169
168
  try {
170
- const result = await client.request(
171
- {
172
- method: "tools/call",
173
- params: {
174
- name: toolName,
175
- arguments: args
176
- }
177
- },
178
- undefined as any // Type inference will work at runtime
179
- )
169
+ const result = await client.callTool({
170
+ name: toolName,
171
+ arguments: args
172
+ })
180
173
 
181
174
  return result as CallToolResult
182
175
  } catch (error) {
@@ -199,6 +192,66 @@ export class MCPClientManager {
199
192
  return Array.from(this.clients.keys())
200
193
  }
201
194
 
195
+ /**
196
+ * Subscribe to tool updates for a specific MCP. Returns an unsubscribe function.
197
+ */
198
+ onToolsUpdated(subscriber: (info: { mcpName: string; tools: Tool[] }) => void): () => void {
199
+ this.toolUpdateSubscribers.push(subscriber)
200
+ return () => {
201
+ this.toolUpdateSubscribers = this.toolUpdateSubscribers.filter((fn) => fn !== subscriber)
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Wait for at least one downstream MCP to finish tool discovery, or resolve after a timeout.
207
+ * Prevents registering an empty toolset before downstream servers are ready.
208
+ */
209
+ async waitForInitialTools(timeoutMs: number = 8000): Promise<void> {
210
+ if (this.getAllTools().length > 0) {
211
+ return
212
+ }
213
+
214
+ await new Promise<void>((resolve) => {
215
+ let timeoutId: NodeJS.Timeout
216
+
217
+ const onDiscovery = () => {
218
+ clearTimeout(timeoutId)
219
+ this.toolDiscoveryListeners = this.toolDiscoveryListeners.filter((listener) => listener !== onDiscovery)
220
+ resolve()
221
+ }
222
+
223
+ timeoutId = setTimeout(() => {
224
+ this.toolDiscoveryListeners = this.toolDiscoveryListeners.filter((listener) => listener !== onDiscovery)
225
+ resolve()
226
+ }, timeoutMs)
227
+
228
+ this.toolDiscoveryListeners.push(onDiscovery)
229
+ })
230
+ }
231
+
232
+ private notifyToolDiscovery(): void {
233
+ if (this.toolDiscoveryListeners.length === 0 || this.getAllTools().length === 0) {
234
+ return
235
+ }
236
+
237
+ const listeners = [...this.toolDiscoveryListeners]
238
+ this.toolDiscoveryListeners = []
239
+ for (const listener of listeners) {
240
+ listener()
241
+ }
242
+ }
243
+
244
+ private notifyToolsUpdated(mcpName: string): void {
245
+ const tools = this.tools.get(mcpName) ?? []
246
+ for (const subscriber of this.toolUpdateSubscribers) {
247
+ try {
248
+ subscriber({ mcpName, tools })
249
+ } catch (error) {
250
+ console.warn(`[MCP Orchestrator] Tool update subscriber error for ${mcpName}:`, error)
251
+ }
252
+ }
253
+ }
254
+
202
255
  /**
203
256
  * Schedule reconnection for a disconnected MCP
204
257
  */
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { homedir } from "node:os"
3
3
  import { join } from "node:path"
4
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js"
4
5
  import { createMcpHandler } from "mcp-handler"
5
6
  import { z } from "zod"
6
7
  import { getMCPClientManager } from "./client-manager"
@@ -95,14 +96,39 @@ const initializeOrchestration = async () => {
95
96
  return config
96
97
  }
97
98
 
99
+ const waitForInitialConfig = async (
100
+ timeoutMs: number = 10000,
101
+ pollIntervalMs: number = 250
102
+ ): Promise<{ config: Parameters<typeof clientManager.initialize>[0]; waited: boolean }> => {
103
+ const startTime = Date.now()
104
+ let waited = false
105
+ let config = getConfigFromSessions()
106
+
107
+ while (Object.keys(config).length === 0 && Date.now() - startTime < timeoutMs) {
108
+ if (!waited) {
109
+ console.log("[MCP Orchestrator] Waiting for session info before connecting downstream MCPs...")
110
+ waited = true
111
+ }
112
+
113
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
114
+ config = getConfigFromSessions()
115
+ }
116
+
117
+ return { config, waited }
118
+ }
119
+
98
120
  try {
99
121
  // Initial attempt to connect
100
- const config = getConfigFromSessions()
122
+ const { config, waited } = await waitForInitialConfig()
101
123
  if (Object.keys(config).length > 0) {
102
124
  await clientManager.initialize(config)
103
125
  console.log(`[MCP Orchestrator] Initialized with ${Object.keys(config).join(", ")}`)
104
126
  } else {
105
- console.log("[MCP Orchestrator] No downstream MCPs found yet (will retry)")
127
+ if (waited) {
128
+ console.log("[MCP Orchestrator] No downstream MCPs detected after waiting for session info (will retry)")
129
+ } else {
130
+ console.log("[MCP Orchestrator] No downstream MCPs found yet (will retry)")
131
+ }
106
132
  }
107
133
 
108
134
  // Since MCP server starts before Chrome, periodically retry connection
@@ -146,29 +172,128 @@ const initializeOrchestration = async () => {
146
172
  }
147
173
 
148
174
  // Initialize on module load
149
- initializeOrchestration().catch(console.error)
175
+ const orchestrationReady = initializeOrchestration().catch((error) => {
176
+ console.error("[MCP Orchestrator] Failed to initialize downstream MCPs:", error)
177
+ })
150
178
 
151
179
  const handler = createMcpHandler(
152
- (server) => {
153
- // Dynamically register proxied tools from downstream MCPs
180
+ async (server) => {
154
181
  const clientManager = getMCPClientManager()
155
- const downstreamTools = clientManager.getAllTools()
156
182
 
157
- for (const { mcpName, tool } of downstreamTools) {
158
- // Add prefix to avoid conflicts with dev3000's own tools
183
+ await orchestrationReady
184
+ await clientManager.waitForInitialTools()
185
+
186
+ const registeredProxiedTools = new Map<
187
+ string,
188
+ {
189
+ mcpName: string
190
+ toolName: string
191
+ registered: ReturnType<typeof server.tool>
192
+ }
193
+ >()
194
+
195
+ const registerOrUpdateProxiedTool = (mcpName: string, tool: Tool): boolean => {
159
196
  const proxiedToolName = `${mcpName}_${tool.name}`
197
+ const existing = registeredProxiedTools.get(proxiedToolName)
198
+ const description = `[${mcpName}] ${tool.description || ""}`
199
+ const annotations = {
200
+ ...(tool.annotations ?? {}),
201
+ proxiedFrom: mcpName,
202
+ originalInputSchema: tool.inputSchema
203
+ }
204
+
205
+ if (existing) {
206
+ existing.registered.update({
207
+ description,
208
+ annotations
209
+ })
210
+ return false
211
+ }
160
212
 
161
- server.tool(
162
- proxiedToolName,
163
- `[${mcpName}] ${tool.description || ""}`,
164
- tool.inputSchema as any,
165
- async (params: Record<string, unknown>) => {
166
- // Proxy the call to the downstream MCP
213
+ try {
214
+ const proxiedTool = server.tool(proxiedToolName, description, {}, async (params: Record<string, unknown>) => {
167
215
  return clientManager.callTool(mcpName, tool.name, params)
216
+ })
217
+
218
+ // Allow arbitrary argument objects to pass through to downstream MCPs
219
+ proxiedTool.inputSchema = z.object({}).passthrough()
220
+
221
+ proxiedTool.update({
222
+ annotations
223
+ })
224
+
225
+ registeredProxiedTools.set(proxiedToolName, {
226
+ mcpName,
227
+ toolName: tool.name,
228
+ registered: proxiedTool
229
+ })
230
+
231
+ console.log(`[MCP Orchestrator] Registered proxied tool ${proxiedToolName}`)
232
+ return true
233
+ } catch (error) {
234
+ console.warn(`[MCP Orchestrator] Failed to register proxied tool ${proxiedToolName}:`, error)
235
+ return false
236
+ }
237
+ }
238
+
239
+ const removeToolsForMcp = (mcpName: string): number => {
240
+ let removed = 0
241
+ for (const [proxiedToolName, entry] of registeredProxiedTools.entries()) {
242
+ if (entry.mcpName === mcpName) {
243
+ try {
244
+ entry.registered.remove()
245
+ registeredProxiedTools.delete(proxiedToolName)
246
+ removed++
247
+ console.log(`[MCP Orchestrator] Removed proxied tool ${proxiedToolName}`)
248
+ } catch (error) {
249
+ console.warn(`[MCP Orchestrator] Failed to remove proxied tool ${proxiedToolName}:`, error)
250
+ }
168
251
  }
169
- )
252
+ }
253
+ return removed
170
254
  }
171
255
 
256
+ // Dynamically register proxied tools from downstream MCPs
257
+ const downstreamTools = clientManager.getAllTools()
258
+
259
+ if (downstreamTools.length === 0) {
260
+ console.log("[MCP Orchestrator] No downstream MCP tools available during initial registration")
261
+ } else {
262
+ console.log(`[MCP Orchestrator] Registering ${downstreamTools.length} downstream MCP tools`)
263
+ }
264
+
265
+ let initialNewTools = 0
266
+ for (const { mcpName, tool } of downstreamTools) {
267
+ if (registerOrUpdateProxiedTool(mcpName, tool)) {
268
+ initialNewTools++
269
+ }
270
+ }
271
+
272
+ if (initialNewTools > 0) {
273
+ server.sendToolListChanged()
274
+ }
275
+
276
+ clientManager.onToolsUpdated(({ mcpName, tools }) => {
277
+ if (tools.length === 0) {
278
+ const removed = removeToolsForMcp(mcpName)
279
+ if (removed > 0) {
280
+ server.sendToolListChanged()
281
+ }
282
+ return
283
+ }
284
+
285
+ let addedOrUpdated = 0
286
+ for (const tool of tools) {
287
+ if (registerOrUpdateProxiedTool(mcpName, tool)) {
288
+ addedOrUpdated++
289
+ }
290
+ }
291
+
292
+ if (addedOrUpdated > 0) {
293
+ server.sendToolListChanged()
294
+ }
295
+ })
296
+
172
297
  // Dev3000's own tools below:
173
298
  // Enhanced fix_my_app - the ultimate error fixing tool
174
299
  server.tool(
@@ -669,7 +669,13 @@ export async function fixMyApp({
669
669
  )
670
670
  }
671
671
 
672
- results.push(`📹 **View all frames**: ${videoUrl}`)
672
+ const triggerLabel =
673
+ jankResult.captureTrigger === "navigation"
674
+ ? "Navigation complete"
675
+ : jankResult.captureTrigger === "load"
676
+ ? "Load complete"
677
+ : "View all frames"
678
+ results.push(`📹 **${triggerLabel}**: ${videoUrl}`)
673
679
  results.push(`🎞️ **Session ID**: ${jankResult.sessionId} (${jankResult.totalFrames} frames)`)
674
680
  results.push("")
675
681
 
@@ -2122,48 +2128,128 @@ export async function getMcpCapabilities({
2122
2128
  * - Layout shifts: Elements move to new positions (top region changes while bottom stays same)
2123
2129
  * - Content changes: Same regions change in-place (image loads with pixels appearing)
2124
2130
  */
2125
- function detectLayoutShiftVsContentChange(prevPng: PNG, currPng: PNG): { isLayoutShift: boolean; shiftScore: number } {
2131
+ function detectLayoutShiftVsContentChange(
2132
+ prevPng: PNG,
2133
+ currPng: PNG
2134
+ ): { isLayoutShift: boolean; shiftScore: number; isOverlayNoise: boolean } {
2126
2135
  const width = prevPng.width
2127
2136
  const height = prevPng.height
2128
2137
 
2129
- // Divide screen into horizontal bands (top, middle, bottom)
2130
- const bandHeight = Math.floor(height / 8)
2131
- const bands = Array(8).fill(0)
2138
+ // Track changes at row-level for detecting correlated shifts
2139
+ const rowChangeCounts = new Array(height).fill(0)
2132
2140
 
2133
- // Count changed pixels in each band
2141
+ // Count changed pixels per row (for correlation analysis)
2134
2142
  for (let y = 0; y < height; y++) {
2135
- const bandIndex = Math.min(Math.floor(y / bandHeight), 7)
2136
2143
  for (let x = 0; x < width; x++) {
2137
2144
  const idx = (width * y + x) << 2
2138
2145
  const rDiff = Math.abs(prevPng.data[idx] - currPng.data[idx])
2139
2146
  const gDiff = Math.abs(prevPng.data[idx + 1] - currPng.data[idx + 1])
2140
2147
  const bDiff = Math.abs(prevPng.data[idx + 2] - currPng.data[idx + 2])
2141
2148
 
2142
- // If any channel differs significantly, count as changed pixel
2143
2149
  if (rDiff > 30 || gDiff > 30 || bDiff > 30) {
2144
- bands[bandIndex]++
2150
+ rowChangeCounts[y]++
2145
2151
  }
2146
2152
  }
2147
2153
  }
2148
2154
 
2149
- // Calculate percentage of pixels changed in each band
2155
+ // Calculate percentage of pixels changed per row
2156
+ const rowChangePercents = rowChangeCounts.map((count) => (count / width) * 100)
2157
+
2158
+ // Detect consecutive rows with high change (indicates shift boundary)
2159
+ // True CLS: Many consecutive rows change together (content moved as a block)
2160
+ let maxConsecutiveHighChangeRows = 0
2161
+ let currentConsecutive = 0
2162
+
2163
+ for (let i = 0; i < height; i++) {
2164
+ if (rowChangePercents[i] > 50) {
2165
+ // >50% of row changed
2166
+ currentConsecutive++
2167
+ maxConsecutiveHighChangeRows = Math.max(maxConsecutiveHighChangeRows, currentConsecutive)
2168
+ } else {
2169
+ currentConsecutive = 0
2170
+ }
2171
+ }
2172
+
2173
+ // Detect isolated hotspots (fixed/absolute overlay noise)
2174
+ // Pattern: low change → spike → low change (element appearing in place)
2175
+ let isolatedHotspots = 0
2176
+ const windowSize = 5
2177
+
2178
+ for (let i = windowSize; i < height - windowSize; i++) {
2179
+ // Calculate average change in windows before, during, and after
2180
+ const before = rowChangePercents.slice(i - windowSize, i).reduce((a, b) => a + b, 0) / windowSize
2181
+ const during = rowChangePercents[i]
2182
+ const after = rowChangePercents.slice(i + 1, i + windowSize + 1).reduce((a, b) => a + b, 0) / windowSize
2183
+
2184
+ // Isolated spike: calm before/after, high during
2185
+ if (before < 10 && during > 60 && after < 10) {
2186
+ isolatedHotspots++
2187
+ }
2188
+ }
2189
+
2190
+ // Detect narrow fixed elements (toolbars, indicators)
2191
+ // Pattern: Many rows with LOW percentage change (5-25%) = narrow element across many rows
2192
+ // This catches toolbars/indicators that are thin but tall
2193
+ let narrowChangeRows = 0
2194
+ for (let i = 0; i < height; i++) {
2195
+ // Low but consistent change (narrow element)
2196
+ if (rowChangePercents[i] > 5 && rowChangePercents[i] < 25) {
2197
+ narrowChangeRows++
2198
+ }
2199
+ }
2200
+
2201
+ // If many rows have narrow changes, this is likely a fixed toolbar/sidebar
2202
+ const hasNarrowFixedElement = narrowChangeRows > height * 0.3 // >30% of rows have narrow changes
2203
+
2204
+ // Calculate band-based metrics for backward compatibility
2205
+ const bandHeight = Math.floor(height / 8)
2206
+ const bands = Array(8).fill(0)
2207
+
2208
+ for (let y = 0; y < height; y++) {
2209
+ const bandIndex = Math.min(Math.floor(y / bandHeight), 7)
2210
+ bands[bandIndex] += rowChangeCounts[y]
2211
+ }
2212
+
2150
2213
  const pixelsPerBand = width * bandHeight
2151
2214
  const bandPercentages = bands.map((count) => (count / pixelsPerBand) * 100)
2152
-
2153
- // Layout shift pattern: High change in top bands (nav/header area), low change in bottom
2154
- // Content change pattern: Evenly distributed or contained to specific regions
2155
2215
  const topBandChange = (bandPercentages[0] + bandPercentages[1]) / 2
2156
2216
  const bottomBandChange = (bandPercentages[6] + bandPercentages[7]) / 2
2157
2217
 
2158
- // If top 25% of screen has >5% pixel change but bottom has <2% change = layout shift
2159
- const isLayoutShift = topBandChange > 5 && bottomBandChange < 2 && topBandChange > bottomBandChange * 2
2160
-
2161
- // Calculate shift score (similar to CLS score)
2218
+ // Calculate variance to detect if changes are uniform (shift) or scattered (overlay)
2219
+ const meanChange = bandPercentages.reduce((a, b) => a + b, 0) / bandPercentages.length
2220
+ const variance = bandPercentages.reduce((sum, val) => sum + (val - meanChange) ** 2, 0) / bandPercentages.length
2221
+
2222
+ // Determine if this is a layout shift or overlay noise
2223
+ // True layout shift indicators:
2224
+ // 1. Many consecutive rows changed (>20 rows = significant shift)
2225
+ // 2. Top heavy change pattern (topBandChange > bottomBandChange)
2226
+ // 3. Low variance (uniform change across bands)
2227
+ // 4. Few isolated hotspots
2228
+
2229
+ const hasConsecutiveShift = maxConsecutiveHighChangeRows > 20
2230
+ const hasTopHeavyPattern = topBandChange > 5 && bottomBandChange < 2 && topBandChange > bottomBandChange * 2
2231
+ const hasUniformChange = variance < 200 && meanChange > 10
2232
+ const hasIsolatedHotspots = isolatedHotspots >= 3
2233
+
2234
+ // Overlay noise indicators:
2235
+ // 1. High variance (scattered changes)
2236
+ // 2. Multiple isolated hotspots
2237
+ // 3. Few consecutive rows changed
2238
+ // 4. Narrow fixed element (toolbar/indicator pattern)
2239
+ const isOverlayNoise =
2240
+ hasNarrowFixedElement || // Narrow element like toolbar
2241
+ (hasIsolatedHotspots && !hasConsecutiveShift && (variance > 500 || meanChange < 10))
2242
+
2243
+ // Layout shift: Either consecutive shift pattern OR traditional top-heavy pattern
2244
+ // But NOT if it looks like overlay noise
2245
+ const isLayoutShift = !isOverlayNoise && (hasConsecutiveShift || hasTopHeavyPattern || hasUniformChange)
2246
+
2247
+ // Calculate shift score
2162
2248
  const totalChanged = bands.reduce((sum, count) => sum + count, 0)
2163
2249
  const totalPixels = width * height
2164
- const shiftScore = (totalChanged / totalPixels) * 0.1 // Rough CLS equivalent
2250
+ const shiftScore = (totalChanged / totalPixels) * 0.1
2165
2251
 
2166
- return { isLayoutShift, shiftScore }
2252
+ return { isLayoutShift, shiftScore, isOverlayNoise }
2167
2253
  }
2168
2254
 
2169
2255
  /**
@@ -2186,6 +2272,7 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2186
2272
  totalFrames: number
2187
2273
  screenshotDir: string
2188
2274
  realCLS?: { score: number; grade: string }
2275
+ captureTrigger?: "navigation" | "load"
2189
2276
  }> {
2190
2277
  const screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots")
2191
2278
 
@@ -2224,13 +2311,20 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2224
2311
  | {
2225
2312
  score: number
2226
2313
  grade: string
2227
- shifts: Array<{ score: number; timestamp: number; sources?: Array<{ node?: string }> }>
2314
+ shifts: Array<{
2315
+ score: number
2316
+ timestamp: number
2317
+ sources?: Array<{ node?: string; position?: string | null }>
2318
+ }>
2228
2319
  }
2229
2320
  | undefined
2321
+ let captureTrigger: "navigation" | "load" | undefined
2230
2322
 
2231
2323
  if (existsSync(metadataPath)) {
2232
2324
  try {
2233
2325
  const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"))
2326
+ // Capture the trigger type for use in output messages
2327
+ captureTrigger = metadata.captureTrigger
2234
2328
  // Set realCLSData even if there are zero shifts - this tells us Chrome ran and found nothing
2235
2329
  if (metadata.layoutShifts !== undefined) {
2236
2330
  realCLSData = {
@@ -2329,15 +2423,27 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2329
2423
  continue
2330
2424
  }
2331
2425
 
2332
- // Detect if this is a layout shift vs content change
2426
+ // Detect if this is a layout shift vs content change vs overlay noise
2333
2427
  const shiftAnalysis = detectLayoutShiftVsContentChange(prevPng, currPng)
2334
2428
 
2335
- // If we detect a layout shift (not just content loading), flag it
2429
+ // Skip if this looks like overlay noise (fixed/absolute elements like Next.js dev indicator or Vercel toolbar)
2430
+ if (shiftAnalysis.isOverlayNoise) {
2431
+ logToDevFile(
2432
+ `Pixel Diff Hydration: Skipping frame ${i} - detected overlay noise (fixed/absolute elements), not true CLS`
2433
+ )
2434
+ continue
2435
+ }
2436
+
2437
+ // If we detect a true layout shift (not just content loading or overlay noise), flag it
2336
2438
  if (shiftAnalysis.isLayoutShift) {
2337
2439
  foundHydrationShift = true
2338
2440
  const timeMatch = earlyFrames[i].match(/-(\d+)ms\.png$/)
2339
2441
  const timeSinceStart = timeMatch ? parseInt(timeMatch[1], 10) : 0
2340
2442
 
2443
+ logToDevFile(
2444
+ `Pixel Diff Hydration: Detected true layout shift at ${timeSinceStart}ms (score: ${shiftAnalysis.shiftScore.toFixed(4)})`
2445
+ )
2446
+
2341
2447
  const mcpPort = process.env.MCP_PORT || "3684"
2342
2448
  jankDetections.push({
2343
2449
  timestamp: `${timeSinceStart}ms`,
@@ -2378,9 +2484,29 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2378
2484
  }
2379
2485
 
2380
2486
  // Process actual layout shifts detected by Chrome
2381
- // Trust Chrome's Layout Instability API completely - if Chrome reports it, it's real
2487
+ // Trust Chrome's Layout Instability API - BUT ONLY if we can identify the culprit element
2488
+ // and verify it's not a fixed/absolute positioned overlay
2382
2489
  realCLSData.shifts.forEach((shift) => {
2383
2490
  const element = shift.sources?.[0]?.node || "unknown"
2491
+ const position = shift.sources?.[0]?.position
2492
+
2493
+ // FILTER: Skip shifts where we couldn't identify the element
2494
+ // Chrome sometimes reports CLS for fixed overlays but fails to identify the element
2495
+ if (!shift.sources?.[0] || element === "unknown" || position === null || position === undefined) {
2496
+ logToDevFile(
2497
+ `Chrome CLS: Skipping unidentified shift (score: ${shift.score.toFixed(4)}) - cannot verify if it's a true CLS or fixed overlay noise`
2498
+ )
2499
+ return // Skip this shift - can't verify it's real
2500
+ }
2501
+
2502
+ // FILTER: Skip fixed/absolute positioned elements - these are overlays, not true CLS
2503
+ if (position === "fixed" || position === "absolute") {
2504
+ logToDevFile(
2505
+ `Chrome CLS: Filtering out ${element} shift (position: ${position}) - fixed/absolute elements don't cause true layout shifts`
2506
+ )
2507
+ return // Skip this shift
2508
+ }
2509
+
2384
2510
  const isCriticalElement = ["NAV", "HEADER", "BUTTON", "A"].includes(element.toUpperCase())
2385
2511
  const isDuringLoad = shift.timestamp < 1000 // First second
2386
2512
 
@@ -2479,7 +2605,8 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2479
2605
  detections: jankDetections,
2480
2606
  sessionId: latestSessionId,
2481
2607
  totalFrames: sessionFiles.length,
2482
- screenshotDir
2608
+ screenshotDir,
2609
+ captureTrigger
2483
2610
  }
2484
2611
  }
2485
2612
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev3000",
3
- "version": "0.0.91",
3
+ "version": "0.0.93",
4
4
  "description": "AI-powered development tools with browser monitoring and MCP server integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",