dev3000 0.0.77 → 0.0.79

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 (162) hide show
  1. package/README.md +18 -0
  2. package/dist/cdp-monitor.d.ts.map +1 -1
  3. package/dist/cdp-monitor.js +33 -10
  4. package/dist/cdp-monitor.js.map +1 -1
  5. package/dist/dev-environment.d.ts +2 -1
  6. package/dist/dev-environment.d.ts.map +1 -1
  7. package/dist/dev-environment.js +230 -271
  8. package/dist/dev-environment.js.map +1 -1
  9. package/dist/screencast-manager.d.ts +76 -0
  10. package/dist/screencast-manager.d.ts.map +1 -0
  11. package/dist/screencast-manager.js +410 -0
  12. package/dist/screencast-manager.js.map +1 -0
  13. package/dist/src/tui-interface-impl.tsx +45 -14
  14. package/dist/tui-interface-impl.d.ts +1 -0
  15. package/dist/tui-interface-impl.d.ts.map +1 -1
  16. package/dist/tui-interface-impl.js +24 -7
  17. package/dist/tui-interface-impl.js.map +1 -1
  18. package/dist/tui-interface.d.ts +2 -0
  19. package/dist/tui-interface.d.ts.map +1 -1
  20. package/dist/tui-interface.js +8 -1
  21. package/dist/tui-interface.js.map +1 -1
  22. package/mcp-server/.next/BUILD_ID +1 -1
  23. package/mcp-server/.next/app-path-routes-manifest.json +4 -1
  24. package/mcp-server/.next/build-manifest.json +5 -5
  25. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  26. package/mcp-server/.next/prerender-manifest.json +3 -3
  27. package/mcp-server/.next/routes-manifest.json +16 -0
  28. package/mcp-server/.next/server/app/_global-error/page/build-manifest.json +3 -3
  29. package/mcp-server/.next/server/app/_global-error/page.js +1 -1
  30. package/mcp-server/.next/server/app/_global-error/page.js.nft.json +1 -1
  31. package/mcp-server/.next/server/app/_global-error.html +2 -2
  32. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  33. package/mcp-server/.next/server/app/_not-found/page/build-manifest.json +3 -3
  34. package/mcp-server/.next/server/app/_not-found/page.js +1 -1
  35. package/mcp-server/.next/server/app/_not-found/page.js.nft.json +1 -1
  36. package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  37. package/mcp-server/.next/server/app/_not-found.html +1 -1
  38. package/mcp-server/.next/server/app/_not-found.rsc +2 -2
  39. package/mcp-server/.next/server/app/api/jank/[session]/route/app-paths-manifest.json +3 -0
  40. package/mcp-server/.next/server/app/api/jank/[session]/route/build-manifest.json +11 -0
  41. package/mcp-server/.next/server/app/api/jank/[session]/route/server-reference-manifest.json +4 -0
  42. package/mcp-server/.next/server/app/api/jank/[session]/route.js +9 -0
  43. package/mcp-server/.next/server/app/api/jank/[session]/route.js.map +5 -0
  44. package/mcp-server/.next/server/app/api/jank/[session]/route.js.nft.json +1 -0
  45. package/mcp-server/.next/server/app/api/jank/[session]/route_client-reference-manifest.js +2 -0
  46. package/mcp-server/.next/server/app/api/logs/head/route.js.nft.json +1 -1
  47. package/mcp-server/.next/server/app/api/logs/list/route.js.nft.json +1 -1
  48. package/mcp-server/.next/server/app/api/logs/rotate/route.js +1 -1
  49. package/mcp-server/.next/server/app/api/logs/rotate/route.js.nft.json +1 -1
  50. package/mcp-server/.next/server/app/api/logs/stream/route.js.nft.json +1 -1
  51. package/mcp-server/.next/server/app/api/logs/tail/route.js.nft.json +1 -1
  52. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js +1 -1
  53. package/mcp-server/.next/server/app/api/screenshots/[filename]/route.js.nft.json +1 -1
  54. package/mcp-server/.next/server/app/api/screenshots/list/route/app-paths-manifest.json +3 -0
  55. package/mcp-server/.next/server/app/api/screenshots/list/route/build-manifest.json +11 -0
  56. package/mcp-server/.next/server/app/api/screenshots/list/route/server-reference-manifest.json +4 -0
  57. package/mcp-server/.next/server/app/api/screenshots/list/route.js +7 -0
  58. package/mcp-server/.next/server/app/api/screenshots/list/route.js.map +5 -0
  59. package/mcp-server/.next/server/app/api/screenshots/list/route.js.nft.json +1 -0
  60. package/mcp-server/.next/server/app/api/screenshots/list/route_client-reference-manifest.js +2 -0
  61. package/mcp-server/.next/server/app/api/tools/route.js +1 -1
  62. package/mcp-server/.next/server/app/api/tools/route.js.nft.json +1 -1
  63. package/mcp-server/.next/server/app/index.html +1 -1
  64. package/mcp-server/.next/server/app/index.rsc +3 -3
  65. package/mcp-server/.next/server/app/logs/page/build-manifest.json +3 -3
  66. package/mcp-server/.next/server/app/logs/page.js +1 -1
  67. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  68. package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  69. package/mcp-server/.next/server/app/mcp/route.js +3 -2
  70. package/mcp-server/.next/server/app/mcp/route.js.nft.json +1 -1
  71. package/mcp-server/.next/server/app/page/build-manifest.json +3 -3
  72. package/mcp-server/.next/server/app/page.js +1 -1
  73. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  74. package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
  75. package/mcp-server/.next/server/app/video/[session]/page/app-paths-manifest.json +3 -0
  76. package/mcp-server/.next/server/app/video/[session]/page/build-manifest.json +18 -0
  77. package/mcp-server/.next/server/app/video/[session]/page/next-font-manifest.json +6 -0
  78. package/mcp-server/.next/server/app/video/[session]/page/react-loadable-manifest.json +1 -0
  79. package/mcp-server/.next/server/app/video/[session]/page/server-reference-manifest.json +4 -0
  80. package/mcp-server/.next/server/app/video/[session]/page.js +15 -0
  81. package/mcp-server/.next/server/app/video/[session]/page.js.map +5 -0
  82. package/mcp-server/.next/server/app/video/[session]/page.js.nft.json +1 -0
  83. package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +2 -0
  84. package/mcp-server/.next/server/app-paths-manifest.json +4 -1
  85. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js +34 -0
  86. package/mcp-server/.next/server/chunks/[root-of-the-server]__00592d3f._.js.map +1 -0
  87. package/mcp-server/.next/server/chunks/{[root-of-the-server]__5580d2ea._.js → [root-of-the-server]__177c72c6._.js} +3 -3
  88. package/mcp-server/.next/server/chunks/[root-of-the-server]__177c72c6._.js.map +1 -0
  89. package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js → [root-of-the-server]__2056c8b5._.js} +2 -2
  90. package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js → [root-of-the-server]__55c04517._.js} +2 -2
  91. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js +3 -0
  92. package/mcp-server/.next/server/chunks/[root-of-the-server]__6ee9a99f._.js.map +1 -0
  93. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js +3 -0
  94. package/mcp-server/.next/server/chunks/[root-of-the-server]__9a45c8f9._.js.map +1 -0
  95. package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js → [root-of-the-server]__bc773251._.js} +2 -2
  96. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js +3 -0
  97. package/mcp-server/.next/server/chunks/[root-of-the-server]__e1a64519._.js.map +1 -0
  98. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js +3 -0
  99. package/mcp-server/.next/server/chunks/[root-of-the-server]__e6dcd8bf._.js.map +1 -0
  100. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js +3 -0
  101. package/mcp-server/.next/server/chunks/d1d76_next_dist_esm_build_templates_app-route_820fc951.js.map +1 -0
  102. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js +3 -0
  103. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_jank_[session]_route_actions_3b2b275b.js.map +1 -0
  104. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js +3 -0
  105. package/mcp-server/.next/server/chunks/mcp-server__next-internal_server_app_api_screenshots_list_route_actions_acfa57bd.js.map +1 -0
  106. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__bf771f7e._.js +3 -0
  107. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__8db775f9._.js.map → [root-of-the-server]__bf771f7e._.js.map} +1 -1
  108. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__c4e78a20._.js +3 -0
  109. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__e5dec879._.js.map → [root-of-the-server]__c4e78a20._.js.map} +1 -1
  110. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js +3 -0
  111. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e1bc1b8a._.js.map +1 -0
  112. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js +3 -0
  113. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__fc10c8f1._.js.map +1 -0
  114. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js +1 -1
  115. package/mcp-server/.next/server/chunks/ssr/_0b8335fc._.js.map +1 -1
  116. package/mcp-server/.next/server/chunks/ssr/_62451611._.js.map +1 -1
  117. package/mcp-server/.next/server/chunks/ssr/_b15f05ee._.js.map +1 -1
  118. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js +2 -2
  119. package/mcp-server/.next/server/chunks/ssr/_bacf0748._.js.map +1 -1
  120. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js +4 -0
  121. package/mcp-server/.next/server/chunks/ssr/_e4aa8f16._.js.map +1 -0
  122. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js +3 -0
  123. package/mcp-server/.next/server/chunks/ssr/mcp-server__next-internal_server_app_video_[session]_page_actions_a6aab323.js.map +1 -0
  124. package/mcp-server/.next/server/middleware-build-manifest.js +3 -3
  125. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  126. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  127. package/mcp-server/.next/static/chunks/274a8d03fad7f819.js +1 -0
  128. package/mcp-server/.next/static/chunks/3d37ed424c6aaf63.css +1 -0
  129. package/mcp-server/.next/static/chunks/543e14c771a22442.js +1 -0
  130. package/mcp-server/.next/static/chunks/58fdd5192b305065.js +1 -0
  131. package/mcp-server/.next/static/chunks/6bd684c2018a357c.js +1 -0
  132. package/mcp-server/.next/static/chunks/6d59e588420330ca.js +1 -0
  133. package/mcp-server/.next/static/chunks/9625e4da85a132f3.js +1 -0
  134. package/mcp-server/.next/static/chunks/c36bc797d535a4dc.js +1 -0
  135. package/mcp-server/.next/static/chunks/{turbopack-7cd5a898ed038e26.js → turbopack-9656e7304584cab2.js} +2 -2
  136. package/mcp-server/app/api/jank/[session]/route.ts +344 -0
  137. package/mcp-server/app/api/screenshots/list/route.ts +22 -0
  138. package/mcp-server/app/logs/LogsClient.tsx +33 -0
  139. package/mcp-server/app/logs/utils.ts +2 -0
  140. package/mcp-server/app/mcp/route.ts +20 -0
  141. package/mcp-server/app/mcp/tools.ts +288 -9
  142. package/mcp-server/app/video/[session]/page.tsx +237 -0
  143. package/mcp-server/package.json +4 -6
  144. package/package.json +7 -1
  145. package/src/tui-interface-impl.tsx +45 -14
  146. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js +0 -34
  147. package/mcp-server/.next/server/chunks/[root-of-the-server]__270b33b7._.js.map +0 -1
  148. package/mcp-server/.next/server/chunks/[root-of-the-server]__5580d2ea._.js.map +0 -1
  149. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__8db775f9._.js +0 -3
  150. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__e5dec879._.js +0 -3
  151. package/mcp-server/.next/static/chunks/11ac0d0e69696c72.js +0 -1
  152. package/mcp-server/.next/static/chunks/172f6179d608c15f.js +0 -1
  153. package/mcp-server/.next/static/chunks/65b18bf1ede9811a.css +0 -1
  154. package/mcp-server/.next/static/chunks/bb8a4e5f381a85ec.js +0 -1
  155. package/mcp-server/.next/static/chunks/e09be78bba2194fd.js +0 -1
  156. package/mcp-server/.next/static/chunks/fec3a6ddaef02b8b.js +0 -1
  157. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__ffb73672._.js.map → [root-of-the-server]__2056c8b5._.js.map} +0 -0
  158. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__d1f9e389._.js.map → [root-of-the-server]__55c04517._.js.map} +0 -0
  159. /package/mcp-server/.next/server/chunks/{[root-of-the-server]__e2089993._.js.map → [root-of-the-server]__bc773251._.js.map} +0 -0
  160. /package/mcp-server/.next/static/{NWQx_KX68gOo5fDkdXq6G → T9qI0it_H-9Z-TE5_ROut}/_buildManifest.js +0 -0
  161. /package/mcp-server/.next/static/{NWQx_KX68gOo5fDkdXq6G → T9qI0it_H-9Z-TE5_ROut}/_clientMiddlewareManifest.json +0 -0
  162. /package/mcp-server/.next/static/{NWQx_KX68gOo5fDkdXq6G → T9qI0it_H-9Z-TE5_ROut}/_ssgManifest.js +0 -0
@@ -0,0 +1,410 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { WebSocket } from "ws";
5
+ /**
6
+ * ScreencastManager - Passive screencast capture for navigation events
7
+ *
8
+ * Listens for Page.frameStartedLoading and automatically captures 5 seconds
9
+ * of screencast frames for jank detection. No artificial page reloads needed!
10
+ */
11
+ export class ScreencastManager {
12
+ cdpUrl;
13
+ logFn;
14
+ ws = null;
15
+ buffer = [];
16
+ isCapturing = false;
17
+ navigationStartTime = 0;
18
+ currentSessionId = "";
19
+ screenshotDir;
20
+ messageId = 1000; // Start high to avoid conflicts
21
+ appPort;
22
+ layoutShifts = [];
23
+ viewportInfo = {};
24
+ clsObserverInstalled = false;
25
+ constructor(cdpUrl, logFn, appPort) {
26
+ this.cdpUrl = cdpUrl;
27
+ this.logFn = logFn;
28
+ this.screenshotDir = process.env.SCREENSHOT_DIR || join(tmpdir(), "dev3000-mcp-deps", "public", "screenshots");
29
+ this.appPort = appPort || process.env.APP_PORT || "3000";
30
+ if (!existsSync(this.screenshotDir)) {
31
+ mkdirSync(this.screenshotDir, { recursive: true });
32
+ }
33
+ }
34
+ /**
35
+ * Start listening for navigation events and capturing screencasts
36
+ */
37
+ async start() {
38
+ if (this.ws) {
39
+ return;
40
+ }
41
+ try {
42
+ this.ws = new WebSocket(this.cdpUrl);
43
+ this.ws.on("open", () => {
44
+ // Enable Page domain to receive navigation events
45
+ this.send("Page.enable", {});
46
+ // Enable Runtime domain for URL checking
47
+ this.send("Runtime.enable", {});
48
+ });
49
+ this.ws.on("message", (data) => {
50
+ this.handleMessage(JSON.parse(data.toString()));
51
+ });
52
+ this.ws.on("error", () => {
53
+ // Silently handle errors
54
+ });
55
+ this.ws.on("close", () => {
56
+ this.ws = null;
57
+ });
58
+ }
59
+ catch {
60
+ // Silently handle errors
61
+ }
62
+ }
63
+ /**
64
+ * Stop capturing and cleanup
65
+ */
66
+ async stop() {
67
+ if (this.isCapturing) {
68
+ await this.stopScreencast();
69
+ }
70
+ if (this.ws) {
71
+ this.ws.close();
72
+ this.ws = null;
73
+ }
74
+ // this.logFn("[CDP] Stopped")
75
+ }
76
+ /**
77
+ * Handle CDP messages
78
+ */
79
+ handleMessage(message) {
80
+ // Uncomment for CDP event debugging:
81
+ // if (message.method && (message.method.startsWith("Page.") || message.method.startsWith("Network."))) {
82
+ // this.logFn(`[CDP] Received CDP event: ${message.method}`)
83
+ // }
84
+ // Navigation started - check URL before capturing
85
+ // Note: Page.frameStartedNavigating fires earlier than Page.frameStartedLoading
86
+ if (message.method === "Page.frameStartedNavigating" || message.method === "Page.frameStartedLoading") {
87
+ this.checkUrlAndStartCapture();
88
+ }
89
+ // Navigation finished - save and stop
90
+ else if (message.method === "Page.loadEventFired") {
91
+ this.onNavigationComplete();
92
+ }
93
+ // Screencast frame received
94
+ else if (message.method === "Page.screencastFrame" && message.params) {
95
+ this.onScreencastFrame(message.params);
96
+ }
97
+ }
98
+ /**
99
+ * Check URL before starting capture - only capture localhost:{appPort}
100
+ */
101
+ async checkUrlAndStartCapture() {
102
+ try {
103
+ // Query current page URL using Runtime.evaluate
104
+ const evalId = this.messageId++;
105
+ this.send("Runtime.evaluate", {
106
+ expression: "window.location.href",
107
+ returnByValue: true
108
+ }, evalId);
109
+ // Wait for response (hacky but works for now)
110
+ const checkResponse = (message) => {
111
+ if (message.id === evalId && message.result?.result?.value) {
112
+ const url = message.result.result.value;
113
+ // this.logFn(`[CDP] Current URL: ${url}`)
114
+ // Only capture if it's the app URL (localhost:appPort)
115
+ if (url.includes(`localhost:${this.appPort}`)) {
116
+ // this.logFn("[CDP] URL matches app, starting capture")
117
+ this.onNavigationStart();
118
+ }
119
+ else {
120
+ // this.logFn(`[CDP] Skipping capture - URL does not match localhost:${this.appPort}`)
121
+ }
122
+ // Remove listener after handling
123
+ if (this.ws) {
124
+ this.ws.off("message", responseHandler);
125
+ }
126
+ }
127
+ };
128
+ const responseHandler = (data) => {
129
+ checkResponse(JSON.parse(data.toString()));
130
+ };
131
+ if (this.ws) {
132
+ this.ws.on("message", responseHandler);
133
+ // Timeout after 500ms
134
+ setTimeout(() => {
135
+ if (this.ws) {
136
+ this.ws.off("message", responseHandler);
137
+ }
138
+ }, 500);
139
+ }
140
+ }
141
+ catch (error) {
142
+ this.logFn(`[CDP] Failed to check URL - ${error}`);
143
+ // Fall back to capturing anyway
144
+ this.onNavigationStart();
145
+ }
146
+ }
147
+ /**
148
+ * Navigation started - begin capturing screencast
149
+ */
150
+ onNavigationStart() {
151
+ // this.logFn("[CDP] Navigation started, beginning screencast capture")
152
+ this.navigationStartTime = Date.now();
153
+ this.currentSessionId = new Date()
154
+ .toISOString()
155
+ .replace(/:/g, "-")
156
+ .replace(/\.\d{3}Z$/, "Z");
157
+ this.buffer = [];
158
+ this.layoutShifts = [];
159
+ this.isCapturing = true;
160
+ // Install CLS observer if not already present
161
+ this.installCLSObserver();
162
+ // Start screencast at 15fps with good quality
163
+ this.send("Page.startScreencast", {
164
+ format: "png",
165
+ quality: 80,
166
+ maxWidth: 1920,
167
+ maxHeight: 1080,
168
+ everyNthFrame: 1
169
+ });
170
+ }
171
+ /**
172
+ * Navigation completed - save frames and stop (after delay to catch hydration)
173
+ */
174
+ async onNavigationComplete() {
175
+ if (!this.isCapturing)
176
+ return;
177
+ // this.logFn(`[CDP] Page loaded, capturing 2 more seconds for hydration jank...`)
178
+ // Continue capturing for 2 more seconds to catch hydration issues
179
+ // Hydration often happens right after page load completes
180
+ setTimeout(async () => {
181
+ if (!this.isCapturing)
182
+ return;
183
+ // this.logFn(`[CDP] Navigation complete, saving ${this.buffer.length} frames`)
184
+ // Save all buffered frames
185
+ for (const frame of this.buffer) {
186
+ const screenshotPath = join(this.screenshotDir, `${this.currentSessionId}-jank-${frame.timestamp}ms.png`);
187
+ try {
188
+ const buffer = Buffer.from(frame.data, "base64");
189
+ writeFileSync(screenshotPath, buffer);
190
+ }
191
+ catch (error) {
192
+ this.logFn(`[CDP] Failed to save frame - ${error}`);
193
+ }
194
+ }
195
+ // this.logFn(`[CDP] Saved ${this.buffer.length} frames for session ${this.currentSessionId}`)
196
+ // Save session metadata with CLS data
197
+ const metadataPath = join(this.screenshotDir, `${this.currentSessionId}-metadata.json`);
198
+ const totalCLS = this.layoutShifts.reduce((sum, shift) => sum + shift.score, 0);
199
+ const metadata = {
200
+ sessionId: this.currentSessionId,
201
+ frameCount: this.buffer.length,
202
+ navigationStartTime: this.navigationStartTime,
203
+ captureEndTime: Date.now(),
204
+ appPort: this.appPort,
205
+ cssViewport: this.viewportInfo, // CSS viewport dimensions from window.innerWidth
206
+ layoutShifts: this.layoutShifts,
207
+ totalCLS,
208
+ clsGrade: totalCLS <= 0.1 ? "good" : totalCLS <= 0.25 ? "needs-improvement" : "poor"
209
+ };
210
+ try {
211
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
212
+ if (totalCLS > 0) {
213
+ this.logFn(`[CDP] Detected ${this.layoutShifts.length} layout shifts (CLS: ${totalCLS.toFixed(4)})`);
214
+ }
215
+ }
216
+ catch (error) {
217
+ this.logFn(`[CDP] Failed to save metadata - ${error}`);
218
+ }
219
+ this.logFn(`[CDP] View jank analysis: http://localhost:${process.env.MCP_PORT || "3684"}/video/${this.currentSessionId}`);
220
+ await this.stopScreencast();
221
+ }, 2000);
222
+ }
223
+ /**
224
+ * Received a screencast frame - add to buffer
225
+ */
226
+ onScreencastFrame(params) {
227
+ if (!this.isCapturing)
228
+ return;
229
+ const frameTimestamp = Date.now() - this.navigationStartTime;
230
+ const frameData = params.data;
231
+ const sessionId = params.sessionId;
232
+ // Acknowledge frame so we get more
233
+ this.send("Page.screencastFrameAck", { sessionId });
234
+ // Buffer frames (no time limit, onNavigationComplete handles stopping)
235
+ this.buffer.push({
236
+ timestamp: frameTimestamp,
237
+ data: frameData,
238
+ absoluteTime: Date.now()
239
+ });
240
+ // Keep buffer trimmed to prevent memory issues (max 10 seconds of frames)
241
+ const now = Date.now();
242
+ this.buffer = this.buffer.filter((f) => now - f.absoluteTime < 10000);
243
+ }
244
+ /**
245
+ * Stop screencast capture
246
+ */
247
+ async stopScreencast() {
248
+ if (!this.isCapturing)
249
+ return;
250
+ this.send("Page.stopScreencast", {});
251
+ this.isCapturing = false;
252
+ this.buffer = [];
253
+ // this.logFn("[CDP] Stopped screencast capture")
254
+ }
255
+ /**
256
+ * Install PerformanceObserver for layout shifts (passive, no reload needed)
257
+ */
258
+ installCLSObserver() {
259
+ if (this.clsObserverInstalled)
260
+ return;
261
+ const observerScript = `
262
+ (function() {
263
+ if (window.__dev3000_cls_observer__) return; // Already installed
264
+
265
+ window.__dev3000_cls_observer__ = true;
266
+ window.__dev3000_layout_shifts__ = [];
267
+ window.__dev3000_viewport__ = {
268
+ width: window.innerWidth,
269
+ height: window.innerHeight,
270
+ devicePixelRatio: window.devicePixelRatio || 1
271
+ };
272
+
273
+ try {
274
+ const observer = new PerformanceObserver((list) => {
275
+ for (const entry of list.getEntries()) {
276
+ if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) {
277
+ // For each shift, try to get the actual current bounding box
278
+ const sources = entry.sources ? entry.sources.map(s => {
279
+ let actualRect = null;
280
+ if (s.node && s.node.nodeName) {
281
+ try {
282
+ // Query the first matching element (nav, header, etc.)
283
+ const element = document.querySelector(s.node.nodeName.toLowerCase());
284
+ if (element) {
285
+ const rect = element.getBoundingClientRect();
286
+ actualRect = {
287
+ x: rect.x,
288
+ y: rect.y,
289
+ width: rect.width,
290
+ height: rect.height
291
+ };
292
+ }
293
+ } catch (e) {
294
+ // Ignore errors
295
+ }
296
+ }
297
+
298
+ return {
299
+ node: s.node ? s.node.nodeName : undefined,
300
+ previousRect: s.previousRect ? {
301
+ x: s.previousRect.x,
302
+ y: s.previousRect.y,
303
+ width: s.previousRect.width,
304
+ height: s.previousRect.height
305
+ } : {},
306
+ currentRect: s.currentRect ? {
307
+ x: s.currentRect.x,
308
+ y: s.currentRect.y,
309
+ width: s.currentRect.width,
310
+ height: s.currentRect.height
311
+ } : {},
312
+ actualRect: actualRect
313
+ };
314
+ }) : [];
315
+
316
+ window.__dev3000_layout_shifts__.push({
317
+ score: entry.value,
318
+ timestamp: entry.startTime,
319
+ sources: sources
320
+ });
321
+ }
322
+ }
323
+ });
324
+
325
+ observer.observe({ type: 'layout-shift', buffered: true });
326
+ console.log('[dev3000] CLS observer installed');
327
+ } catch (e) {
328
+ console.error('[dev3000] Failed to install CLS observer:', e);
329
+ }
330
+ })();
331
+ `;
332
+ // Inject observer via Runtime.evaluate
333
+ const evalId = this.messageId++;
334
+ this.send("Runtime.evaluate", { expression: observerScript, returnByValue: false }, evalId);
335
+ // Set up periodic polling to retrieve layout shift data
336
+ this.pollLayoutShifts();
337
+ this.clsObserverInstalled = true;
338
+ // this.logFn("Installed CLS observer")
339
+ }
340
+ /**
341
+ * Poll for layout shift data from the injected observer
342
+ */
343
+ pollLayoutShifts() {
344
+ if (!this.isCapturing)
345
+ return;
346
+ const pollId = this.messageId++;
347
+ const viewportId = this.messageId++;
348
+ this.send("Runtime.evaluate", {
349
+ expression: "window.__dev3000_layout_shifts__ || []",
350
+ returnByValue: true
351
+ }, pollId);
352
+ // Also get viewport info
353
+ this.send("Runtime.evaluate", {
354
+ expression: "window.__dev3000_viewport__ || {}",
355
+ returnByValue: true
356
+ }, viewportId);
357
+ // Listen for response
358
+ const handlePollResponse = (message) => {
359
+ if (message.id === pollId && message.result?.result?.value) {
360
+ const shifts = message.result.result.value;
361
+ if (shifts.length > this.layoutShifts.length) {
362
+ // New shifts detected
363
+ const newShifts = shifts.slice(this.layoutShifts.length);
364
+ this.layoutShifts.push(...newShifts);
365
+ newShifts.forEach((shift) => {
366
+ this.logFn(`[CDP] Layout shift detected (score: ${shift.score.toFixed(4)}, time: ${shift.timestamp.toFixed(0)}ms)`);
367
+ });
368
+ }
369
+ }
370
+ if (message.id === viewportId && message.result?.result?.value) {
371
+ this.viewportInfo = message.result.result.value;
372
+ }
373
+ };
374
+ const responseHandler = (data) => {
375
+ handlePollResponse(JSON.parse(data.toString()));
376
+ };
377
+ if (this.ws) {
378
+ this.ws.on("message", responseHandler);
379
+ // Timeout after 500ms
380
+ setTimeout(() => {
381
+ if (this.ws) {
382
+ this.ws.off("message", responseHandler);
383
+ }
384
+ }, 500);
385
+ }
386
+ // Poll every 500ms while capturing
387
+ if (this.isCapturing) {
388
+ setTimeout(() => this.pollLayoutShifts(), 500);
389
+ }
390
+ }
391
+ /**
392
+ * Send CDP command
393
+ */
394
+ send(method, params, id) {
395
+ if (!this.ws)
396
+ return;
397
+ this.ws.send(JSON.stringify({
398
+ id: id ?? this.messageId++,
399
+ method,
400
+ params
401
+ }));
402
+ }
403
+ /**
404
+ * Get the most recent session ID (for fix_my_app to reference)
405
+ */
406
+ getLatestSessionId() {
407
+ return this.currentSessionId;
408
+ }
409
+ }
410
+ //# sourceMappingURL=screencast-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screencast-manager.js","sourceRoot":"","sources":["../src/screencast-manager.ts"],"names":[],"mappings":"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"}
@@ -27,18 +27,23 @@ const COMPACT_LOGO = "d3k"
27
27
  const FULL_LOGO = [" ▐▌▄▄▄▄ █ ▄ ", " ▐▌ █ █▄▀ ", "▗▞▀▜▌▀▀▀█ █ ▀▄ ", "▝▚▄▟▌▄▄▄█ █ █ "]
28
28
 
29
29
  const TUIApp = ({
30
- appPort,
30
+ appPort: initialAppPort,
31
31
  mcpPort,
32
32
  logFile,
33
33
  commandName,
34
34
  serversOnly,
35
35
  version,
36
36
  projectName,
37
- onStatusUpdate
38
- }: TUIOptions & { onStatusUpdate: (fn: (status: string | null) => void) => void }) => {
37
+ onStatusUpdate,
38
+ onAppPortUpdate
39
+ }: TUIOptions & {
40
+ onStatusUpdate: (fn: (status: string | null) => void) => void
41
+ onAppPortUpdate: (fn: (port: string) => void) => void
42
+ }) => {
39
43
  const [logs, setLogs] = useState<LogEntry[]>([])
40
44
  const [scrollOffset, setScrollOffset] = useState(0)
41
45
  const [initStatus, setInitStatus] = useState<string | null>("Initializing...")
46
+ const [appPort, setAppPort] = useState<string>(initialAppPort)
42
47
  const logIdCounter = useRef(0)
43
48
  const { stdout } = useStdout()
44
49
  const ctrlCMessageDefault = "^C quit"
@@ -99,6 +104,13 @@ const TUIApp = ({
99
104
  })
100
105
  }, [onStatusUpdate])
101
106
 
107
+ // Provide app port update function to parent
108
+ useEffect(() => {
109
+ onAppPortUpdate((port: string) => {
110
+ setAppPort(port)
111
+ })
112
+ }, [onAppPortUpdate])
113
+
102
114
  // Calculate available lines for logs dynamically based on terminal height and mode
103
115
  const calculateMaxVisibleLogs = () => {
104
116
  if (isVeryCompact) {
@@ -248,12 +260,15 @@ const TUIApp = ({
248
260
  {initStatus && <Text dimColor>- {initStatus}</Text>}
249
261
  </Box>
250
262
  {!isVeryCompact && (
251
- <>
263
+ <Box flexDirection="column">
252
264
  <Text dimColor>
253
265
  App: localhost:{appPort} | MCP: localhost:{mcpPort}
254
266
  </Text>
255
- <Text dimColor>↑/↓ scroll | Ctrl-C quit</Text>
256
- </>
267
+ <Text dimColor>
268
+ 📸 http://localhost:{mcpPort}/logs
269
+ {projectName ? `?project=${encodeURIComponent(projectName)}` : ""}
270
+ </Text>
271
+ </Box>
257
272
  )}
258
273
  </Box>
259
274
  </Box>
@@ -332,9 +347,9 @@ const TUIApp = ({
332
347
  if (parts) {
333
348
  let [, timestamp, source, type, message] = parts
334
349
 
335
- // Replace specific emoji in common port-in-use error to avoid terminal width issues
336
- if (message?.includes("ERROR: Port") && message.includes("is in use by process")) {
337
- message = message.replace("ERROR: ⚠ Port", "ERROR: [!] Port")
350
+ // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
351
+ if (message && (type === "ERROR" || type === "WARNING")) {
352
+ message = message.replace(/⚠/g, "[!]")
338
353
  }
339
354
 
340
355
  // In very compact mode, simplify the output
@@ -438,19 +453,27 @@ const TUIApp = ({
438
453
 
439
454
  {/* Bottom status line - no border, just text */}
440
455
  <Box paddingX={1} justifyContent="space-between">
441
- <Text color="#A18CE5">⏵⏵ {logFile.replace(process.env.HOME || "", "~")}</Text>
456
+ <Text color="#A18CE5">
457
+ ⏵⏵{" "}
458
+ {isVeryCompact
459
+ ? logFile.split("/").slice(-2, -1)[0] || "logs" // Just show directory name
460
+ : logFile.replace(process.env.HOME || "", "~")}
461
+ </Text>
442
462
  <Text color="#A18CE5">{ctrlCMessage}</Text>
443
463
  </Box>
444
464
  </Box>
445
465
  )
446
466
  }
447
467
 
448
- export async function runTUI(
449
- options: TUIOptions
450
- ): Promise<{ app: { unmount: () => void }; updateStatus: (status: string | null) => void }> {
468
+ export async function runTUI(options: TUIOptions): Promise<{
469
+ app: { unmount: () => void }
470
+ updateStatus: (status: string | null) => void
471
+ updateAppPort: (port: string) => void
472
+ }> {
451
473
  return new Promise((resolve, reject) => {
452
474
  try {
453
475
  let statusUpdater: ((status: string | null) => void) | null = null
476
+ let appPortUpdater: ((port: string) => void) | null = null
454
477
 
455
478
  const app = render(
456
479
  <TUIApp
@@ -458,11 +481,14 @@ export async function runTUI(
458
481
  onStatusUpdate={(fn) => {
459
482
  statusUpdater = fn
460
483
  }}
484
+ onAppPortUpdate={(fn) => {
485
+ appPortUpdater = fn
486
+ }}
461
487
  />,
462
488
  { exitOnCtrlC: false }
463
489
  )
464
490
 
465
- // Give React time to set up the status updater
491
+ // Give React time to set up the updaters
466
492
  setTimeout(() => {
467
493
  resolve({
468
494
  app,
@@ -470,6 +496,11 @@ export async function runTUI(
470
496
  if (statusUpdater) {
471
497
  statusUpdater(status)
472
498
  }
499
+ },
500
+ updateAppPort: (port: string) => {
501
+ if (appPortUpdater) {
502
+ appPortUpdater(port)
503
+ }
473
504
  }
474
505
  })
475
506
  }, 100)
@@ -12,5 +12,6 @@ export declare function runTUI(options: TUIOptions): Promise<{
12
12
  unmount: () => void;
13
13
  };
14
14
  updateStatus: (status: string | null) => void;
15
+ updateAppPort: (port: string) => void;
15
16
  }>;
16
17
  //# sourceMappingURL=tui-interface-impl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tui-interface-impl.d.ts","sourceRoot":"","sources":["../src/tui-interface-impl.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgbD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAAC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAAE,CAAC,CA+B1F"}
1
+ {"version":3,"file":"tui-interface-impl.d.ts","sourceRoot":"","sources":["../src/tui-interface-impl.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAocD,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;IACzD,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;IAC5B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC,CAAC,CAwCD"}
@@ -8,10 +8,11 @@ import { LOG_COLORS } from "./constants/log-colors.js";
8
8
  const COMPACT_LOGO = "d3k";
9
9
  // Full ASCII logo lines as array for easier rendering
10
10
  const FULL_LOGO = [" ▐▌▄▄▄▄ █ ▄ ", " ▐▌ █ █▄▀ ", "▗▞▀▜▌▀▀▀█ █ ▀▄ ", "▝▚▄▟▌▄▄▄█ █ █ "];
11
- const TUIApp = ({ appPort, mcpPort, logFile, commandName, serversOnly, version, projectName, onStatusUpdate }) => {
11
+ const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, serversOnly, version, projectName, onStatusUpdate, onAppPortUpdate }) => {
12
12
  const [logs, setLogs] = useState([]);
13
13
  const [scrollOffset, setScrollOffset] = useState(0);
14
14
  const [initStatus, setInitStatus] = useState("Initializing...");
15
+ const [appPort, setAppPort] = useState(initialAppPort);
15
16
  const logIdCounter = useRef(0);
16
17
  const { stdout } = useStdout();
17
18
  const ctrlCMessageDefault = "^C quit";
@@ -65,6 +66,12 @@ const TUIApp = ({ appPort, mcpPort, logFile, commandName, serversOnly, version,
65
66
  }
66
67
  });
67
68
  }, [onStatusUpdate]);
69
+ // Provide app port update function to parent
70
+ useEffect(() => {
71
+ onAppPortUpdate((port) => {
72
+ setAppPort(port);
73
+ });
74
+ }, [onAppPortUpdate]);
68
75
  // Calculate available lines for logs dynamically based on terminal height and mode
69
76
  const calculateMaxVisibleLogs = () => {
70
77
  if (isVeryCompact) {
@@ -193,7 +200,7 @@ const TUIApp = ({ appPort, mcpPort, logFile, commandName, serversOnly, version,
193
200
  // Calculate visible logs
194
201
  const visibleLogs = logs.slice(Math.max(0, logs.length - maxVisibleLogs - scrollOffset), logs.length - scrollOffset);
195
202
  // Render compact header for small terminals
196
- const renderCompactHeader = () => (_jsx(Box, { borderStyle: "single", borderColor: "#A18CE5", paddingX: 1, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#A18CE5", bold: true, children: COMPACT_LOGO }), _jsxs(Text, { children: [" v", version, " "] }), initStatus && _jsxs(Text, { dimColor: true, children: ["- ", initStatus] })] }), !isVeryCompact && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["App: localhost:", appPort, " | MCP: localhost:", mcpPort] }), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 scroll | Ctrl-C quit" })] }))] }) }));
203
+ const renderCompactHeader = () => (_jsx(Box, { borderStyle: "single", borderColor: "#A18CE5", paddingX: 1, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#A18CE5", bold: true, children: COMPACT_LOGO }), _jsxs(Text, { children: [" v", version, " "] }), initStatus && _jsxs(Text, { dimColor: true, children: ["- ", initStatus] })] }), !isVeryCompact && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["App: localhost:", appPort, " | MCP: localhost:", mcpPort] }), _jsxs(Text, { dimColor: true, children: ["\uD83D\uDCF8 http://localhost:", mcpPort, "/logs", projectName ? `?project=${encodeURIComponent(projectName)}` : ""] })] }))] }) }));
197
204
  // Render normal header
198
205
  const renderNormalHeader = () => {
199
206
  // Create custom top border with title embedded (like Claude Code)
@@ -212,9 +219,9 @@ const TUIApp = ({ appPort, mcpPort, logFile, commandName, serversOnly, version,
212
219
  const parts = log.content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/);
213
220
  if (parts) {
214
221
  let [, timestamp, source, type, message] = parts;
215
- // Replace specific emoji in common port-in-use error to avoid terminal width issues
216
- if (message?.includes("ERROR: Port") && message.includes("is in use by process")) {
217
- message = message.replace("ERROR: ⚠ Port", "ERROR: [!] Port");
222
+ // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
223
+ if (message && (type === "ERROR" || type === "WARNING")) {
224
+ message = message.replace(/⚠/g, "[!]");
218
225
  }
219
226
  // In very compact mode, simplify the output
220
227
  if (isVeryCompact) {
@@ -258,16 +265,21 @@ const TUIApp = ({ appPort, mcpPort, logFile, commandName, serversOnly, version,
258
265
  }
259
266
  // Fallback for unparsed lines
260
267
  return (_jsx(Text, { wrap: "truncate-end", children: log.content }, log.id));
261
- })) }), !isVeryCompact && logs.length > maxVisibleLogs && scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: ["(", scrollOffset, " lines below)"] }))] }), _jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: "#A18CE5", children: ["\u23F5\u23F5 ", logFile.replace(process.env.HOME || "", "~")] }), _jsx(Text, { color: "#A18CE5", children: ctrlCMessage })] })] }));
268
+ })) }), !isVeryCompact && logs.length > maxVisibleLogs && scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: ["(", scrollOffset, " lines below)"] }))] }), _jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: "#A18CE5", children: ["\u23F5\u23F5", " ", isVeryCompact
269
+ ? logFile.split("/").slice(-2, -1)[0] || "logs" // Just show directory name
270
+ : logFile.replace(process.env.HOME || "", "~")] }), _jsx(Text, { color: "#A18CE5", children: ctrlCMessage })] })] }));
262
271
  };
263
272
  export async function runTUI(options) {
264
273
  return new Promise((resolve, reject) => {
265
274
  try {
266
275
  let statusUpdater = null;
276
+ let appPortUpdater = null;
267
277
  const app = render(_jsx(TUIApp, { ...options, onStatusUpdate: (fn) => {
268
278
  statusUpdater = fn;
279
+ }, onAppPortUpdate: (fn) => {
280
+ appPortUpdater = fn;
269
281
  } }), { exitOnCtrlC: false });
270
- // Give React time to set up the status updater
282
+ // Give React time to set up the updaters
271
283
  setTimeout(() => {
272
284
  resolve({
273
285
  app,
@@ -275,6 +287,11 @@ export async function runTUI(options) {
275
287
  if (statusUpdater) {
276
288
  statusUpdater(status);
277
289
  }
290
+ },
291
+ updateAppPort: (port) => {
292
+ if (appPortUpdater) {
293
+ appPortUpdater(port);
294
+ }
278
295
  }
279
296
  });
280
297
  }, 100);