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.
Files changed (34) hide show
  1. package/dist/dev-environment.d.ts.map +1 -1
  2. package/dist/dev-environment.js +8 -7
  3. package/dist/dev-environment.js.map +1 -1
  4. package/dist/screencast-manager.d.ts +1 -0
  5. package/dist/screencast-manager.d.ts.map +1 -1
  6. package/dist/screencast-manager.js +29 -4
  7. package/dist/screencast-manager.js.map +1 -1
  8. package/dist/src/tui-interface-impl.tsx +22 -13
  9. package/dist/tui-interface-impl.d.ts.map +1 -1
  10. package/dist/tui-interface-impl.js +19 -11
  11. package/dist/tui-interface-impl.js.map +1 -1
  12. package/mcp-server/.next/BUILD_ID +1 -1
  13. package/mcp-server/.next/build-manifest.json +2 -2
  14. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  15. package/mcp-server/.next/prerender-manifest.json +3 -3
  16. package/mcp-server/.next/server/app/_global-error.html +2 -2
  17. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  18. package/mcp-server/.next/server/app/_not-found.html +1 -1
  19. package/mcp-server/.next/server/app/_not-found.rsc +1 -1
  20. package/mcp-server/.next/server/app/index.html +1 -1
  21. package/mcp-server/.next/server/app/index.rsc +1 -1
  22. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js +50 -12
  23. package/mcp-server/.next/server/chunks/[root-of-the-server]__ae49815f._.js.map +1 -1
  24. package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js +1 -1
  25. package/mcp-server/.next/server/chunks/[root-of-the-server]__dc0b0022._.js.map +1 -1
  26. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  27. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  28. package/mcp-server/app/mcp/route.ts +13 -1
  29. package/mcp-server/app/mcp/tools.ts +366 -26
  30. package/package.json +1 -1
  31. package/src/tui-interface-impl.tsx +22 -13
  32. /package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_buildManifest.js +0 -0
  33. /package/mcp-server/.next/static/{XC8IdMzOP-R6sszO2tUlu → _qXVPjMKmdbmLNrnd_3zK}/_clientMiddlewareManifest.json +0 -0
  34. /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\": \"coTPdU0svpRNodp0Rw8CHLLSd/s45e/rD4kkbrVURd0=\"\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": "coTPdU0svpRNodp0Rw8CHLLSd/s45e/rD4kkbrVURd0="
4
+ "encryptionKey": "lZgibe2ziYqyEAkZ7RFH62f938aB/wxVlceTcZMnbR8="
5
5
  }
@@ -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
- 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}`)
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(prevPng: PNG, currPng: PNG): { isLayoutShift: boolean; shiftScore: number } {
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
- // Divide screen into horizontal bands (top, middle, bottom)
2127
- const bandHeight = Math.floor(height / 8)
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 in each band
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
- bands[bandIndex]++
2150
+ rowChangeCounts[y]++
2142
2151
  }
2143
2152
  }
2144
2153
  }
2145
2154
 
2146
- // 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
+
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
- // If top 25% of screen has >5% pixel change but bottom has <2% change = layout shift
2156
- const isLayoutShift = topBandChange > 5 && bottomBandChange < 2 && topBandChange > bottomBandChange * 2
2157
-
2158
- // 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
2159
2248
  const totalChanged = bands.reduce((sum, count) => sum + count, 0)
2160
2249
  const totalPixels = width * height
2161
- const shiftScore = (totalChanged / totalPixels) * 0.1 // Rough CLS equivalent
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<{ 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
+ }>
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
- // 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
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 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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev3000",
3
- "version": "0.0.90",
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>