dev3000 0.0.114 → 0.0.116

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 (146) hide show
  1. package/dist/cdp-monitor.d.ts.map +1 -1
  2. package/dist/cdp-monitor.js +20 -28
  3. package/dist/cdp-monitor.js.map +1 -1
  4. package/dist/cli.js +74 -22
  5. package/dist/cli.js.map +1 -1
  6. package/dist/dev-environment.d.ts.map +1 -1
  7. package/dist/dev-environment.js +43 -11
  8. package/dist/dev-environment.js.map +1 -1
  9. package/dist/src/tui-interface-impl.tsx +175 -127
  10. package/dist/tui-interface-impl.d.ts.map +1 -1
  11. package/dist/tui-interface-impl.js +113 -74
  12. package/dist/tui-interface-impl.js.map +1 -1
  13. package/mcp-server/.next/BUILD_ID +1 -1
  14. package/mcp-server/.next/build-manifest.json +2 -2
  15. package/mcp-server/.next/fallback-build-manifest.json +2 -2
  16. package/mcp-server/.next/prerender-manifest.json +3 -3
  17. package/mcp-server/.next/server/app/_global-error.html +2 -2
  18. package/mcp-server/.next/server/app/_global-error.rsc +1 -1
  19. package/mcp-server/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  20. package/mcp-server/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/mcp-server/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  22. package/mcp-server/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  23. package/mcp-server/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  24. package/mcp-server/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  25. package/mcp-server/.next/server/app/_not-found.html +1 -1
  26. package/mcp-server/.next/server/app/_not-found.rsc +2 -2
  27. package/mcp-server/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  28. package/mcp-server/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  29. package/mcp-server/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  30. package/mcp-server/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  31. package/mcp-server/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  32. package/mcp-server/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  33. package/mcp-server/.next/server/app/api/cloud/start-fix/route.js +2 -2
  34. package/mcp-server/.next/server/app/api/cloud/start-fix/route.js.nft.json +1 -1
  35. package/mcp-server/.next/server/app/auth/error/page_client-reference-manifest.js +1 -1
  36. package/mcp-server/.next/server/app/auth/error.html +1 -1
  37. package/mcp-server/.next/server/app/auth/error.rsc +2 -2
  38. package/mcp-server/.next/server/app/auth/error.segments/_full.segment.rsc +2 -2
  39. package/mcp-server/.next/server/app/auth/error.segments/_head.segment.rsc +1 -1
  40. package/mcp-server/.next/server/app/auth/error.segments/_index.segment.rsc +2 -2
  41. package/mcp-server/.next/server/app/auth/error.segments/_tree.segment.rsc +2 -2
  42. package/mcp-server/.next/server/app/auth/error.segments/auth/error/__PAGE__.segment.rsc +1 -1
  43. package/mcp-server/.next/server/app/auth/error.segments/auth/error.segment.rsc +1 -1
  44. package/mcp-server/.next/server/app/auth/error.segments/auth.segment.rsc +1 -1
  45. package/mcp-server/.next/server/app/index.html +1 -1
  46. package/mcp-server/.next/server/app/index.rsc +3 -3
  47. package/mcp-server/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  48. package/mcp-server/.next/server/app/index.segments/_full.segment.rsc +3 -3
  49. package/mcp-server/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/mcp-server/.next/server/app/index.segments/_index.segment.rsc +2 -2
  51. package/mcp-server/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  52. package/mcp-server/.next/server/app/logs/page.js.nft.json +1 -1
  53. package/mcp-server/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  54. package/mcp-server/.next/server/app/mcp/route.js +2 -2
  55. package/mcp-server/.next/server/app/page.js.nft.json +1 -1
  56. package/mcp-server/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/mcp-server/.next/server/app/signin/page_client-reference-manifest.js +1 -1
  58. package/mcp-server/.next/server/app/video/[session]/page_client-reference-manifest.js +1 -1
  59. package/mcp-server/.next/server/app/workflows/[id]/report/page_client-reference-manifest.js +1 -1
  60. package/mcp-server/.next/server/app/workflows/new/page.js.nft.json +1 -1
  61. package/mcp-server/.next/server/app/workflows/new/page_client-reference-manifest.js +1 -1
  62. package/mcp-server/.next/server/app/workflows/page.js.nft.json +1 -1
  63. package/mcp-server/.next/server/app/workflows/page_client-reference-manifest.js +1 -1
  64. package/mcp-server/.next/server/chunks/[root-of-the-server]__157de66b._.js +284 -37
  65. package/mcp-server/.next/server/chunks/[root-of-the-server]__157de66b._.js.map +1 -1
  66. package/mcp-server/.next/server/chunks/[root-of-the-server]__730a8fd0._.js +1 -1
  67. package/mcp-server/.next/server/chunks/[root-of-the-server]__730a8fd0._.js.map +1 -1
  68. package/mcp-server/.next/server/chunks/[root-of-the-server]__748f411f._.js +1 -1
  69. package/mcp-server/.next/server/chunks/[root-of-the-server]__748f411f._.js.map +1 -1
  70. package/mcp-server/.next/server/chunks/[root-of-the-server]__8a84f9f4._.js +20 -20
  71. package/mcp-server/.next/server/chunks/[root-of-the-server]__8a84f9f4._.js.map +1 -1
  72. package/mcp-server/.next/server/chunks/[root-of-the-server]__c1681338._.js +3 -0
  73. package/mcp-server/.next/server/chunks/[root-of-the-server]__c1681338._.js.map +1 -0
  74. package/mcp-server/.next/server/chunks/[root-of-the-server]__ec6a1335._.js.map +1 -1
  75. package/mcp-server/.next/server/chunks/bee4f_next_dist_esm_build_templates_app-route_1ece9366.js +5 -5
  76. package/mcp-server/.next/server/chunks/bee4f_next_dist_esm_build_templates_app-route_1ece9366.js.map +1 -1
  77. package/mcp-server/.next/server/chunks/mcp-server_app_api_cloud_fix-workflow_steps_ts_b65f3271._.js +121 -27
  78. package/mcp-server/.next/server/chunks/mcp-server_app_api_cloud_fix-workflow_steps_ts_b65f3271._.js.map +1 -1
  79. package/mcp-server/.next/server/chunks/node_modules__pnpm_85ddbe9c._.js +1 -1
  80. package/mcp-server/.next/server/chunks/node_modules__pnpm_85ddbe9c._.js.map +1 -1
  81. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__2e44f0db._.js +3 -0
  82. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__2e44f0db._.js.map +1 -0
  83. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__3585c949._.js +3 -0
  84. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__3585c949._.js.map +1 -0
  85. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__477c3bbb._.js +3 -0
  86. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__477c3bbb._.js.map +1 -0
  87. package/mcp-server/.next/server/chunks/ssr/{[root-of-the-server]__570677dc._.js → [root-of-the-server]__880839a0._.js} +2 -2
  88. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__880839a0._.js.map +1 -0
  89. package/mcp-server/.next/server/chunks/ssr/_41b8f993._.js +3 -0
  90. package/mcp-server/.next/server/chunks/ssr/_41b8f993._.js.map +1 -0
  91. package/mcp-server/.next/server/chunks/ssr/_9ba0ef29._.js +3 -0
  92. package/mcp-server/.next/server/chunks/ssr/_9ba0ef29._.js.map +1 -0
  93. package/mcp-server/.next/server/chunks/ssr/_cd4dc25e._.js.map +1 -1
  94. package/mcp-server/.next/server/chunks/ssr/mcp-server_app_workflows_new_new-workflow-client_tsx_1312c046._.js +2 -2
  95. package/mcp-server/.next/server/chunks/ssr/mcp-server_app_workflows_new_new-workflow-client_tsx_1312c046._.js.map +1 -1
  96. package/mcp-server/.next/server/chunks/ssr/mcp-server_app_workflows_workflows-client_tsx_268cfd4a._.js +7 -0
  97. package/mcp-server/.next/server/chunks/ssr/mcp-server_app_workflows_workflows-client_tsx_268cfd4a._.js.map +1 -0
  98. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_961f21c4._.js +3 -0
  99. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_961f21c4._.js.map +1 -0
  100. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_a82244bf._.js +3 -0
  101. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_a82244bf._.js.map +1 -0
  102. package/mcp-server/.next/server/chunks/ssr/{node_modules__pnpm_07527699._.js → node_modules__pnpm_eb98e511._.js} +2 -2
  103. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_eb98e511._.js.map +1 -0
  104. package/mcp-server/.next/server/server-reference-manifest.js +1 -1
  105. package/mcp-server/.next/server/server-reference-manifest.json +1 -1
  106. package/mcp-server/.next/static/chunks/000849a6a897f531.css +1 -0
  107. package/mcp-server/.next/static/chunks/048cee2510ddb1a0.js +1 -0
  108. package/mcp-server/.next/static/chunks/0622bd0e093adee7.js +3 -0
  109. package/mcp-server/.next/static/chunks/{46f60efee5f19794.js → 16359f64918a93f3.js} +1 -1
  110. package/mcp-server/.next/static/chunks/1851a3e70d7efc10.js +1 -0
  111. package/mcp-server/.next/static/chunks/{cc6addc4bb10fa11.js → 2ad16eeb719786f1.js} +1 -1
  112. package/mcp-server/.next/static/chunks/57feca7a4e06545e.js +7 -0
  113. package/mcp-server/.next/static/chunks/93db5737a327ab0c.js +6 -0
  114. package/mcp-server/.next/static/chunks/9fd3c715ecfb4d05.js +1 -0
  115. package/mcp-server/.next/static/chunks/b4b1ec6435790587.js +1 -0
  116. package/mcp-server/.next/static/chunks/cfe150cb2048b7e8.js +1 -0
  117. package/mcp-server/app/api/cloud/fix-workflow/steps.ts +359 -28
  118. package/mcp-server/app/api/cloud/fix-workflow/workflow.ts +16 -8
  119. package/mcp-server/app/api/cloud/start-fix/route.ts +2 -2
  120. package/mcp-server/app/api/tools/route.ts +11 -12
  121. package/mcp-server/app/api/workflows/route.ts +45 -1
  122. package/mcp-server/app/mcp/tools.ts +58 -98
  123. package/mcp-server/app/workflows/workflows-client.tsx +259 -100
  124. package/package.json +1 -1
  125. package/src/tui-interface-impl.tsx +175 -127
  126. package/mcp-server/.next/server/chunks/[root-of-the-server]__75d68567._.js +0 -3
  127. package/mcp-server/.next/server/chunks/[root-of-the-server]__75d68567._.js.map +0 -1
  128. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__0ff05d72._.js +0 -3
  129. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__0ff05d72._.js.map +0 -1
  130. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__27cc5956._.js +0 -3
  131. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__27cc5956._.js.map +0 -1
  132. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__570677dc._.js.map +0 -1
  133. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__ef510343._.js +0 -3
  134. package/mcp-server/.next/server/chunks/ssr/[root-of-the-server]__ef510343._.js.map +0 -1
  135. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_07527699._.js.map +0 -1
  136. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_7cc36047._.js +0 -3
  137. package/mcp-server/.next/server/chunks/ssr/node_modules__pnpm_7cc36047._.js.map +0 -1
  138. package/mcp-server/.next/static/chunks/07848f6bd2a7e5f6.js +0 -3
  139. package/mcp-server/.next/static/chunks/637a66565f27572f.js +0 -6
  140. package/mcp-server/.next/static/chunks/aed4fb5252a4bc95.js +0 -3
  141. package/mcp-server/.next/static/chunks/e8d521464b0c96ca.css +0 -1
  142. package/mcp-server/.next/static/chunks/ff53279afa939907.js +0 -1
  143. package/mcp-server/.next/static/chunks/ffa2ecb6845be49c.js +0 -1
  144. /package/mcp-server/.next/static/{ZvLsxTWoDyQzaSsr_VeX6 → G5taiQ-Jp0B_MdvkQuoIT}/_buildManifest.js +0 -0
  145. /package/mcp-server/.next/static/{ZvLsxTWoDyQzaSsr_VeX6 → G5taiQ-Jp0B_MdvkQuoIT}/_clientMiddlewareManifest.json +0 -0
  146. /package/mcp-server/.next/static/{ZvLsxTWoDyQzaSsr_VeX6 → G5taiQ-Jp0B_MdvkQuoIT}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@ import chalk from "chalk"
2
2
  import { createReadStream, unwatchFile, watchFile } from "fs"
3
3
  import { Box, render, Text, useInput, useStdout } from "ink"
4
4
  import Spinner from "ink-spinner"
5
- import { useEffect, useRef, useState } from "react"
5
+ import { memo, useEffect, useRef, useState } from "react"
6
6
  import type { Readable } from "stream"
7
7
  import { LOG_COLORS } from "./constants/log-colors.js"
8
8
 
@@ -29,6 +29,117 @@ const COMPACT_LOGO = "d3k"
29
29
  // Full ASCII logo lines as array for easier rendering
30
30
  const FULL_LOGO = [" ▐▌▄▄▄▄ █ ▄ ", " ▐▌ █ █▄▀ ", "▗▞▀▜▌▀▀▀█ █ ▀▄ ", "▝▚▄▟▌▄▄▄█ █ █ "]
31
31
 
32
+ // Type colors map - defined outside component to avoid recreation
33
+ const TYPE_COLORS: Record<string, string> = {
34
+ NETWORK: LOG_COLORS.NETWORK,
35
+ ERROR: LOG_COLORS.ERROR,
36
+ WARNING: LOG_COLORS.WARNING,
37
+ INFO: LOG_COLORS.INFO,
38
+ LOG: LOG_COLORS.LOG,
39
+ DEBUG: LOG_COLORS.DEBUG,
40
+ SCREENSHOT: LOG_COLORS.SCREENSHOT,
41
+ DOM: LOG_COLORS.DOM,
42
+ CDP: LOG_COLORS.CDP,
43
+ CHROME: LOG_COLORS.CHROME,
44
+ CRASH: LOG_COLORS.CRASH,
45
+ REPLAY: LOG_COLORS.REPLAY,
46
+ NAVIGATION: LOG_COLORS.NAVIGATION,
47
+ INTERACTION: LOG_COLORS.INTERACTION,
48
+ GET: LOG_COLORS.SERVER,
49
+ POST: LOG_COLORS.SERVER,
50
+ PUT: LOG_COLORS.SERVER,
51
+ DELETE: LOG_COLORS.SERVER,
52
+ PATCH: LOG_COLORS.SERVER,
53
+ HEAD: LOG_COLORS.SERVER,
54
+ OPTIONS: LOG_COLORS.SERVER
55
+ }
56
+
57
+ // Memoized log line component to prevent re-parsing on every render
58
+ const LogLine = memo(
59
+ ({ log, isCompact, isVeryCompact }: { log: LogEntry; isCompact: boolean; isVeryCompact: boolean }) => {
60
+ // Parse log line to colorize different parts
61
+ const parts = log.content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/)
62
+
63
+ if (parts) {
64
+ let [, timestamp, source, type, message] = parts
65
+
66
+ // Extract HTTP method from SERVER logs as a secondary tag
67
+ if (source === "SERVER" && !type && message) {
68
+ const methodMatch = message.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/)
69
+ if (methodMatch) {
70
+ type = methodMatch[1]
71
+ message = message.slice(type.length + 1) // Remove method from message
72
+ }
73
+ }
74
+
75
+ // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
76
+ if (message && (type === "ERROR" || type === "WARNING")) {
77
+ message = message.replace(/⚠/g, "[!]")
78
+ }
79
+
80
+ // In very compact mode, simplify the output
81
+ if (isVeryCompact) {
82
+ const shortSource = source === "BROWSER" ? "B" : "S"
83
+ const shortType = type ? type.split(".")[0].charAt(0) : ""
84
+ return (
85
+ <Text wrap="truncate-end">
86
+ <Text dimColor>[{shortSource}]</Text>
87
+ {shortType && <Text dimColor>[{shortType}]</Text>}
88
+ <Text> {message}</Text>
89
+ </Text>
90
+ )
91
+ }
92
+
93
+ // Use shared color constants
94
+ const sourceColor = source === "BROWSER" ? LOG_COLORS.BROWSER : LOG_COLORS.SERVER
95
+
96
+ // In compact mode, skip padding
97
+ if (isCompact) {
98
+ return (
99
+ <Text wrap="truncate-end">
100
+ <Text dimColor>[{timestamp}]</Text>
101
+ <Text> </Text>
102
+ <Text color={sourceColor} bold>
103
+ [{source.charAt(0)}]
104
+ </Text>
105
+ {type && (
106
+ <>
107
+ <Text> </Text>
108
+ <Text color={TYPE_COLORS[type] || "#A0A0A0"}>[{type}]</Text>
109
+ </>
110
+ )}
111
+ <Text> {message}</Text>
112
+ </Text>
113
+ )
114
+ }
115
+
116
+ // Normal mode with minimal padding
117
+ return (
118
+ <Text wrap="truncate-end">
119
+ <Text dimColor>[{timestamp}]</Text>
120
+ <Text> </Text>
121
+ <Text color={sourceColor} bold>
122
+ [{source}]
123
+ </Text>
124
+ {type ? (
125
+ <>
126
+ <Text> </Text>
127
+ <Text color={TYPE_COLORS[type] || "#A0A0A0"}>[{type}]</Text>
128
+ <Text> </Text>
129
+ </>
130
+ ) : (
131
+ <Text> </Text>
132
+ )}
133
+ <Text>{message}</Text>
134
+ </Text>
135
+ )
136
+ }
137
+
138
+ // Fallback for unparsed lines
139
+ return <Text wrap="truncate-end">{log.content}</Text>
140
+ }
141
+ )
142
+
32
143
  const TUIApp = ({
33
144
  appPort: initialAppPort,
34
145
  mcpPort,
@@ -155,19 +266,19 @@ const TUIApp = ({
155
266
  useEffect(() => {
156
267
  let logStream: Readable | undefined
157
268
  let buffer = ""
269
+ let pendingLogs: LogEntry[] = []
270
+ let flushTimeout: NodeJS.Timeout | null = null
158
271
 
159
- const appendLog = (line: string) => {
160
- if (NEXTJS_MCP_404_REGEX.test(line)) {
161
- return
162
- }
272
+ // Batch log updates to prevent excessive renders
273
+ const flushPendingLogs = () => {
274
+ if (pendingLogs.length === 0) return
163
275
 
164
- const newLog: LogEntry = {
165
- id: logIdCounter.current++,
166
- content: line
167
- }
276
+ const logsToAdd = pendingLogs
277
+ pendingLogs = []
278
+ flushTimeout = null
168
279
 
169
280
  setLogs((prevLogs) => {
170
- const updated = [...prevLogs, newLog]
281
+ const updated = [...prevLogs, ...logsToAdd]
171
282
  // Keep only last N logs to prevent memory issues
172
283
  if (updated.length > maxLogs) {
173
284
  return updated.slice(-maxLogs)
@@ -176,12 +287,32 @@ const TUIApp = ({
176
287
  })
177
288
 
178
289
  // Auto-scroll to bottom only if user is already at the bottom
179
- // Otherwise, increment scroll offset by 1, accounting for the appended log and max scroll offset
290
+ // Otherwise, increment scroll offset by count of new logs
180
291
  setScrollOffset((currentOffset) => {
181
- return currentOffset === 0 ? 0 : Math.min(maxScrollOffsetRef.current, currentOffset + 1)
292
+ return currentOffset === 0 ? 0 : Math.min(maxScrollOffsetRef.current, currentOffset + logsToAdd.length)
182
293
  })
183
294
  }
184
295
 
296
+ const appendLog = (line: string) => {
297
+ if (NEXTJS_MCP_404_REGEX.test(line)) {
298
+ return
299
+ }
300
+
301
+ const newLog: LogEntry = {
302
+ id: logIdCounter.current++,
303
+ content: line
304
+ }
305
+
306
+ pendingLogs.push(newLog)
307
+
308
+ // Debounce: flush after 50ms of no new logs
309
+ // Terminal synchronized updates prevent flicker, so we can be more responsive
310
+ if (flushTimeout) {
311
+ clearTimeout(flushTimeout)
312
+ }
313
+ flushTimeout = setTimeout(flushPendingLogs, 50)
314
+ }
315
+
185
316
  // Create a read stream for the log file
186
317
  logStream = createReadStream(logFile, {
187
318
  encoding: "utf8",
@@ -236,6 +367,9 @@ const TUIApp = ({
236
367
  if (logStream) {
237
368
  logStream.destroy()
238
369
  }
370
+ if (flushTimeout) {
371
+ clearTimeout(flushTimeout)
372
+ }
239
373
  unwatchFile(logFile)
240
374
  }
241
375
  }, [logFile])
@@ -376,121 +510,9 @@ const TUIApp = ({
376
510
  {visibleLogs.length === 0 ? (
377
511
  <Text dimColor>Waiting for logs...</Text>
378
512
  ) : (
379
- visibleLogs.map((log) => {
380
- // Parse log line to colorize different parts
381
- const parts = log.content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/)
382
-
383
- if (parts) {
384
- let [, timestamp, source, type, message] = parts
385
-
386
- // Extract HTTP method from SERVER logs as a secondary tag
387
- if (source === "SERVER" && !type && message) {
388
- const methodMatch = message.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/)
389
- if (methodMatch) {
390
- type = methodMatch[1]
391
- message = message.slice(type.length + 1) // Remove method from message
392
- }
393
- }
394
-
395
- // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
396
- if (message && (type === "ERROR" || type === "WARNING")) {
397
- message = message.replace(/⚠/g, "[!]")
398
- }
399
-
400
- // In very compact mode, simplify the output
401
- if (isVeryCompact) {
402
- const shortSource = source === "BROWSER" ? "B" : "S"
403
- const shortType = type ? type.split(".")[0].charAt(0) : ""
404
- return (
405
- <Text key={log.id} wrap="truncate-end">
406
- <Text dimColor>[{shortSource}]</Text>
407
- {shortType && <Text dimColor>[{shortType}]</Text>}
408
- <Text> {message}</Text>
409
- </Text>
410
- )
411
- }
412
-
413
- // Use shared color constants
414
- const sourceColor = source === "BROWSER" ? LOG_COLORS.BROWSER : LOG_COLORS.SERVER
415
- const typeColors: Record<string, string> = {
416
- NETWORK: LOG_COLORS.NETWORK,
417
- ERROR: LOG_COLORS.ERROR,
418
- WARNING: LOG_COLORS.WARNING,
419
- INFO: LOG_COLORS.INFO,
420
- LOG: LOG_COLORS.LOG,
421
- DEBUG: LOG_COLORS.DEBUG,
422
- SCREENSHOT: LOG_COLORS.SCREENSHOT,
423
- DOM: LOG_COLORS.DOM,
424
- CDP: LOG_COLORS.CDP,
425
- CHROME: LOG_COLORS.CHROME,
426
- CRASH: LOG_COLORS.CRASH,
427
- REPLAY: LOG_COLORS.REPLAY,
428
- NAVIGATION: LOG_COLORS.NAVIGATION,
429
- INTERACTION: LOG_COLORS.INTERACTION,
430
- GET: LOG_COLORS.SERVER,
431
- POST: LOG_COLORS.SERVER,
432
- PUT: LOG_COLORS.SERVER,
433
- DELETE: LOG_COLORS.SERVER,
434
- PATCH: LOG_COLORS.SERVER,
435
- HEAD: LOG_COLORS.SERVER,
436
- OPTIONS: LOG_COLORS.SERVER
437
- }
438
-
439
- // In compact mode, skip padding
440
- if (isCompact) {
441
- return (
442
- <Text key={log.id} wrap="truncate-end">
443
- <Text dimColor>[{timestamp}]</Text>
444
- <Text> </Text>
445
- <Text color={sourceColor} bold>
446
- [{source.charAt(0)}]
447
- </Text>
448
- {type && (
449
- <>
450
- <Text> </Text>
451
- <Text color={typeColors[type] || "#A0A0A0"}>[{type}]</Text>
452
- </>
453
- )}
454
- <Text> {message}</Text>
455
- </Text>
456
- )
457
- }
458
-
459
- // Normal mode with minimal padding
460
- // Single space after source
461
- const sourceSpacing = ""
462
-
463
- // Single space after type
464
- const typeSpacing = ""
465
-
466
- return (
467
- <Text key={log.id} wrap="truncate-end">
468
- <Text dimColor>[{timestamp}]</Text>
469
- <Text> </Text>
470
- <Text color={sourceColor} bold>
471
- [{source}]
472
- </Text>
473
- {type ? (
474
- <>
475
- <Text>{sourceSpacing} </Text>
476
- <Text color={typeColors[type] || "#A0A0A0"}>[{type}]</Text>
477
- <Text>{typeSpacing} </Text>
478
- </>
479
- ) : (
480
- <Text> </Text>
481
- )}
482
- <Text>{message}</Text>
483
- </Text>
484
- )
485
- }
486
-
487
- // Fallback for unparsed lines
488
- return (
489
- <Text key={log.id} wrap="truncate-end">
490
- {log.content}
491
- </Text>
492
- )
493
- })
513
+ visibleLogs.map((log) => (
514
+ <LogLine key={log.id} log={log} isCompact={isCompact} isVeryCompact={isVeryCompact} />
515
+ ))
494
516
  )}
495
517
  </Box>
496
518
 
@@ -524,6 +546,32 @@ export async function runTUI(options: TUIOptions): Promise<{
524
546
  let statusUpdater: ((status: string | null) => void) | null = null
525
547
  let appPortUpdater: ((port: string) => void) | null = null
526
548
 
549
+ // Wrap stdout.write to add synchronized update escape sequences
550
+ // This tells the terminal to buffer all output until the end marker
551
+ // Supported by iTerm2, Kitty, WezTerm, and other modern terminals
552
+ const originalWrite = process.stdout.write.bind(process.stdout)
553
+ const syncStart = "\x1b[?2026h" // Begin synchronized update (DECSM 2026)
554
+ const syncEnd = "\x1b[?2026l" // End synchronized update (DECRM 2026)
555
+
556
+ process.stdout.write = ((
557
+ chunk: string | Uint8Array,
558
+ encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
559
+ cb?: (err?: Error | null) => void
560
+ ): boolean => {
561
+ if (typeof chunk === "string" && chunk.length > 0) {
562
+ // Wrap output in synchronized update markers to prevent partial renders
563
+ const wrapped = syncStart + chunk + syncEnd
564
+ if (typeof encodingOrCb === "function") {
565
+ return originalWrite(wrapped, encodingOrCb)
566
+ }
567
+ return originalWrite(wrapped, encodingOrCb, cb)
568
+ }
569
+ if (typeof encodingOrCb === "function") {
570
+ return originalWrite(chunk, encodingOrCb)
571
+ }
572
+ return originalWrite(chunk, encodingOrCb, cb)
573
+ }) as typeof process.stdout.write
574
+
527
575
  const app = render(
528
576
  <TUIApp
529
577
  {...options}
@@ -1 +1 @@
1
- {"version":3,"file":"tui-interface-impl.d.ts","sourceRoot":"","sources":["../src/tui-interface-impl.tsx"],"names":[],"mappings":"AAQA,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;AAofD,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"}
1
+ {"version":3,"file":"tui-interface-impl.d.ts","sourceRoot":"","sources":["../src/tui-interface-impl.tsx"],"names":[],"mappings":"AAQA,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;AA0gBD,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,CAkED"}
@@ -1,15 +1,75 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import chalk from "chalk";
3
3
  import { createReadStream, unwatchFile, watchFile } from "fs";
4
4
  import { Box, render, Text, useInput, useStdout } from "ink";
5
5
  import Spinner from "ink-spinner";
6
- import { useEffect, useRef, useState } from "react";
6
+ import { memo, useEffect, useRef, useState } from "react";
7
7
  import { LOG_COLORS } from "./constants/log-colors.js";
8
8
  const NEXTJS_MCP_404_REGEX = /(?:\[POST\]|POST)\s+\/_next\/mcp\b[^\n]*\b404\b/i;
9
9
  // Compact ASCII logo for very small terminals
10
10
  const COMPACT_LOGO = "d3k";
11
11
  // Full ASCII logo lines as array for easier rendering
12
12
  const FULL_LOGO = [" ▐▌▄▄▄▄ █ ▄ ", " ▐▌ █ █▄▀ ", "▗▞▀▜▌▀▀▀█ █ ▀▄ ", "▝▚▄▟▌▄▄▄█ █ █ "];
13
+ // Type colors map - defined outside component to avoid recreation
14
+ const TYPE_COLORS = {
15
+ NETWORK: LOG_COLORS.NETWORK,
16
+ ERROR: LOG_COLORS.ERROR,
17
+ WARNING: LOG_COLORS.WARNING,
18
+ INFO: LOG_COLORS.INFO,
19
+ LOG: LOG_COLORS.LOG,
20
+ DEBUG: LOG_COLORS.DEBUG,
21
+ SCREENSHOT: LOG_COLORS.SCREENSHOT,
22
+ DOM: LOG_COLORS.DOM,
23
+ CDP: LOG_COLORS.CDP,
24
+ CHROME: LOG_COLORS.CHROME,
25
+ CRASH: LOG_COLORS.CRASH,
26
+ REPLAY: LOG_COLORS.REPLAY,
27
+ NAVIGATION: LOG_COLORS.NAVIGATION,
28
+ INTERACTION: LOG_COLORS.INTERACTION,
29
+ GET: LOG_COLORS.SERVER,
30
+ POST: LOG_COLORS.SERVER,
31
+ PUT: LOG_COLORS.SERVER,
32
+ DELETE: LOG_COLORS.SERVER,
33
+ PATCH: LOG_COLORS.SERVER,
34
+ HEAD: LOG_COLORS.SERVER,
35
+ OPTIONS: LOG_COLORS.SERVER
36
+ };
37
+ // Memoized log line component to prevent re-parsing on every render
38
+ const LogLine = memo(({ log, isCompact, isVeryCompact }) => {
39
+ // Parse log line to colorize different parts
40
+ const parts = log.content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/);
41
+ if (parts) {
42
+ let [, timestamp, source, type, message] = parts;
43
+ // Extract HTTP method from SERVER logs as a secondary tag
44
+ if (source === "SERVER" && !type && message) {
45
+ const methodMatch = message.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/);
46
+ if (methodMatch) {
47
+ type = methodMatch[1];
48
+ message = message.slice(type.length + 1); // Remove method from message
49
+ }
50
+ }
51
+ // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
52
+ if (message && (type === "ERROR" || type === "WARNING")) {
53
+ message = message.replace(/⚠/g, "[!]");
54
+ }
55
+ // In very compact mode, simplify the output
56
+ if (isVeryCompact) {
57
+ const shortSource = source === "BROWSER" ? "B" : "S";
58
+ const shortType = type ? type.split(".")[0].charAt(0) : "";
59
+ return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", shortSource, "]"] }), shortType && _jsxs(Text, { dimColor: true, children: ["[", shortType, "]"] }), _jsxs(Text, { children: [" ", message] })] }));
60
+ }
61
+ // Use shared color constants
62
+ const sourceColor = source === "BROWSER" ? LOG_COLORS.BROWSER : LOG_COLORS.SERVER;
63
+ // In compact mode, skip padding
64
+ if (isCompact) {
65
+ return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", timestamp, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, bold: true, children: ["[", source.charAt(0), "]"] }), type && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { color: TYPE_COLORS[type] || "#A0A0A0", children: ["[", type, "]"] })] })), _jsxs(Text, { children: [" ", message] })] }));
66
+ }
67
+ // Normal mode with minimal padding
68
+ return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", timestamp, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, bold: true, children: ["[", source, "]"] }), type ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { color: TYPE_COLORS[type] || "#A0A0A0", children: ["[", type, "]"] }), _jsx(Text, { children: " " })] })) : (_jsx(Text, { children: " " })), _jsx(Text, { children: message })] }));
69
+ }
70
+ // Fallback for unparsed lines
71
+ return _jsx(Text, { wrap: "truncate-end", children: log.content });
72
+ });
13
73
  const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, serversOnly, version, projectName, onStatusUpdate, onAppPortUpdate }) => {
14
74
  const [logs, setLogs] = useState([]);
15
75
  const [scrollOffset, setScrollOffset] = useState(0);
@@ -114,16 +174,17 @@ const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, server
114
174
  useEffect(() => {
115
175
  let logStream;
116
176
  let buffer = "";
117
- const appendLog = (line) => {
118
- if (NEXTJS_MCP_404_REGEX.test(line)) {
177
+ let pendingLogs = [];
178
+ let flushTimeout = null;
179
+ // Batch log updates to prevent excessive renders
180
+ const flushPendingLogs = () => {
181
+ if (pendingLogs.length === 0)
119
182
  return;
120
- }
121
- const newLog = {
122
- id: logIdCounter.current++,
123
- content: line
124
- };
183
+ const logsToAdd = pendingLogs;
184
+ pendingLogs = [];
185
+ flushTimeout = null;
125
186
  setLogs((prevLogs) => {
126
- const updated = [...prevLogs, newLog];
187
+ const updated = [...prevLogs, ...logsToAdd];
127
188
  // Keep only last N logs to prevent memory issues
128
189
  if (updated.length > maxLogs) {
129
190
  return updated.slice(-maxLogs);
@@ -131,11 +192,27 @@ const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, server
131
192
  return updated;
132
193
  });
133
194
  // Auto-scroll to bottom only if user is already at the bottom
134
- // Otherwise, increment scroll offset by 1, accounting for the appended log and max scroll offset
195
+ // Otherwise, increment scroll offset by count of new logs
135
196
  setScrollOffset((currentOffset) => {
136
- return currentOffset === 0 ? 0 : Math.min(maxScrollOffsetRef.current, currentOffset + 1);
197
+ return currentOffset === 0 ? 0 : Math.min(maxScrollOffsetRef.current, currentOffset + logsToAdd.length);
137
198
  });
138
199
  };
200
+ const appendLog = (line) => {
201
+ if (NEXTJS_MCP_404_REGEX.test(line)) {
202
+ return;
203
+ }
204
+ const newLog = {
205
+ id: logIdCounter.current++,
206
+ content: line
207
+ };
208
+ pendingLogs.push(newLog);
209
+ // Debounce: flush after 50ms of no new logs
210
+ // Terminal synchronized updates prevent flicker, so we can be more responsive
211
+ if (flushTimeout) {
212
+ clearTimeout(flushTimeout);
213
+ }
214
+ flushTimeout = setTimeout(flushPendingLogs, 50);
215
+ };
139
216
  // Create a read stream for the log file
140
217
  logStream = createReadStream(logFile, {
141
218
  encoding: "utf8",
@@ -182,6 +259,9 @@ const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, server
182
259
  if (logStream) {
183
260
  logStream.destroy();
184
261
  }
262
+ if (flushTimeout) {
263
+ clearTimeout(flushTimeout);
264
+ }
185
265
  unwatchFile(logFile);
186
266
  };
187
267
  }, [logFile]);
@@ -242,68 +322,7 @@ const TUIApp = ({ appPort: initialAppPort, mcpPort, logFile, commandName, server
242
322
  // Calculate the height for the logs box to ensure stable layout from first render
243
323
  // This prevents the layout from shifting as logs fill in
244
324
  const logsBoxHeight = maxVisibleLogs + 3; // +3 for header line, borders
245
- return (_jsxs(Box, { flexDirection: "column", height: termHeight, children: [isCompact ? renderCompactHeader() : renderNormalHeader(), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: logsBoxHeight, children: [!isVeryCompact && (_jsxs(Text, { color: "gray", dimColor: true, children: ["Logs (", filteredLogs.length, " total", scrollOffset > 0 && `, scrolled up ${scrollOffset} lines`, ")"] })), _jsx(Box, { flexDirection: "column", children: visibleLogs.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for logs..." })) : (visibleLogs.map((log) => {
246
- // Parse log line to colorize different parts
247
- const parts = log.content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/);
248
- if (parts) {
249
- let [, timestamp, source, type, message] = parts;
250
- // Extract HTTP method from SERVER logs as a secondary tag
251
- if (source === "SERVER" && !type && message) {
252
- const methodMatch = message.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/);
253
- if (methodMatch) {
254
- type = methodMatch[1];
255
- message = message.slice(type.length + 1); // Remove method from message
256
- }
257
- }
258
- // Replace warning emoji in ERROR/WARNING messages for consistent terminal rendering
259
- if (message && (type === "ERROR" || type === "WARNING")) {
260
- message = message.replace(/⚠/g, "[!]");
261
- }
262
- // In very compact mode, simplify the output
263
- if (isVeryCompact) {
264
- const shortSource = source === "BROWSER" ? "B" : "S";
265
- const shortType = type ? type.split(".")[0].charAt(0) : "";
266
- return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", shortSource, "]"] }), shortType && _jsxs(Text, { dimColor: true, children: ["[", shortType, "]"] }), _jsxs(Text, { children: [" ", message] })] }, log.id));
267
- }
268
- // Use shared color constants
269
- const sourceColor = source === "BROWSER" ? LOG_COLORS.BROWSER : LOG_COLORS.SERVER;
270
- const typeColors = {
271
- NETWORK: LOG_COLORS.NETWORK,
272
- ERROR: LOG_COLORS.ERROR,
273
- WARNING: LOG_COLORS.WARNING,
274
- INFO: LOG_COLORS.INFO,
275
- LOG: LOG_COLORS.LOG,
276
- DEBUG: LOG_COLORS.DEBUG,
277
- SCREENSHOT: LOG_COLORS.SCREENSHOT,
278
- DOM: LOG_COLORS.DOM,
279
- CDP: LOG_COLORS.CDP,
280
- CHROME: LOG_COLORS.CHROME,
281
- CRASH: LOG_COLORS.CRASH,
282
- REPLAY: LOG_COLORS.REPLAY,
283
- NAVIGATION: LOG_COLORS.NAVIGATION,
284
- INTERACTION: LOG_COLORS.INTERACTION,
285
- GET: LOG_COLORS.SERVER,
286
- POST: LOG_COLORS.SERVER,
287
- PUT: LOG_COLORS.SERVER,
288
- DELETE: LOG_COLORS.SERVER,
289
- PATCH: LOG_COLORS.SERVER,
290
- HEAD: LOG_COLORS.SERVER,
291
- OPTIONS: LOG_COLORS.SERVER
292
- };
293
- // In compact mode, skip padding
294
- if (isCompact) {
295
- return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", timestamp, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, bold: true, children: ["[", source.charAt(0), "]"] }), type && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { color: typeColors[type] || "#A0A0A0", children: ["[", type, "]"] })] })), _jsxs(Text, { children: [" ", message] })] }, log.id));
296
- }
297
- // Normal mode with minimal padding
298
- // Single space after source
299
- const sourceSpacing = "";
300
- // Single space after type
301
- const typeSpacing = "";
302
- return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { dimColor: true, children: ["[", timestamp, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, bold: true, children: ["[", source, "]"] }), type ? (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [sourceSpacing, " "] }), _jsxs(Text, { color: typeColors[type] || "#A0A0A0", children: ["[", type, "]"] }), _jsxs(Text, { children: [typeSpacing, " "] })] })) : (_jsx(Text, { children: " " })), _jsx(Text, { children: message })] }, log.id));
303
- }
304
- // Fallback for unparsed lines
305
- return (_jsx(Text, { wrap: "truncate-end", children: log.content }, log.id));
306
- })) }), !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
325
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, children: [isCompact ? renderCompactHeader() : renderNormalHeader(), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: logsBoxHeight, children: [!isVeryCompact && (_jsxs(Text, { color: "gray", dimColor: true, children: ["Logs (", filteredLogs.length, " total", scrollOffset > 0 && `, scrolled up ${scrollOffset} lines`, ")"] })), _jsx(Box, { flexDirection: "column", children: visibleLogs.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for logs..." })) : (visibleLogs.map((log) => (_jsx(LogLine, { log: log, isCompact: isCompact, isVeryCompact: isVeryCompact }, log.id)))) }), !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
307
326
  ? logFile.split("/").slice(-2, -1)[0] || "logs" // Just show directory name
308
327
  : logFile.replace(process.env.HOME || "", "~")] }), _jsx(Text, { color: "#A18CE5", children: ctrlCMessage })] })] }));
309
328
  };
@@ -312,6 +331,26 @@ export async function runTUI(options) {
312
331
  try {
313
332
  let statusUpdater = null;
314
333
  let appPortUpdater = null;
334
+ // Wrap stdout.write to add synchronized update escape sequences
335
+ // This tells the terminal to buffer all output until the end marker
336
+ // Supported by iTerm2, Kitty, WezTerm, and other modern terminals
337
+ const originalWrite = process.stdout.write.bind(process.stdout);
338
+ const syncStart = "\x1b[?2026h"; // Begin synchronized update (DECSM 2026)
339
+ const syncEnd = "\x1b[?2026l"; // End synchronized update (DECRM 2026)
340
+ process.stdout.write = ((chunk, encodingOrCb, cb) => {
341
+ if (typeof chunk === "string" && chunk.length > 0) {
342
+ // Wrap output in synchronized update markers to prevent partial renders
343
+ const wrapped = syncStart + chunk + syncEnd;
344
+ if (typeof encodingOrCb === "function") {
345
+ return originalWrite(wrapped, encodingOrCb);
346
+ }
347
+ return originalWrite(wrapped, encodingOrCb, cb);
348
+ }
349
+ if (typeof encodingOrCb === "function") {
350
+ return originalWrite(chunk, encodingOrCb);
351
+ }
352
+ return originalWrite(chunk, encodingOrCb, cb);
353
+ });
315
354
  const app = render(_jsx(TUIApp, { ...options, onStatusUpdate: (fn) => {
316
355
  statusUpdater = fn;
317
356
  }, onAppPortUpdate: (fn) => {