dev3000 0.0.90 → 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.
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +8 -7
- package/dist/dev-environment.js.map +1 -1
- package/dist/screencast-manager.d.ts +1 -0
- package/dist/screencast-manager.d.ts.map +1 -1
- package/dist/screencast-manager.js +29 -4
- package/dist/screencast-manager.js.map +1 -1
- package/dist/src/tui-interface-impl.tsx +22 -13
- package/dist/tui-interface-impl.d.ts.map +1 -1
- package/dist/tui-interface-impl.js +19 -11
- package/dist/tui-interface-impl.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/build-manifest.json +2 -2
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error.html +2 -2
- package/mcp-server/.next/server/app/_global-error.rsc +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +1 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js +50 -12
- package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js.map +1 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js +1 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js.map +1 -1
- package/mcp-server/.next/server/server-reference-manifest.js +1 -1
- package/mcp-server/.next/server/server-reference-manifest.json +1 -1
- package/mcp-server/app/mcp/route.ts +13 -1
- package/mcp-server/app/mcp/tools.ts +366 -26
- package/package.json +1 -1
- package/src/tui-interface-impl.tsx +22 -13
- /package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_clientMiddlewareManifest.json +0 -0
- /package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"
|
|
1
|
+
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"lZgibe2ziYqyEAkZ7RFH62f938aB/wxVlceTcZMnbR8=\"\n}"
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path"
|
|
|
4
4
|
import { createMcpHandler } from "mcp-handler"
|
|
5
5
|
import { z } from "zod"
|
|
6
6
|
import { getMCPClientManager } from "./client-manager"
|
|
7
|
-
import { executeBrowserAction, findComponentSource, fixMyApp, TOOL_DESCRIPTIONS } from "./tools"
|
|
7
|
+
import { executeBrowserAction, findComponentSource, fixMyApp, restartDevServer, TOOL_DESCRIPTIONS } from "./tools"
|
|
8
8
|
|
|
9
9
|
// Detect available package runner (npx, pnpm dlx, or fail)
|
|
10
10
|
const getPackageRunner = (): { command: string; args: string[] } | null => {
|
|
@@ -285,6 +285,18 @@ const handler = createMcpHandler(
|
|
|
285
285
|
}
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
+
// Dev server restart tool
|
|
289
|
+
server.tool(
|
|
290
|
+
"restart_dev_server",
|
|
291
|
+
TOOL_DESCRIPTIONS.restart_dev_server,
|
|
292
|
+
{
|
|
293
|
+
projectName: z.string().optional().describe("Project name (if multiple dev3000 instances are running)")
|
|
294
|
+
},
|
|
295
|
+
async (params) => {
|
|
296
|
+
return restartDevServer(params)
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
288
300
|
// Tool that returns monitoring code for Claude to execute
|
|
289
301
|
// TODO: Commenting out for now - need to figure out the right approach for proactive monitoring
|
|
290
302
|
/*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from "child_process"
|
|
1
|
+
import { exec, spawn } from "child_process"
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "fs"
|
|
3
3
|
import { homedir, tmpdir } from "os"
|
|
4
4
|
import { join } from "path"
|
|
@@ -21,7 +21,10 @@ export const TOOL_DESCRIPTIONS = {
|
|
|
21
21
|
"🔍 **VISUAL DIFF ANALYZER** - Analyzes two screenshots to identify and describe visual differences. Returns detailed instructions for Claude to load and compare the images, focusing on what changed that could cause layout shifts.\n\n🎯 **WHAT IT PROVIDES:**\n• Direct instructions to load both images via Read tool\n• Context about what to look for\n• Guidance on identifying layout shift causes\n• Structured format for easy analysis\n\n💡 **PERFECT FOR:** Understanding what visual changes occurred between before/after frames in CLS detection, identifying elements that appeared/moved/resized.",
|
|
22
22
|
|
|
23
23
|
find_component_source:
|
|
24
|
-
"🔍 **COMPONENT SOURCE FINDER** - Maps DOM elements to their source code by extracting the React component function and finding unique patterns to search for.\n\n🎯 **HOW IT WORKS:**\n• Inspects the element via Chrome DevTools Protocol\n• Extracts the React component function source using .toString()\n• Identifies unique code patterns (specific JSX, classNames, imports)\n• Returns targeted grep patterns to find the exact source file\n\n💡 **PERFECT FOR:** Finding which file contains the code for a specific element, especially useful for CLS debugging when you need to fix layout shifts in specific components."
|
|
24
|
+
"🔍 **COMPONENT SOURCE FINDER** - Maps DOM elements to their source code by extracting the React component function and finding unique patterns to search for.\n\n🎯 **HOW IT WORKS:**\n• Inspects the element via Chrome DevTools Protocol\n• Extracts the React component function source using .toString()\n• Identifies unique code patterns (specific JSX, classNames, imports)\n• Returns targeted grep patterns to find the exact source file\n\n💡 **PERFECT FOR:** Finding which file contains the code for a specific element, especially useful for CLS debugging when you need to fix layout shifts in specific components.",
|
|
25
|
+
|
|
26
|
+
restart_dev_server:
|
|
27
|
+
"🔄 **DEV SERVER RESTART** - Safely restarts the development server while preserving dev3000's monitoring, logs, and browser connection.\n\n🎯 **SMART RESTART LOGIC:**\n• First tries nextjs-dev MCP restart (if available and user has Next.js canary)\n• Falls back to dev3000's own restart mechanism:\n - Kills the old server process on the app port\n - Waits for clean shutdown\n - Spawns a new server with the same command that was originally used\n - Keeps dev3000's MCP server, browser monitoring, and screenshot capture running\n• All logging continues seamlessly - no data loss\n• Browser monitoring stays connected - no need to relaunch Chrome\n\n⚡ **WHEN TO USE:**\n• After modifying next.config.js, middleware, or environment variables\n• When you need a clean restart to clear server state\n• After significant code changes that Next.js HMR can't handle\n• When debugging persistent state or memory issues\n\n⚠️ **CRITICAL - DO NOT:**\n• ❌ NEVER manually run kill commands on the dev server like `pkill -f \"next dev\"` or `lsof -ti :3000 | xargs kill`\n• ❌ NEVER manually start the dev server with `npm run dev`, `pnpm dev`, `next dev`, etc.\n• ✅ ALWAYS use this tool for dev server restarts - it preserves all dev3000 infrastructure\n\n⚠️ **IMPORTANT:**\n• AVOID using this unnecessarily - Next.js HMR handles most changes automatically\n• Only restart when truly needed for config changes or state issues\n• The server will be offline for a few seconds during restart\n• Browser may show connection error briefly while server restarts\n\n💡 **PERFECT FOR:** 'restart the dev server', 'clean restart', 'reload the server' - but only when actually needed, not for regular code changes."
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
// Types
|
|
@@ -666,7 +669,13 @@ export async function fixMyApp({
|
|
|
666
669
|
)
|
|
667
670
|
}
|
|
668
671
|
|
|
669
|
-
|
|
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}`)
|
|
670
679
|
results.push(`🎞️ **Session ID**: ${jankResult.sessionId} (${jankResult.totalFrames} frames)`)
|
|
671
680
|
results.push("")
|
|
672
681
|
|
|
@@ -2119,48 +2128,128 @@ export async function getMcpCapabilities({
|
|
|
2119
2128
|
* - Layout shifts: Elements move to new positions (top region changes while bottom stays same)
|
|
2120
2129
|
* - Content changes: Same regions change in-place (image loads with pixels appearing)
|
|
2121
2130
|
*/
|
|
2122
|
-
function detectLayoutShiftVsContentChange(
|
|
2131
|
+
function detectLayoutShiftVsContentChange(
|
|
2132
|
+
prevPng: PNG,
|
|
2133
|
+
currPng: PNG
|
|
2134
|
+
): { isLayoutShift: boolean; shiftScore: number; isOverlayNoise: boolean } {
|
|
2123
2135
|
const width = prevPng.width
|
|
2124
2136
|
const height = prevPng.height
|
|
2125
2137
|
|
|
2126
|
-
//
|
|
2127
|
-
const
|
|
2128
|
-
const bands = Array(8).fill(0)
|
|
2138
|
+
// Track changes at row-level for detecting correlated shifts
|
|
2139
|
+
const rowChangeCounts = new Array(height).fill(0)
|
|
2129
2140
|
|
|
2130
|
-
// Count changed pixels
|
|
2141
|
+
// Count changed pixels per row (for correlation analysis)
|
|
2131
2142
|
for (let y = 0; y < height; y++) {
|
|
2132
|
-
const bandIndex = Math.min(Math.floor(y / bandHeight), 7)
|
|
2133
2143
|
for (let x = 0; x < width; x++) {
|
|
2134
2144
|
const idx = (width * y + x) << 2
|
|
2135
2145
|
const rDiff = Math.abs(prevPng.data[idx] - currPng.data[idx])
|
|
2136
2146
|
const gDiff = Math.abs(prevPng.data[idx + 1] - currPng.data[idx + 1])
|
|
2137
2147
|
const bDiff = Math.abs(prevPng.data[idx + 2] - currPng.data[idx + 2])
|
|
2138
2148
|
|
|
2139
|
-
// If any channel differs significantly, count as changed pixel
|
|
2140
2149
|
if (rDiff > 30 || gDiff > 30 || bDiff > 30) {
|
|
2141
|
-
|
|
2150
|
+
rowChangeCounts[y]++
|
|
2142
2151
|
}
|
|
2143
2152
|
}
|
|
2144
2153
|
}
|
|
2145
2154
|
|
|
2146
|
-
// Calculate percentage of pixels changed
|
|
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
|
+
|
|
2147
2213
|
const pixelsPerBand = width * bandHeight
|
|
2148
2214
|
const bandPercentages = bands.map((count) => (count / pixelsPerBand) * 100)
|
|
2149
|
-
|
|
2150
|
-
// Layout shift pattern: High change in top bands (nav/header area), low change in bottom
|
|
2151
|
-
// Content change pattern: Evenly distributed or contained to specific regions
|
|
2152
2215
|
const topBandChange = (bandPercentages[0] + bandPercentages[1]) / 2
|
|
2153
2216
|
const bottomBandChange = (bandPercentages[6] + bandPercentages[7]) / 2
|
|
2154
2217
|
|
|
2155
|
-
//
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
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
|
|
2159
2248
|
const totalChanged = bands.reduce((sum, count) => sum + count, 0)
|
|
2160
2249
|
const totalPixels = width * height
|
|
2161
|
-
const shiftScore = (totalChanged / totalPixels) * 0.1
|
|
2250
|
+
const shiftScore = (totalChanged / totalPixels) * 0.1
|
|
2162
2251
|
|
|
2163
|
-
return { isLayoutShift, shiftScore }
|
|
2252
|
+
return { isLayoutShift, shiftScore, isOverlayNoise }
|
|
2164
2253
|
}
|
|
2165
2254
|
|
|
2166
2255
|
/**
|
|
@@ -2183,6 +2272,7 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
|
|
|
2183
2272
|
totalFrames: number
|
|
2184
2273
|
screenshotDir: string
|
|
2185
2274
|
realCLS?: { score: number; grade: string }
|
|
2275
|
+
captureTrigger?: "navigation" | "load"
|
|
2186
2276
|
}> {
|
|
2187
2277
|
const screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots")
|
|
2188
2278
|
|
|
@@ -2221,13 +2311,20 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
|
|
|
2221
2311
|
| {
|
|
2222
2312
|
score: number
|
|
2223
2313
|
grade: string
|
|
2224
|
-
shifts: Array<{
|
|
2314
|
+
shifts: Array<{
|
|
2315
|
+
score: number
|
|
2316
|
+
timestamp: number
|
|
2317
|
+
sources?: Array<{ node?: string; position?: string | null }>
|
|
2318
|
+
}>
|
|
2225
2319
|
}
|
|
2226
2320
|
| undefined
|
|
2321
|
+
let captureTrigger: "navigation" | "load" | undefined
|
|
2227
2322
|
|
|
2228
2323
|
if (existsSync(metadataPath)) {
|
|
2229
2324
|
try {
|
|
2230
2325
|
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"))
|
|
2326
|
+
// Capture the trigger type for use in output messages
|
|
2327
|
+
captureTrigger = metadata.captureTrigger
|
|
2231
2328
|
// Set realCLSData even if there are zero shifts - this tells us Chrome ran and found nothing
|
|
2232
2329
|
if (metadata.layoutShifts !== undefined) {
|
|
2233
2330
|
realCLSData = {
|
|
@@ -2326,15 +2423,27 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
|
|
|
2326
2423
|
continue
|
|
2327
2424
|
}
|
|
2328
2425
|
|
|
2329
|
-
// Detect if this is a layout shift vs content change
|
|
2426
|
+
// Detect if this is a layout shift vs content change vs overlay noise
|
|
2330
2427
|
const shiftAnalysis = detectLayoutShiftVsContentChange(prevPng, currPng)
|
|
2331
2428
|
|
|
2332
|
-
//
|
|
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
|
|
2333
2438
|
if (shiftAnalysis.isLayoutShift) {
|
|
2334
2439
|
foundHydrationShift = true
|
|
2335
2440
|
const timeMatch = earlyFrames[i].match(/-(\d+)ms\.png$/)
|
|
2336
2441
|
const timeSinceStart = timeMatch ? parseInt(timeMatch[1], 10) : 0
|
|
2337
2442
|
|
|
2443
|
+
logToDevFile(
|
|
2444
|
+
`Pixel Diff Hydration: Detected true layout shift at ${timeSinceStart}ms (score: ${shiftAnalysis.shiftScore.toFixed(4)})`
|
|
2445
|
+
)
|
|
2446
|
+
|
|
2338
2447
|
const mcpPort = process.env.MCP_PORT || "3684"
|
|
2339
2448
|
jankDetections.push({
|
|
2340
2449
|
timestamp: `${timeSinceStart}ms`,
|
|
@@ -2375,9 +2484,29 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
|
|
|
2375
2484
|
}
|
|
2376
2485
|
|
|
2377
2486
|
// Process actual layout shifts detected by Chrome
|
|
2378
|
-
// Trust Chrome's Layout Instability API
|
|
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
|
|
2379
2489
|
realCLSData.shifts.forEach((shift) => {
|
|
2380
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
|
+
|
|
2381
2510
|
const isCriticalElement = ["NAV", "HEADER", "BUTTON", "A"].includes(element.toUpperCase())
|
|
2382
2511
|
const isDuringLoad = shift.timestamp < 1000 // First second
|
|
2383
2512
|
|
|
@@ -2476,7 +2605,8 @@ async function detectJankFromScreenshots(_projectName?: string): Promise<{
|
|
|
2476
2605
|
detections: jankDetections,
|
|
2477
2606
|
sessionId: latestSessionId,
|
|
2478
2607
|
totalFrames: sessionFiles.length,
|
|
2479
|
-
screenshotDir
|
|
2608
|
+
screenshotDir,
|
|
2609
|
+
captureTrigger
|
|
2480
2610
|
}
|
|
2481
2611
|
}
|
|
2482
2612
|
|
|
@@ -3266,3 +3396,213 @@ export async function findComponentSource(params: {
|
|
|
3266
3396
|
}
|
|
3267
3397
|
}
|
|
3268
3398
|
}
|
|
3399
|
+
|
|
3400
|
+
/**
|
|
3401
|
+
* Restart the development server while preserving logs and monitoring
|
|
3402
|
+
*/
|
|
3403
|
+
export async function restartDevServer(params: {
|
|
3404
|
+
projectName?: string
|
|
3405
|
+
}): Promise<{ content: Array<{ type: "text"; text: string }> }> {
|
|
3406
|
+
const { projectName } = params
|
|
3407
|
+
|
|
3408
|
+
try {
|
|
3409
|
+
// Find active session
|
|
3410
|
+
const sessions = findActiveSessions()
|
|
3411
|
+
if (sessions.length === 0) {
|
|
3412
|
+
return {
|
|
3413
|
+
content: [
|
|
3414
|
+
{
|
|
3415
|
+
type: "text",
|
|
3416
|
+
text: "❌ **NO ACTIVE SESSIONS**\n\nNo active dev3000 sessions found. Make sure your app is running with dev3000."
|
|
3417
|
+
}
|
|
3418
|
+
]
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// Use specified project or first available session
|
|
3423
|
+
let targetSession = sessions[0]
|
|
3424
|
+
if (projectName) {
|
|
3425
|
+
const found = sessions.find((s) => s.projectName === projectName)
|
|
3426
|
+
if (found) {
|
|
3427
|
+
targetSession = found
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
const sessionData = JSON.parse(readFileSync(targetSession.sessionFile, "utf-8"))
|
|
3432
|
+
const appPort = sessionData.appPort
|
|
3433
|
+
const serverCommand = sessionData.serverCommand
|
|
3434
|
+
const cwd = sessionData.cwd
|
|
3435
|
+
|
|
3436
|
+
if (!appPort) {
|
|
3437
|
+
return {
|
|
3438
|
+
content: [
|
|
3439
|
+
{
|
|
3440
|
+
type: "text",
|
|
3441
|
+
text: "❌ **NO APP PORT FOUND**\n\nSession file doesn't contain app port information."
|
|
3442
|
+
}
|
|
3443
|
+
]
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
if (!serverCommand) {
|
|
3448
|
+
return {
|
|
3449
|
+
content: [
|
|
3450
|
+
{
|
|
3451
|
+
type: "text",
|
|
3452
|
+
text: "❌ **NO SERVER COMMAND FOUND**\n\nSession file doesn't contain the original server command. This session may have been created with an older version of dev3000."
|
|
3453
|
+
}
|
|
3454
|
+
]
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
logToDevFile(
|
|
3459
|
+
`Restart Dev Server: Starting restart for project [${targetSession.projectName}] on port ${appPort} with command [${serverCommand}]`
|
|
3460
|
+
)
|
|
3461
|
+
|
|
3462
|
+
// Check if nextjs-dev MCP is available
|
|
3463
|
+
const availableMcps = await discoverAvailableMcps(targetSession.projectName)
|
|
3464
|
+
const hasNextjsDev = availableMcps.includes("nextjs-dev")
|
|
3465
|
+
|
|
3466
|
+
logToDevFile(`Restart Dev Server: Has nextjs-dev MCP: ${hasNextjsDev}`)
|
|
3467
|
+
|
|
3468
|
+
// Try nextjs-dev MCP first if available
|
|
3469
|
+
if (hasNextjsDev) {
|
|
3470
|
+
try {
|
|
3471
|
+
logToDevFile("Restart Dev Server: Attempting to use nextjs-dev MCP restart")
|
|
3472
|
+
|
|
3473
|
+
// Check if nextjs-dev has restart capability
|
|
3474
|
+
const capabilities = await getMcpCapabilities({ mcpName: "nextjs-dev" })
|
|
3475
|
+
const capabilitiesText =
|
|
3476
|
+
capabilities.content[0] && "text" in capabilities.content[0] ? capabilities.content[0].text : ""
|
|
3477
|
+
|
|
3478
|
+
if (capabilitiesText.includes("restart") || capabilitiesText.includes("reload")) {
|
|
3479
|
+
logToDevFile("Restart Dev Server: nextjs-dev MCP has restart capability, delegating")
|
|
3480
|
+
|
|
3481
|
+
return {
|
|
3482
|
+
content: [
|
|
3483
|
+
{
|
|
3484
|
+
type: "text",
|
|
3485
|
+
text: "✅ **DELEGATING TO NEXTJS-DEV MCP**\n\nThe nextjs-dev MCP has restart capabilities. Please use the nextjs-dev MCP restart tool directly for better integration with Next.js."
|
|
3486
|
+
}
|
|
3487
|
+
]
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
logToDevFile("Restart Dev Server: nextjs-dev MCP doesn't have restart capability, falling back")
|
|
3492
|
+
} catch (error) {
|
|
3493
|
+
logToDevFile(`Restart Dev Server: Failed to check nextjs-dev capabilities - ${error}`)
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// Fallback: Use dev3000's own restart mechanism
|
|
3498
|
+
logToDevFile("Restart Dev Server: Using dev3000 restart mechanism")
|
|
3499
|
+
|
|
3500
|
+
// Kill processes on the app port
|
|
3501
|
+
const killCommand = `lsof -ti :${appPort} | xargs kill 2>/dev/null || true`
|
|
3502
|
+
logToDevFile(`Restart Dev Server: Executing kill command: ${killCommand}`)
|
|
3503
|
+
|
|
3504
|
+
try {
|
|
3505
|
+
await execAsync(killCommand)
|
|
3506
|
+
logToDevFile("Restart Dev Server: Kill command executed successfully")
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
logToDevFile(`Restart Dev Server: Kill command failed (may be ok) - ${error}`)
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// Wait for clean shutdown
|
|
3512
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
3513
|
+
|
|
3514
|
+
// Check if port is now free
|
|
3515
|
+
const checkCommand = `lsof -ti :${appPort}`
|
|
3516
|
+
let portFree = false
|
|
3517
|
+
try {
|
|
3518
|
+
const { stdout } = await execAsync(checkCommand)
|
|
3519
|
+
portFree = stdout.trim() === ""
|
|
3520
|
+
logToDevFile(`Restart Dev Server: Port check result - free: ${portFree}`)
|
|
3521
|
+
} catch {
|
|
3522
|
+
// Command failed means no process on port (port is free)
|
|
3523
|
+
portFree = true
|
|
3524
|
+
logToDevFile("Restart Dev Server: Port is free (lsof returned no results)")
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
if (!portFree) {
|
|
3528
|
+
return {
|
|
3529
|
+
content: [
|
|
3530
|
+
{
|
|
3531
|
+
type: "text",
|
|
3532
|
+
text: `⚠️ **PORT STILL IN USE**\n\nFailed to free port ${appPort}. There may be a process that couldn't be killed.\n\nTry manually killing the process:\n\`\`\`bash\nlsof -ti :${appPort} | xargs kill -9\n\`\`\``
|
|
3533
|
+
}
|
|
3534
|
+
]
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
logToDevFile("Restart Dev Server: Port is now free, spawning new server process")
|
|
3539
|
+
|
|
3540
|
+
// Spawn new server process
|
|
3541
|
+
try {
|
|
3542
|
+
const serverProcess = spawn(serverCommand, {
|
|
3543
|
+
stdio: "inherit", // Inherit stdio so output goes to dev3000's logs
|
|
3544
|
+
shell: true,
|
|
3545
|
+
detached: true, // Run independently
|
|
3546
|
+
cwd: cwd || process.cwd() // Use original working directory
|
|
3547
|
+
})
|
|
3548
|
+
|
|
3549
|
+
// Unref so this process doesn't keep MCP server alive
|
|
3550
|
+
serverProcess.unref()
|
|
3551
|
+
|
|
3552
|
+
logToDevFile(`Restart Dev Server: Spawned new server process with PID ${serverProcess.pid}`)
|
|
3553
|
+
|
|
3554
|
+
// Wait a moment for server to start
|
|
3555
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
3556
|
+
|
|
3557
|
+
// Check if server is actually running on the port
|
|
3558
|
+
try {
|
|
3559
|
+
const { stdout: checkResult } = await execAsync(`lsof -ti :${appPort}`)
|
|
3560
|
+
const isRunning = checkResult.trim() !== ""
|
|
3561
|
+
|
|
3562
|
+
if (isRunning) {
|
|
3563
|
+
logToDevFile("Restart Dev Server: Server successfully restarted and running on port")
|
|
3564
|
+
return {
|
|
3565
|
+
content: [
|
|
3566
|
+
{
|
|
3567
|
+
type: "text",
|
|
3568
|
+
text: `✅ **DEV SERVER RESTARTED**\n\nSuccessfully restarted the development server on port ${appPort}.\n\n🎯 **STATUS:**\n• Old server process: Killed\n• New server process: Running (PID ${serverProcess.pid})\n• Port ${appPort}: Active\n• Browser monitoring: Unchanged\n• Logs: Still being captured\n\n💡 The server has been restarted while keeping dev3000's monitoring, screenshots, and logging intact.`
|
|
3569
|
+
}
|
|
3570
|
+
]
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
logToDevFile("Restart Dev Server: Server process spawned but not yet listening on port (may still be starting)")
|
|
3574
|
+
} catch {
|
|
3575
|
+
logToDevFile("Restart Dev Server: Server process spawned but not yet listening on port (may still be starting)")
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
return {
|
|
3579
|
+
content: [
|
|
3580
|
+
{
|
|
3581
|
+
type: "text",
|
|
3582
|
+
text: `🔄 **DEV SERVER RESTARTING**\n\nStarted a new server process (PID ${serverProcess.pid}).\n\n⏳ **STATUS:**\n• Old server: Killed\n• New server: Starting (may take a few moments)\n• Command: \`${serverCommand}\`\n• Port: ${appPort}\n\nThe server is restarting. Check the dev3000 logs to see when it's ready.`
|
|
3583
|
+
}
|
|
3584
|
+
]
|
|
3585
|
+
}
|
|
3586
|
+
} catch (spawnError) {
|
|
3587
|
+
logToDevFile(`Restart Dev Server: Failed to spawn new server process - ${spawnError}`)
|
|
3588
|
+
return {
|
|
3589
|
+
content: [
|
|
3590
|
+
{
|
|
3591
|
+
type: "text",
|
|
3592
|
+
text: `❌ **RESTART FAILED**\n\nFailed to start new server process.\n\n**Error:** ${spawnError instanceof Error ? spawnError.message : String(spawnError)}\n\n**Command:** \`${serverCommand}\`\n\nThe old server was killed but the new one failed to start. You may need to manually restart dev3000.`
|
|
3593
|
+
}
|
|
3594
|
+
]
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
} catch (error) {
|
|
3598
|
+
logToDevFile(`Restart Dev Server: Error - ${error}`)
|
|
3599
|
+
return {
|
|
3600
|
+
content: [
|
|
3601
|
+
{
|
|
3602
|
+
type: "text",
|
|
3603
|
+
text: `❌ **ERROR**\n\n${error instanceof Error ? error.message : String(error)}`
|
|
3604
|
+
}
|
|
3605
|
+
]
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ({
|
|
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>
|
|
457
|
+
<Text> </Text>
|
|
449
458
|
)}
|
|
450
459
|
<Text>{message}</Text>
|
|
451
460
|
</Text>
|
/package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_ssgManifest.js
RENAMED
|
File without changes
|