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.
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +11 -2
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/dev-environment.d.ts +1 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +29 -3
- package/dist/dev-environment.js.map +1 -1
- package/dist/screencast-manager.d.ts +76 -0
- package/dist/screencast-manager.d.ts.map +1 -0
- package/dist/screencast-manager.js +410 -0
- package/dist/screencast-manager.js.map +1 -0
- package/dist/src/tui-interface-impl.tsx +15 -7
- package/dist/tui-interface-impl.d.ts.map +1 -1
- package/dist/tui-interface-impl.js +7 -5
- package/dist/tui-interface-impl.js.map +1 -1
- package/mcp-server/.next/BUILD_ID +1 -1
- package/mcp-server/.next/app-path-routes-manifest.json +4 -1
- package/mcp-server/.next/build-manifest.json +5 -5
- package/mcp-server/.next/fallback-build-manifest.json +2 -2
- package/mcp-server/.next/prerender-manifest.json +3 -3
- package/mcp-server/.next/routes-manifest.json +16 -0
- package/mcp-server/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/_global-error/page.js +1 -1
- package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
- 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/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/_not-found/page.js +1 -1
- package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/_not-found.html +1 -1
- package/mcp-server/.next/server/app/_not-found.rsc +2 -2
- package/mcp-server/.next/server/app/api/jank/[session]/route/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route/build-manifest.json +11 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js +9 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.map +5 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -0
- package/mcp-server/.next/server/app/api/jank/[session]/route_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
- package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
- package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/api/screenshots/list/route/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route/build-manifest.json +11 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js +7 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.map +5 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -0
- package/mcp-server/.next/server/app/api/screenshots/list/route_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app/api/tools/route.js +1 -1
- package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/index.html +1 -1
- package/mcp-server/.next/server/app/index.rsc +3 -3
- package/mcp-server/.next/server/app/logs/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/logs/page.js +1 -1
- package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/mcp/route.js +3 -2
- package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page/build-manifest.json +3 -3
- package/mcp-server/.next/server/app/page.js +1 -1
- package/mcp-server/.next/server/app/page.js.nft.json +1 -1
- package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
- package/mcp-server/.next/server/app/video/[session]/page/app-paths-manifest.json +3 -0
- package/mcp-server/.next/server/app/video/[session]/page/build-manifest.json +18 -0
- package/mcp-server/.next/server/app/video/[session]/page/next-font-manifest.json +6 -0
- package/mcp-server/.next/server/app/video/[session]/page/react-loadable-manifest.json +1 -0
- package/mcp-server/.next/server/app/video/[session]/page/server-reference-manifest.json +4 -0
- package/mcp-server/.next/server/app/video/[session]/page.js +15 -0
- package/mcp-server/.next/server/app/video/[session]/page.js.map +5 -0
- package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -0
- package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +2 -0
- package/mcp-server/.next/server/app-paths-manifest.json +4 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js +34 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__5580d2ea._.js → [root-of-the-server]__177c72c6._.js} +3 -3
- package/mcp-server/.next/server/chunks/[root-of-the-server]__177c72c6._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js → [root-of-the-server]__2056c8b5._.js} +2 -2
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js → [root-of-the-server]__55c04517._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js.map +1 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js.map +1 -0
- package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js → [root-of-the-server]__bc773251._.js} +2 -2
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js.map +1 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js +3 -0
- package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js.map +1 -0
- package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js +3 -0
- package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js.map +1 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js +3 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js.map +1 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js +3 -0
- package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__bf771f7e._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__8db775f9._.js.map → [root-of-the-server]__bf771f7e._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__c4e78a20._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__e5dec879._.js.map → [root-of-the-server]__c4e78a20._.js.map} +1 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js +1 -1
- package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js.map +1 -1
- package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js +2 -2
- package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js.map +1 -1
- package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js +4 -0
- package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js.map +1 -0
- package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js +3 -0
- package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js.map +1 -0
- package/mcp-server/.next/server/middleware-build-manifest.js +3 -3
- 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/.next/static/chunks/274a8d03fad7f819.js +1 -0
- package/mcp-server/.next/static/chunks/3d37ed424c6aaf63.css +1 -0
- package/mcp-server/.next/static/chunks/543e14c771a22442.js +1 -0
- package/mcp-server/.next/static/chunks/58fdd5192b305065.js +1 -0
- package/mcp-server/.next/static/chunks/6bd684c2018a357c.js +1 -0
- package/mcp-server/.next/static/chunks/6d59e588420330ca.js +1 -0
- package/mcp-server/.next/static/chunks/9625e4da85a132f3.js +1 -0
- package/mcp-server/.next/static/chunks/c36bc797d535a4dc.js +1 -0
- package/mcp-server/.next/static/chunks/{turbopack-7cd5a898ed038e26.js → turbopack-9656e7304584cab2.js} +2 -2
- package/mcp-server/app/api/jank/[session]/route.ts +344 -0
- package/mcp-server/app/api/screenshots/list/route.ts +22 -0
- package/mcp-server/app/logs/LogsClient.tsx +33 -0
- package/mcp-server/app/logs/utils.ts +2 -0
- package/mcp-server/app/mcp/route.ts +20 -0
- package/mcp-server/app/mcp/tools.ts +288 -9
- package/mcp-server/app/video/[session]/page.tsx +237 -0
- package/mcp-server/package.json +4 -0
- package/package.json +1 -1
- package/src/tui-interface-impl.tsx +15 -7
- package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js +0 -34
- package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js.map +0 -1
- package/mcp-server/.next/server/chunks/[root-of-the-server]__5580d2ea._.js.map +0 -1
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__8db775f9._.js +0 -3
- package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e5dec879._.js +0 -3
- package/mcp-server/.next/static/chunks/11ac0d0e69696c72.js +0 -1
- package/mcp-server/.next/static/chunks/172f6179d608c15f.js +0 -1
- package/mcp-server/.next/static/chunks/65b18bf1ede9811a.css +0 -1
- package/mcp-server/.next/static/chunks/bb8a4e5f381a85ec.js +0 -1
- package/mcp-server/.next/static/chunks/e09be78bba2194fd.js +0 -1
- package/mcp-server/.next/static/chunks/fec3a6ddaef02b8b.js +0 -1
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js.map → [root-of-the-server]__2056c8b5._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js.map → [root-of-the-server]__55c04517._.js.map} +0 -0
- /package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js.map → [root-of-the-server]__bc773251._.js.map} +0 -0
- /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_buildManifest.js +0 -0
- /package/mcp-server/.next/static/{GE4QeOWMVNF-ly63PtQFE → xyH2s6kJYyYw2j2TSlqpE}/_clientMiddlewareManifest.json +0 -0
- /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:
|
|
362
|
+
serverErrors: actionableErrors.filter(
|
|
350
363
|
(line) => line.includes("[SERVER]") && (line.includes("ERROR") || line.includes("Exception"))
|
|
351
364
|
),
|
|
352
|
-
browserErrors:
|
|
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:
|
|
370
|
+
buildErrors: actionableErrors.filter(
|
|
358
371
|
(line) => line.includes("Failed to compile") || line.includes("Type error") || line.includes("Build failed")
|
|
359
372
|
),
|
|
360
|
-
networkErrors:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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 =
|
|
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"><{marker.element.toLowerCase()}></span>
|
|
224
|
+
)}
|
|
225
|
+
</>
|
|
226
|
+
) : (
|
|
227
|
+
`CLS ${marker.timestamp}ms`
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
})}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
package/mcp-server/package.json
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
|
348
|
-
if (message
|
|
349
|
-
message = message.replace(
|
|
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"
|
|
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>
|