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.
Files changed (154) hide show
  1. package/dist/cdp-monitor.d.ts.map +1 -1
  2. package/dist/cdp-monitor.js +11 -2
  3. package/dist/cdp-monitor.js.map +1 -1
  4. package/dist/dev-environment.d.ts +1 -0
  5. package/dist/dev-environment.d.ts.map +1 -1
  6. package/dist/dev-environment.js +29 -3
  7. package/dist/dev-environment.js.map +1 -1
  8. package/dist/screencast-manager.d.ts +76 -0
  9. package/dist/screencast-manager.d.ts.map +1 -0
  10. package/dist/screencast-manager.js +410 -0
  11. package/dist/screencast-manager.js.map +1 -0
  12. package/dist/src/tui-interface-impl.tsx +15 -7
  13. package/dist/tui-interface-impl.d.ts.map +1 -1
  14. package/dist/tui-interface-impl.js +7 -5
  15. package/dist/tui-interface-impl.js.map +1 -1
  16. package/mcp-server/.next/BUILD_ID +1 -1
  17. package/mcp-server/.next/app-path-routes-manifest.json +4 -1
  18. package/mcp-server/.next/build-manifest.json +5 -5
  19. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  20. package/mcp-server/.next/prerender-manifest.json +3 -3
  21. package/mcp-server/.next/routes-manifest.json +16 -0
  22. package/mcp-server/.next/server/app/_global-error/page/build-manifest.json +3 -3
  23. package/mcp-server/.next/server/app/_global-error/page.js +1 -1
  24. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  25. package/mcp-server/.next/server/app/_global-error.html +2 -2
  26. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  27. package/mcp-server/.next/server/app/_not-found/page/build-manifest.json +3 -3
  28. package/mcp-server/.next/server/app/_not-found/page.js +1 -1
  29. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  30. package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  31. package/mcp-server/.next/server/app/_not-found.html +1 -1
  32. package/mcp-server/.next/server/app/_not-found.rsc +2 -2
  33. package/mcp-server/.next/server/app/api/jank/[session]/route/app-paths-manifest.json +3 -0
  34. package/mcp-server/.next/server/app/api/jank/[session]/route/build-manifest.json +11 -0
  35. package/mcp-server/.next/server/app/api/jank/[session]/route/server-reference-manifest.json +4 -0
  36. package/mcp-server/.next/server/app/api/jank/[session]/route.js +9 -0
  37. package/mcp-server/.next/server/app/api/jank/[session]/route.js.map +5 -0
  38. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -0
  39. package/mcp-server/.next/server/app/api/jank/[session]/route_client-reference-manifest.js +2 -0
  40. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  41. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  42. package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
  43. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  44. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  45. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  46. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
  47. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  48. package/mcp-server/.next/server/app/api/screenshots/list/route/app-paths-manifest.json +3 -0
  49. package/mcp-server/.next/server/app/api/screenshots/list/route/build-manifest.json +11 -0
  50. package/mcp-server/.next/server/app/api/screenshots/list/route/server-reference-manifest.json +4 -0
  51. package/mcp-server/.next/server/app/api/screenshots/list/route.js +7 -0
  52. package/mcp-server/.next/server/app/api/screenshots/list/route.js.map +5 -0
  53. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -0
  54. package/mcp-server/.next/server/app/api/screenshots/list/route_client-reference-manifest.js +2 -0
  55. package/mcp-server/.next/server/app/api/tools/route.js +1 -1
  56. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  57. package/mcp-server/.next/server/app/index.html +1 -1
  58. package/mcp-server/.next/server/app/index.rsc +3 -3
  59. package/mcp-server/.next/server/app/logs/page/build-manifest.json +3 -3
  60. package/mcp-server/.next/server/app/logs/page.js +1 -1
  61. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  62. package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  63. package/mcp-server/.next/server/app/mcp/route.js +3 -2
  64. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  65. package/mcp-server/.next/server/app/page/build-manifest.json +3 -3
  66. package/mcp-server/.next/server/app/page.js +1 -1
  67. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  68. package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
  69. package/mcp-server/.next/server/app/video/[session]/page/app-paths-manifest.json +3 -0
  70. package/mcp-server/.next/server/app/video/[session]/page/build-manifest.json +18 -0
  71. package/mcp-server/.next/server/app/video/[session]/page/next-font-manifest.json +6 -0
  72. package/mcp-server/.next/server/app/video/[session]/page/react-loadable-manifest.json +1 -0
  73. package/mcp-server/.next/server/app/video/[session]/page/server-reference-manifest.json +4 -0
  74. package/mcp-server/.next/server/app/video/[session]/page.js +15 -0
  75. package/mcp-server/.next/server/app/video/[session]/page.js.map +5 -0
  76. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -0
  77. package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +2 -0
  78. package/mcp-server/.next/server/app-paths-manifest.json +4 -1
  79. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js +34 -0
  80. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js.map +1 -0
  81. package/mcp-server/.next/server/chunks/{[root-of-the-server]__5580d2ea._.js → [root-of-the-server]__177c72c6._.js} +3 -3
  82. package/mcp-server/.next/server/chunks/[root-of-the-server]__177c72c6._.js.map +1 -0
  83. package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js → [root-of-the-server]__2056c8b5._.js} +2 -2
  84. package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js → [root-of-the-server]__55c04517._.js} +2 -2
  85. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js +3 -0
  86. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js.map +1 -0
  87. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js +3 -0
  88. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js.map +1 -0
  89. package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js → [root-of-the-server]__bc773251._.js} +2 -2
  90. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js +3 -0
  91. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js.map +1 -0
  92. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js +3 -0
  93. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js.map +1 -0
  94. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js +3 -0
  95. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js.map +1 -0
  96. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js +3 -0
  97. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js.map +1 -0
  98. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js +3 -0
  99. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js.map +1 -0
  100. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__bf771f7e._.js +3 -0
  101. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__8db775f9._.js.map → [root-of-the-server]__bf771f7e._.js.map} +1 -1
  102. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__c4e78a20._.js +3 -0
  103. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__e5dec879._.js.map → [root-of-the-server]__c4e78a20._.js.map} +1 -1
  104. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js +3 -0
  105. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js.map +1 -0
  106. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js +3 -0
  107. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js.map +1 -0
  108. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js +1 -1
  109. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js.map +1 -1
  110. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js +2 -2
  111. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js.map +1 -1
  112. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js +4 -0
  113. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js.map +1 -0
  114. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js +3 -0
  115. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js.map +1 -0
  116. package/mcp-server/.next/server/middleware-build-manifest.js +3 -3
  117. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  118. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  119. package/mcp-server/.next/static/chunks/274a8d03fad7f819.js +1 -0
  120. package/mcp-server/.next/static/chunks/3d37ed424c6aaf63.css +1 -0
  121. package/mcp-server/.next/static/chunks/543e14c771a22442.js +1 -0
  122. package/mcp-server/.next/static/chunks/58fdd5192b305065.js +1 -0
  123. package/mcp-server/.next/static/chunks/6bd684c2018a357c.js +1 -0
  124. package/mcp-server/.next/static/chunks/6d59e588420330ca.js +1 -0
  125. package/mcp-server/.next/static/chunks/9625e4da85a132f3.js +1 -0
  126. package/mcp-server/.next/static/chunks/c36bc797d535a4dc.js +1 -0
  127. package/mcp-server/.next/static/chunks/{turbopack-7cd5a898ed038e26.js → turbopack-9656e7304584cab2.js} +2 -2
  128. package/mcp-server/app/api/jank/[session]/route.ts +344 -0
  129. package/mcp-server/app/api/screenshots/list/route.ts +22 -0
  130. package/mcp-server/app/logs/LogsClient.tsx +33 -0
  131. package/mcp-server/app/logs/utils.ts +2 -0
  132. package/mcp-server/app/mcp/route.ts +20 -0
  133. package/mcp-server/app/mcp/tools.ts +288 -9
  134. package/mcp-server/app/video/[session]/page.tsx +237 -0
  135. package/mcp-server/package.json +4 -0
  136. package/package.json +1 -1
  137. package/src/tui-interface-impl.tsx +15 -7
  138. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js +0 -34
  139. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js.map +0 -1
  140. package/mcp-server/.next/server/chunks/[root-of-the-server]__5580d2ea._.js.map +0 -1
  141. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__8db775f9._.js +0 -3
  142. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e5dec879._.js +0 -3
  143. package/mcp-server/.next/static/chunks/11ac0d0e69696c72.js +0 -1
  144. package/mcp-server/.next/static/chunks/172f6179d608c15f.js +0 -1
  145. package/mcp-server/.next/static/chunks/65b18bf1ede9811a.css +0 -1
  146. package/mcp-server/.next/static/chunks/bb8a4e5f381a85ec.js +0 -1
  147. package/mcp-server/.next/static/chunks/e09be78bba2194fd.js +0 -1
  148. package/mcp-server/.next/static/chunks/fec3a6ddaef02b8b.js +0 -1
  149. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js.map → [root-of-the-server]__2056c8b5._.js.map} +0 -0
  150. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js.map → [root-of-the-server]__55c04517._.js.map} +0 -0
  151. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js.map → [root-of-the-server]__bc773251._.js.map} +0 -0
  152. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_buildManifest.js +0 -0
  153. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_clientMiddlewareManifest.json +0 -0
  154. /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_ssgManifest.js +0 -0
@@ -1,7 +1,9 @@
1
1
  import { exec } from "child_process"
2
2
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "fs"
3
- import { homedir } from "os"
3
+ import { homedir, tmpdir } from "os"
4
4
  import { join } from "path"
5
+ import pixelmatch from "pixelmatch"
6
+ import { PNG } from "pngjs"
5
7
  import { promisify } from "util"
6
8
  import { WebSocket } from "ws"
7
9
 
@@ -344,26 +346,41 @@ export async function fixMyApp({
344
346
  totalRenders: reactScanLines.filter((line) => line.includes("render")).length
345
347
  }
346
348
 
349
+ // Filter out framework noise (unfixable warnings from Next.js, React, etc.)
350
+ const frameworkNoisePatterns = [
351
+ /link rel=preload.*must have.*valid.*as/i, // Next.js font optimization warning
352
+ /next\/font/i, // Next.js font-related warnings
353
+ /automatically generated/i // Auto-generated code warnings
354
+ ]
355
+
356
+ const actionableErrors = allErrors.filter((line) => {
357
+ return !frameworkNoisePatterns.some((pattern) => pattern.test(line))
358
+ })
359
+
347
360
  // Categorize errors for better analysis
348
361
  const categorizedErrors = {
349
- serverErrors: allErrors.filter(
362
+ serverErrors: actionableErrors.filter(
350
363
  (line) => line.includes("[SERVER]") && (line.includes("ERROR") || line.includes("Exception"))
351
364
  ),
352
- browserErrors: allErrors.filter(
365
+ browserErrors: actionableErrors.filter(
353
366
  (line) =>
354
367
  line.includes("[BROWSER]") &&
355
368
  (line.includes("ERROR") || line.includes("CONSOLE ERROR") || line.includes("RUNTIME.ERROR"))
356
369
  ),
357
- buildErrors: allErrors.filter(
370
+ buildErrors: actionableErrors.filter(
358
371
  (line) => line.includes("Failed to compile") || line.includes("Type error") || line.includes("Build failed")
359
372
  ),
360
- networkErrors: allErrors.filter(
361
- (line) => line.includes("NETWORK") || line.includes("404") || line.includes("500") || line.includes("timeout")
362
- ),
363
- warnings: allErrors.filter((line) => /WARN|WARNING|deprecated/i.test(line) && !/ERROR|Exception|FAIL/i.test(line))
373
+ networkErrors: actionableErrors.filter((line) => {
374
+ // Exclude successful status codes
375
+ if (/\b(200|201|204|304)\b/.test(line)) return false
376
+ return line.includes("NETWORK") || line.includes("404") || line.includes("500") || line.includes("timeout")
377
+ }),
378
+ warnings: actionableErrors.filter(
379
+ (line) => /WARN|WARNING|deprecated/i.test(line) && !/ERROR|Exception|FAIL/i.test(line)
380
+ )
364
381
  }
365
382
 
366
- const totalErrors = allErrors.length
383
+ const totalErrors = actionableErrors.length
367
384
  const criticalErrors = totalErrors - categorizedErrors.warnings.length
368
385
 
369
386
  // Also check for any errors in the entire log file (not just time filtered)
@@ -607,6 +624,83 @@ export async function fixMyApp({
607
624
  })
608
625
  }
609
626
 
627
+ // Jank/Layout Shift Detection (from ScreencastManager passive captures)
628
+ if (focusArea === "performance" || focusArea === "all") {
629
+ const jankResult = await detectJankFromScreenshots(projectName)
630
+ if (jankResult.detections.length > 0) {
631
+ // Get MCP port for video viewer URL
632
+ const sessionInfo = findActiveSessions().find((s) => s.projectName === projectName)
633
+ const mcpPort = sessionInfo ? sessionInfo.sessionFile.match(/"mcpPort":\s*"(\d+)"/)?.[1] || "3684" : "3684"
634
+ const videoUrl = `http://localhost:${mcpPort}/video/${jankResult.sessionId}`
635
+
636
+ results.push("")
637
+
638
+ if (jankResult.realCLS) {
639
+ results.push(
640
+ `🚨 **LAYOUT SHIFT DETECTED** (${jankResult.detections.length} ${jankResult.detections.length === 1 ? "shift" : "shifts"} during page load):`
641
+ )
642
+ } else {
643
+ results.push(
644
+ `🚨 **LOADING JANK DETECTED** (${jankResult.detections.length} layout ${jankResult.detections.length === 1 ? "shift" : "shifts"} found):`
645
+ )
646
+ }
647
+
648
+ results.push(`📹 **Watch video analysis**: ${videoUrl}`)
649
+ results.push(`🎞️ **Session ID**: ${jankResult.sessionId} (${jankResult.totalFrames} frames)`)
650
+ results.push("")
651
+
652
+ jankResult.detections.forEach((jank) => {
653
+ const emoji = jank.severity === "high" ? "🔴" : jank.severity === "medium" ? "🟡" : "🟢"
654
+
655
+ if (jank.uxImpact) {
656
+ results.push(`${emoji} **${jank.timeSinceStart}ms** - ${jank.element}`)
657
+ results.push(` ${jank.uxImpact}`)
658
+ } else {
659
+ results.push(
660
+ `${emoji} **${jank.timeSinceStart}ms**: ${jank.visualDiff.toFixed(1)}% of screen changed (${jank.severity} severity)`
661
+ )
662
+ }
663
+ })
664
+
665
+ results.push("")
666
+
667
+ // Check if we have high-severity shifts that Chrome might miss
668
+ const hasCriticalShifts = jankResult.detections.some((d) => d.severity === "high")
669
+ if (hasCriticalShifts && jankResult.realCLS) {
670
+ results.push("🎯 **WHY DEV3000 CAUGHT THIS BUT CHROME MIGHT NOT:**")
671
+ results.push(
672
+ "• dev3000's PerformanceObserver is installed immediately at page load and buffers ALL shifts from the start"
673
+ )
674
+ results.push(
675
+ "• Chrome DevTools performance trace may start AFTER initial load, missing early navigation shifts"
676
+ )
677
+ results.push(
678
+ "• Our UX-focused detection flags critical element shifts (nav/header) even when CLS score is technically 'good'"
679
+ )
680
+ results.push("")
681
+ }
682
+
683
+ results.push("⚠️ **IMPORTANT: VALIDATE WITH CHROME DEVTOOLS**")
684
+ results.push(
685
+ "The pixel-diff analysis above may include false positives (images loading, position:fixed elements)."
686
+ )
687
+ results.push(
688
+ "RECOMMENDED: Run `mcp__dev3000-chrome-devtools__performance_start_trace({reload: true, autoStop: true})`"
689
+ )
690
+ results.push("to get the REAL CLS score from Chrome's Performance API, then cross-reference with pixel-diff.")
691
+ results.push("")
692
+ results.push("💡 **LAYOUT SHIFT DEBUGGING TIPS:**")
693
+ results.push("• Add explicit width/height to images and media")
694
+ results.push("• Reserve space for dynamic content (ads, embeds, etc.)")
695
+ results.push("• Avoid inserting content above existing content")
696
+ results.push("• Use CSS aspect-ratio for responsive elements")
697
+ results.push("• Check for web fonts causing text reflow (font-display: swap)")
698
+ results.push(`• Raw screenshots: ${jankResult.screenshotDir}`)
699
+ results.push("")
700
+ results.push(`🎬 **IMPORTANT**: Share this video link with the user: ${videoUrl}`)
701
+ }
702
+ }
703
+
610
704
  // React-scan performance data (if available)
611
705
  if (reactScanMetrics.totalRenders > 0 || focusArea === "performance" || focusArea === "all") {
612
706
  if (reactScanMetrics.unnecessaryRenders.length > 0 || reactScanMetrics.slowComponents.length > 0) {
@@ -1973,6 +2067,191 @@ export async function getMcpCapabilities({
1973
2067
  }
1974
2068
  }
1975
2069
 
2070
+ /**
2071
+ * Detect jank/layout shifts by comparing screenshots from ScreencastManager
2072
+ * Returns array of jank detections with timing and visual impact data
2073
+ */
2074
+ async function detectJankFromScreenshots(_projectName?: string): Promise<{
2075
+ detections: Array<{
2076
+ timestamp: string
2077
+ timeSinceStart: number
2078
+ visualDiff: number
2079
+ severity: "low" | "medium" | "high"
2080
+ element?: string
2081
+ clsScore?: number
2082
+ uxImpact?: string
2083
+ }>
2084
+ sessionId: string
2085
+ totalFrames: number
2086
+ screenshotDir: string
2087
+ realCLS?: { score: number; grade: string }
2088
+ }> {
2089
+ const screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots")
2090
+
2091
+ if (!existsSync(screenshotDir)) {
2092
+ return { detections: [], sessionId: "", totalFrames: 0, screenshotDir }
2093
+ }
2094
+
2095
+ // Find the most recent screencast session (files like 2025-10-06T01-54-45Z-jank-*.png)
2096
+ const files = readdirSync(screenshotDir)
2097
+ .filter((f) => f.includes("-jank-") && f.endsWith(".png"))
2098
+ .sort()
2099
+ .reverse()
2100
+
2101
+ if (files.length === 0) {
2102
+ return { detections: [], sessionId: "", totalFrames: 0, screenshotDir }
2103
+ }
2104
+
2105
+ // Get the most recent session ID (timestamp prefix)
2106
+ const latestSessionId = files[0].split("-jank-")[0]
2107
+ const sessionFiles = files
2108
+ .filter((f) => f.startsWith(latestSessionId))
2109
+ .sort((a, b) => {
2110
+ // Extract timestamp (e.g., "28ms" from "2025-10-06T01-54-45Z-jank-28ms.png")
2111
+ const aTime = parseInt(a.match(/-(\d+)ms\.png$/)?.[1] || "0", 10)
2112
+ const bTime = parseInt(b.match(/-(\d+)ms\.png$/)?.[1] || "0", 10)
2113
+ return aTime - bTime
2114
+ })
2115
+
2116
+ if (sessionFiles.length < 2) {
2117
+ return { detections: [], sessionId: latestSessionId, totalFrames: sessionFiles.length, screenshotDir }
2118
+ }
2119
+
2120
+ // Try to read real CLS data from metadata
2121
+ const metadataPath = join(screenshotDir, `${latestSessionId}-metadata.json`)
2122
+ let realCLSData:
2123
+ | {
2124
+ score: number
2125
+ grade: string
2126
+ shifts: Array<{ score: number; timestamp: number; sources?: Array<{ node?: string }> }>
2127
+ }
2128
+ | undefined
2129
+
2130
+ if (existsSync(metadataPath)) {
2131
+ try {
2132
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"))
2133
+ if (metadata.layoutShifts && metadata.layoutShifts.length > 0) {
2134
+ realCLSData = {
2135
+ score: metadata.totalCLS || 0,
2136
+ grade: metadata.clsGrade || "unknown",
2137
+ shifts: metadata.layoutShifts
2138
+ }
2139
+ }
2140
+ } catch {
2141
+ // Ignore metadata read errors
2142
+ }
2143
+ }
2144
+
2145
+ const jankDetections: Array<{
2146
+ timestamp: string
2147
+ timeSinceStart: number
2148
+ visualDiff: number
2149
+ severity: "low" | "medium" | "high"
2150
+ element?: string
2151
+ clsScore?: number
2152
+ uxImpact?: string
2153
+ }> = []
2154
+
2155
+ // If we have real CLS data, use it to flag visual severity
2156
+ if (realCLSData && realCLSData.shifts.length > 0) {
2157
+ realCLSData.shifts.forEach((shift) => {
2158
+ const element = shift.sources?.[0]?.node || "unknown"
2159
+ const isCriticalElement = ["NAV", "HEADER", "BUTTON", "A"].includes(element.toUpperCase())
2160
+ const isDuringLoad = shift.timestamp < 1000 // First second
2161
+
2162
+ // Make element names more descriptive
2163
+ const elementDescriptions: Record<string, string> = {
2164
+ NAV: "Navigation header (<nav>)",
2165
+ HEADER: "Page header (<header>)",
2166
+ BUTTON: "Button (<button>)",
2167
+ A: "Link (<a>)"
2168
+ }
2169
+ const elementDisplay = elementDescriptions[element.toUpperCase()] || element
2170
+
2171
+ // UX impact assessment (not just CLS score!)
2172
+ let severity: "low" | "medium" | "high" = "low"
2173
+ let uxImpact = "Minor visual adjustment"
2174
+
2175
+ if (isCriticalElement && isDuringLoad) {
2176
+ severity = "high"
2177
+ uxImpact = `🚨 CRITICAL: ${elementDisplay} shifted during initial load - highly visible and disruptive to user interaction`
2178
+ } else if (isCriticalElement) {
2179
+ severity = "medium"
2180
+ uxImpact = `⚠️ ${elementDisplay} shifted - affects navigation/interaction`
2181
+ } else if (isDuringLoad) {
2182
+ severity = "medium"
2183
+ uxImpact = "Shift during page load - may cause mis-clicks"
2184
+ }
2185
+
2186
+ jankDetections.push({
2187
+ timestamp: `${shift.timestamp.toFixed(0)}ms`,
2188
+ timeSinceStart: Math.round(shift.timestamp),
2189
+ visualDiff: shift.score * 100, // Convert to percentage-like scale
2190
+ severity,
2191
+ element: elementDisplay,
2192
+ clsScore: shift.score,
2193
+ uxImpact
2194
+ })
2195
+ })
2196
+
2197
+ return {
2198
+ detections: jankDetections,
2199
+ sessionId: latestSessionId,
2200
+ totalFrames: sessionFiles.length,
2201
+ screenshotDir,
2202
+ realCLS: { score: realCLSData.score, grade: realCLSData.grade }
2203
+ }
2204
+ }
2205
+
2206
+ // Fallback to pixel-diff if no real CLS data (old behavior)
2207
+
2208
+ // Compare each frame with the previous frame
2209
+ for (let i = 1; i < sessionFiles.length; i++) {
2210
+ const prevFile = join(screenshotDir, sessionFiles[i - 1])
2211
+ const currFile = join(screenshotDir, sessionFiles[i])
2212
+
2213
+ try {
2214
+ const prevPng = PNG.sync.read(readFileSync(prevFile))
2215
+ const currPng = PNG.sync.read(readFileSync(currFile))
2216
+
2217
+ // Ensure same dimensions
2218
+ if (prevPng.width !== currPng.width || prevPng.height !== currPng.height) {
2219
+ continue
2220
+ }
2221
+
2222
+ const diff = new PNG({ width: prevPng.width, height: prevPng.height })
2223
+ const numDiffPixels = pixelmatch(prevPng.data, currPng.data, diff.data, prevPng.width, prevPng.height, {
2224
+ threshold: 0.1
2225
+ })
2226
+
2227
+ const totalPixels = prevPng.width * prevPng.height
2228
+ const diffPercentage = (numDiffPixels / totalPixels) * 100
2229
+
2230
+ // Consider it jank if more than 1% of pixels changed (layout shift threshold)
2231
+ if (diffPercentage > 1) {
2232
+ const timeMatch = sessionFiles[i].match(/-(\d+)ms\.png$/)
2233
+ const timeSinceStart = timeMatch ? parseInt(timeMatch[1], 10) : 0
2234
+
2235
+ jankDetections.push({
2236
+ timestamp: latestSessionId,
2237
+ timeSinceStart,
2238
+ visualDiff: diffPercentage,
2239
+ severity: diffPercentage > 10 ? "high" : diffPercentage > 5 ? "medium" : "low"
2240
+ })
2241
+ }
2242
+ } catch {
2243
+ // Skip frames that can't be compared
2244
+ }
2245
+ }
2246
+
2247
+ return {
2248
+ detections: jankDetections,
2249
+ sessionId: latestSessionId,
2250
+ totalFrames: sessionFiles.length,
2251
+ screenshotDir
2252
+ }
2253
+ }
2254
+
1976
2255
  /**
1977
2256
  * Log MCP-related events to the project-specific D3K log file (NOT main project log)
1978
2257
  * This prevents Claude from seeing dev3000's orchestration logs as application errors
@@ -0,0 +1,237 @@
1
+ "use client"
2
+
3
+ import { use, useEffect, useState } from "react"
4
+
5
+ export default function VideoPlayer({ params }: { params: Promise<{ session: string }> }) {
6
+ const { session } = use(params)
7
+ const [frames, setFrames] = useState<string[]>([])
8
+ const [currentFrame, setCurrentFrame] = useState(0)
9
+ const [isPlaying, setIsPlaying] = useState(false)
10
+ const [fps, setFps] = useState(10)
11
+ const [screenshotDimensions, setScreenshotDimensions] = useState<{ width: number; height: number }>({
12
+ width: 1920,
13
+ height: 1080
14
+ })
15
+ const [imgRef, setImgRef] = useState<HTMLImageElement | null>(null)
16
+ const [clsMarkers, setClsMarkers] = useState<
17
+ Array<{
18
+ timestamp: number
19
+ boundingBox: { x: number; y: number; width: number; height: number } | null
20
+ clsScore?: number
21
+ element?: string
22
+ }>
23
+ >([])
24
+
25
+ useEffect(() => {
26
+ // Parse session timestamp to find matching screenshots
27
+ // Format: 2025-10-05T23-57-XX-jank-XXms.png
28
+ fetch(`/api/screenshots/list?pattern=${session}`)
29
+ .then((r) => r.json())
30
+ .then((data) => {
31
+ const jankFrames = data.files
32
+ .filter((f: string) => f.includes("jank-") && f.includes(session))
33
+ .sort((a: string, b: string) => {
34
+ const aMs = parseInt(a.match(/jank-(\d+)ms/)?.[1] || "0", 10)
35
+ const bMs = parseInt(b.match(/jank-(\d+)ms/)?.[1] || "0", 10)
36
+ return aMs - bMs
37
+ })
38
+ setFrames(jankFrames)
39
+
40
+ // Load first frame to get dimensions
41
+ if (jankFrames.length > 0) {
42
+ const img = new Image()
43
+ img.onload = () => {
44
+ setScreenshotDimensions({ width: img.naturalWidth, height: img.naturalHeight })
45
+ }
46
+ img.src = `/api/screenshots/${jankFrames[0]}`
47
+ }
48
+ })
49
+
50
+ // Fetch CLS markers from jank detection API
51
+ fetch(`/api/jank/${session}`)
52
+ .then((r) => r.json())
53
+ .then((data) => {
54
+ setClsMarkers(data.clsMarkers || [])
55
+ })
56
+ .catch(() => {
57
+ setClsMarkers([])
58
+ })
59
+ }, [session])
60
+
61
+ useEffect(() => {
62
+ if (!isPlaying || frames.length === 0) return
63
+
64
+ const interval = setInterval(() => {
65
+ setCurrentFrame((prev) => {
66
+ if (prev >= frames.length - 1) {
67
+ setIsPlaying(false)
68
+ return prev
69
+ }
70
+ return prev + 1
71
+ })
72
+ }, 1000 / fps)
73
+
74
+ return () => clearInterval(interval)
75
+ }, [isPlaying, frames.length, fps])
76
+
77
+ // Force re-render on window resize to recalculate bounding box position
78
+ const [, forceUpdate] = useState(0)
79
+ useEffect(() => {
80
+ const handleResize = () => forceUpdate((n) => n + 1)
81
+ window.addEventListener("resize", handleResize)
82
+ return () => window.removeEventListener("resize", handleResize)
83
+ }, [])
84
+
85
+ if (frames.length === 0) {
86
+ return <div className="p-8">Loading frames...</div>
87
+ }
88
+
89
+ const currentFrameFile = frames[currentFrame]
90
+ const frameMs = currentFrameFile?.match(/jank-(\d+)ms/)?.[1] || "0"
91
+ const currentFrameMs = parseInt(frameMs, 10)
92
+ const clsAtFrame = clsMarkers.find((marker) => Math.abs(marker.timestamp - currentFrameMs) < 100)
93
+
94
+ // Calculate the rendered image dimensions and position (accounting for object-contain)
95
+ let imgStyle: React.CSSProperties = {}
96
+ if (imgRef && clsAtFrame?.boundingBox) {
97
+ const containerRect = imgRef.parentElement?.getBoundingClientRect()
98
+ const imgNaturalWidth = screenshotDimensions.width
99
+ const imgNaturalHeight = screenshotDimensions.height
100
+
101
+ if (containerRect) {
102
+ // Calculate how the image is scaled by object-contain
103
+ const containerAspect = containerRect.width / containerRect.height
104
+ const imageAspect = imgNaturalWidth / imgNaturalHeight
105
+
106
+ let renderedWidth: number, renderedHeight: number, offsetX: number, offsetY: number
107
+
108
+ if (containerAspect > imageAspect) {
109
+ // Container is wider - image is constrained by height
110
+ renderedHeight = containerRect.height
111
+ renderedWidth = renderedHeight * imageAspect
112
+ offsetX = (containerRect.width - renderedWidth) / 2
113
+ offsetY = 0
114
+ } else {
115
+ // Container is taller - image is constrained by width
116
+ renderedWidth = containerRect.width
117
+ renderedHeight = renderedWidth / imageAspect
118
+ offsetX = 0
119
+ offsetY = (containerRect.height - renderedHeight) / 2
120
+ }
121
+
122
+ // Calculate bounding box position relative to rendered image
123
+ const scale = renderedWidth / imgNaturalWidth
124
+ imgStyle = {
125
+ left: `${offsetX + clsAtFrame.boundingBox.x * scale}px`,
126
+ top: `${offsetY + clsAtFrame.boundingBox.y * scale}px`,
127
+ width: `${clsAtFrame.boundingBox.width * scale}px`,
128
+ height: `${clsAtFrame.boundingBox.height * scale}px`
129
+ }
130
+ }
131
+ }
132
+
133
+ return (
134
+ <div className="flex flex-col h-screen bg-black">
135
+ <div className="flex-1 flex items-center justify-center relative overflow-hidden">
136
+ {/* eslint-disable-next-line @next/next/no-img-element */}
137
+ <img
138
+ ref={setImgRef}
139
+ src={`/api/screenshots/${currentFrameFile}`}
140
+ alt={`Frame ${currentFrame}`}
141
+ className="w-full h-full object-contain"
142
+ />
143
+ {clsAtFrame?.boundingBox && imgRef && (
144
+ <div className="absolute border-2 border-red-500 pointer-events-none" style={imgStyle} />
145
+ )}
146
+ </div>
147
+
148
+ <div className="bg-gray-900 text-white p-4 space-y-4">
149
+ <div className="flex items-center gap-4">
150
+ <button
151
+ type="button"
152
+ onClick={() => setIsPlaying(!isPlaying)}
153
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded"
154
+ >
155
+ {isPlaying ? "Pause" : "Play"}
156
+ </button>
157
+
158
+ <button
159
+ type="button"
160
+ onClick={() => setCurrentFrame(Math.max(0, currentFrame - 1))}
161
+ disabled={currentFrame === 0}
162
+ className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded disabled:opacity-50"
163
+ >
164
+ Previous
165
+ </button>
166
+
167
+ <button
168
+ type="button"
169
+ onClick={() => setCurrentFrame(Math.min(frames.length - 1, currentFrame + 1))}
170
+ disabled={currentFrame === frames.length - 1}
171
+ className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded disabled:opacity-50"
172
+ >
173
+ Next
174
+ </button>
175
+
176
+ <span className="flex-1 text-center">
177
+ Frame {currentFrame + 1} / {frames.length} ({frameMs}ms)
178
+ </span>
179
+
180
+ <label className="flex items-center gap-2">
181
+ Speed:
182
+ <input
183
+ type="range"
184
+ min="1"
185
+ max="30"
186
+ value={fps}
187
+ onChange={(e) => setFps(Number(e.target.value))}
188
+ className="w-32"
189
+ />
190
+ <span className="w-12">{fps} fps</span>
191
+ </label>
192
+ </div>
193
+
194
+ <div className="relative w-full">
195
+ <input
196
+ type="range"
197
+ min="0"
198
+ max={frames.length - 1}
199
+ value={currentFrame}
200
+ onChange={(e) => setCurrentFrame(Number(e.target.value))}
201
+ className="w-full"
202
+ />
203
+ {/* CLS markers on timeline */}
204
+ {clsMarkers.map((marker) => {
205
+ const frameIndex = frames.findIndex((f) => {
206
+ const frameMs = parseInt(f.match(/jank-(\d+)ms/)?.[1] || "0", 10)
207
+ return frameMs >= marker.timestamp
208
+ })
209
+ if (frameIndex === -1) return null
210
+ const position = (frameIndex / (frames.length - 1)) * 100
211
+ return (
212
+ <div
213
+ key={marker.timestamp}
214
+ className="absolute top-0 h-full pointer-events-none"
215
+ style={{ left: `${position}%` }}
216
+ >
217
+ <div className="w-0.5 h-full bg-red-500" />
218
+ <div className="absolute -top-6 left-0 -translate-x-1/2 bg-red-500 text-white text-xs px-2 py-0.5 rounded whitespace-nowrap">
219
+ {marker.clsScore ? (
220
+ <>
221
+ CLS {marker.clsScore.toFixed(4)} @ {marker.timestamp}ms
222
+ {marker.element && (
223
+ <span className="ml-1 opacity-75">&lt;{marker.element.toLowerCase()}&gt;</span>
224
+ )}
225
+ </>
226
+ ) : (
227
+ `CLS ${marker.timestamp}ms`
228
+ )}
229
+ </div>
230
+ </div>
231
+ )
232
+ })}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ )
237
+ }
@@ -13,6 +13,8 @@
13
13
  "dependencies": {
14
14
  "@modelcontextprotocol/sdk": "^1.11.0",
15
15
  "@radix-ui/react-slot": "^1.1.1",
16
+ "@types/pixelmatch": "^5.2.6",
17
+ "@types/pngjs": "^6.0.5",
16
18
  "autoprefixer": "^10.4.20",
17
19
  "class-variance-authority": "^0.7.1",
18
20
  "clsx": "^2.1.1",
@@ -20,6 +22,8 @@
20
22
  "lucide-react": "^0.544.0",
21
23
  "mcp-handler": "^1.0.2",
22
24
  "next": "15.6.0-canary.31",
25
+ "pixelmatch": "^7.1.0",
26
+ "pngjs": "^7.0.0",
23
27
  "react": "19.1.1",
24
28
  "react-dom": "19.1.1",
25
29
  "tailwind-merge": "^2.5.5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev3000",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "description": "AI-powered development tools with browser monitoring and MCP server integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -260,12 +260,15 @@ const TUIApp = ({
260
260
  {initStatus && <Text dimColor>- {initStatus}</Text>}
261
261
  </Box>
262
262
  {!isVeryCompact && (
263
- <>
263
+ <Box flexDirection="column">
264
264
  <Text dimColor>
265
265
  App: localhost:{appPort} | MCP: localhost:{mcpPort}
266
266
  </Text>
267
- <Text dimColor>↑/↓ scroll | Ctrl-C quit</Text>
268
- </>
267
+ <Text dimColor>
268
+ 📸 http://localhost:{mcpPort}/logs
269
+ {projectName ? `?project=${encodeURIComponent(projectName)}` : ""}
270
+ </Text>
271
+ </Box>
269
272
  )}
270
273
  </Box>
271
274
  </Box>
@@ -344,9 +347,9 @@ const TUIApp = ({
344
347
  if (parts) {
345
348
  let [, timestamp, source, type, message] = parts
346
349
 
347
- // Replace specific emoji in common port-in-use error to avoid terminal width issues
348
- if (message?.includes("ERROR: Port") && message.includes("is in use by process")) {
349
- message = message.replace("ERROR: ⚠ Port", "ERROR: [!] Port")
350
+ // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
351
+ if (message && (type === "ERROR" || type === "WARNING")) {
352
+ message = message.replace(/⚠/g, "[!]")
350
353
  }
351
354
 
352
355
  // In very compact mode, simplify the output
@@ -450,7 +453,12 @@ const TUIApp = ({
450
453
 
451
454
  {/* Bottom status line - no border, just text */}
452
455
  <Box paddingX={1} justifyContent="space-between">
453
- <Text color="#A18CE5">⏵⏵ {logFile.replace(process.env.HOME || "", "~")}</Text>
456
+ <Text color="#A18CE5">
457
+ ⏵⏵{" "}
458
+ {isVeryCompact
459
+ ? logFile.split("/").slice(-2, -1)[0] || "logs" // Just show directory name
460
+ : logFile.replace(process.env.HOME || "", "~")}
461
+ </Text>
454
462
  <Text color="#A18CE5">{ctrlCMessage}</Text>
455
463
  </Box>
456
464
  </Box>