dev3000 0.0.91 → 0.0.92

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 (28) 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 +22 -13
  6. package/dist/tui-interface-impl.d.ts.map +1 -1
  7. package/dist/tui-interface-impl.js +19 -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/prerender-manifest.json +3 -3
  13. package/mcp-server/.next/server/app/_global-error.html +2 -2
  14. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  15. package/mcp-server/.next/server/app/_not-found.html +1 -1
  16. package/mcp-server/.next/server/app/_not-found.rsc +1 -1
  17. package/mcp-server/.next/server/app/index.html +1 -1
  18. package/mcp-server/.next/server/app/index.rsc +1 -1
  19. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js +6 -6
  20. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js.map +1 -1
  21. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  22. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  23. package/mcp-server/app/mcp/tools.ts +151 -24
  24. package/package.json +1 -1
  25. package/src/tui-interface-impl.tsx +22 -13
  26. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → _qXVPjMKmdbmLNrnd_3zK}/_buildManifest.js +0 -0
  27. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → _qXVPjMKmdbmLNrnd_3zK}/_clientMiddlewareManifest.json +0 -0
  28. /package/mcp-server/.next/static/{eVL_05d0pOH_qw2twMoct → _qXVPjMKmdbmLNrnd_3zK}/_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\": \"lZgibe2ziYqyEAkZ7RFH62f938aB/wxVlceTcZMnbR8=\"\n}"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "node": {},
3
3
  "edge": {},
4
- "encryptionKey": "mkpbqeA8hbQBwumcGof8mtazY/m9j2lVb+4otV6eSo0="
4
+ "encryptionKey": "lZgibe2ziYqyEAkZ7RFH62f938aB/wxVlceTcZMnbR8="
5
5
  }
@@ -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.92",
4
4
  "description": "AI-powered development tools with browser monitoring and MCP server integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -45,8 +45,9 @@ const TUIApp = ({
45
45
  const [initStatus, setInitStatus] = useState<string | null>("Initializing...")
46
46
  const [appPort, setAppPort] = useState<string>(initialAppPort)
47
47
  const logIdCounter = useRef(0)
48
+ const [clearFromLogId, setClearFromLogId] = useState<number>(0) // Track log ID to clear from
48
49
  const { stdout } = useStdout()
49
- const ctrlCMessageDefault = "^C quit"
50
+ const ctrlCMessageDefault = "^L clear ^C quit"
50
51
  const [ctrlCMessage, setCtrlCMessage] = useState(ctrlCMessageDefault)
51
52
 
52
53
  const [terminalSize, setTerminalSize] = useState(() => ({
@@ -228,12 +229,19 @@ const TUIApp = ({
228
229
  if (key.ctrl && input === "c") {
229
230
  // Send SIGINT to trigger main process shutdown handler
230
231
  process.kill(process.pid, "SIGINT")
232
+ } else if (key.ctrl && input === "l") {
233
+ // Ctrl-L: Clear logs box - set clear point to last log ID
234
+ const lastLogId = logs.length > 0 ? logs[logs.length - 1].id : logIdCounter.current
235
+ setClearFromLogId(lastLogId)
236
+ setScrollOffset(0) // Reset scroll to bottom
231
237
  } else if (key.upArrow) {
232
- setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, logs.length - maxVisibleLogs)))
238
+ const filteredCount = logs.filter((log) => log.id > clearFromLogId).length
239
+ setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, filteredCount - maxVisibleLogs)))
233
240
  } else if (key.downArrow) {
234
241
  setScrollOffset((prev) => Math.max(0, prev - 1))
235
242
  } else if (key.pageUp) {
236
- setScrollOffset((prev) => Math.min(prev + maxVisibleLogs, Math.max(0, logs.length - maxVisibleLogs)))
243
+ const filteredCount = logs.filter((log) => log.id > clearFromLogId).length
244
+ setScrollOffset((prev) => Math.min(prev + maxVisibleLogs, Math.max(0, filteredCount - maxVisibleLogs)))
237
245
  } else if (key.pageDown) {
238
246
  setScrollOffset((prev) => Math.max(0, prev - maxVisibleLogs))
239
247
  } else if (input === "g" && key.shift) {
@@ -241,12 +249,17 @@ const TUIApp = ({
241
249
  setScrollOffset(0)
242
250
  } else if (input === "g" && !key.shift) {
243
251
  // g to go to beginning
244
- setScrollOffset(Math.max(0, logs.length - maxVisibleLogs))
252
+ const filteredCount = logs.filter((log) => log.id > clearFromLogId).length
253
+ setScrollOffset(Math.max(0, filteredCount - maxVisibleLogs))
245
254
  }
246
255
  })
247
256
 
248
- // Calculate visible logs
249
- const visibleLogs = logs.slice(Math.max(0, logs.length - maxVisibleLogs - scrollOffset), logs.length - scrollOffset)
257
+ // Calculate visible logs - filter to only show logs after the clear point
258
+ const filteredLogs = logs.filter((log) => log.id > clearFromLogId)
259
+ const visibleLogs = filteredLogs.slice(
260
+ Math.max(0, filteredLogs.length - maxVisibleLogs - scrollOffset),
261
+ filteredLogs.length - scrollOffset
262
+ )
250
263
 
251
264
  // Render compact header for small terminals
252
265
  const renderCompactHeader = () => (
@@ -331,7 +344,7 @@ const TUIApp = ({
331
344
  <Box flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1} flexGrow={1} minHeight={0}>
332
345
  {!isVeryCompact && (
333
346
  <Text color="gray" dimColor>
334
- Logs ({logs.length} total{scrollOffset > 0 && `, scrolled up ${scrollOffset} lines`})
347
+ Logs ({filteredLogs.length} total{scrollOffset > 0 && `, scrolled up ${scrollOffset} lines`})
335
348
  </Text>
336
349
  )}
337
350
 
@@ -427,10 +440,6 @@ const TUIApp = ({
427
440
  // Single space after type
428
441
  const typeSpacing = ""
429
442
 
430
- // For alignment: if no type tag, add spacing equivalent to a tag
431
- // This aligns SERVER logs without tags with those that have tags
432
- const alignmentSpacing = !type ? " " : "" // ~7 chars for average tag like [GET]
433
-
434
443
  return (
435
444
  <Text key={log.id} wrap="truncate-end">
436
445
  <Text dimColor>[{timestamp}]</Text>
@@ -438,14 +447,14 @@ const TUIApp = ({
438
447
  <Text color={sourceColor} bold>
439
448
  [{source}]
440
449
  </Text>
441
- <Text>{sourceSpacing} </Text>
442
450
  {type ? (
443
451
  <>
452
+ <Text>{sourceSpacing} </Text>
444
453
  <Text color={typeColors[type] || "#A0A0A0"}>[{type}]</Text>
445
454
  <Text>{typeSpacing} </Text>
446
455
  </>
447
456
  ) : (
448
- <Text>{alignmentSpacing}</Text>
457
+ <Text> </Text>
449
458
  )}
450
459
  <Text>{message}</Text>
451
460
  </Text>