dev3000 0.0.185 → 0.0.187

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 (237) hide show
  1. package/README.md +102 -13
  2. package/bin/dev3000 +1 -2
  3. package/dist/cdp-monitor.d.ts +7 -12
  4. package/dist/cdp-monitor.d.ts.map +1 -1
  5. package/dist/cdp-monitor.js +86 -170
  6. package/dist/cdp-monitor.js.map +1 -1
  7. package/dist/cli.d.ts +1 -2
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +1 -1369
  10. package/dist/cli.js.map +1 -1
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/portless.d.ts +10 -0
  16. package/dist/portless.d.ts.map +1 -1
  17. package/dist/portless.js +44 -1
  18. package/dist/portless.js.map +1 -1
  19. package/dist/runtime/bridge.d.ts +26 -0
  20. package/dist/runtime/bridge.d.ts.map +1 -0
  21. package/dist/runtime/bridge.js +340 -0
  22. package/dist/runtime/bridge.js.map +1 -0
  23. package/dist/runtime/chrome-isolation.d.ts +16 -0
  24. package/dist/runtime/chrome-isolation.d.ts.map +1 -0
  25. package/dist/runtime/chrome-isolation.js +97 -0
  26. package/dist/runtime/chrome-isolation.js.map +1 -0
  27. package/dist/runtime/cli.js +116 -26
  28. package/dist/runtime/cli.js.map +1 -1
  29. package/dist/runtime/event-views.d.ts +21 -0
  30. package/dist/runtime/event-views.d.ts.map +1 -0
  31. package/dist/runtime/event-views.js +53 -0
  32. package/dist/runtime/event-views.js.map +1 -0
  33. package/dist/runtime/harness.d.ts +16 -0
  34. package/dist/runtime/harness.d.ts.map +1 -0
  35. package/dist/runtime/harness.js +68 -0
  36. package/dist/runtime/harness.js.map +1 -0
  37. package/dist/runtime/local-proxy.d.ts +12 -0
  38. package/dist/runtime/local-proxy.d.ts.map +1 -0
  39. package/dist/runtime/local-proxy.js +114 -0
  40. package/dist/runtime/local-proxy.js.map +1 -0
  41. package/dist/runtime/runtime.d.ts +14 -0
  42. package/dist/runtime/runtime.d.ts.map +1 -1
  43. package/dist/runtime/runtime.js +82 -21
  44. package/dist/runtime/runtime.js.map +1 -1
  45. package/dist/runtime/sandbox-agent.d.ts +6 -0
  46. package/dist/runtime/sandbox-agent.d.ts.map +1 -0
  47. package/dist/runtime/sandbox-agent.js +35 -0
  48. package/dist/runtime/sandbox-agent.js.map +1 -0
  49. package/dist/runtime/sandbox-command.d.ts +9 -0
  50. package/dist/runtime/sandbox-command.d.ts.map +1 -0
  51. package/dist/runtime/sandbox-command.js +101 -0
  52. package/dist/runtime/sandbox-command.js.map +1 -0
  53. package/dist/runtime/sandbox-proxy.d.ts +11 -0
  54. package/dist/runtime/sandbox-proxy.d.ts.map +1 -0
  55. package/dist/runtime/sandbox-proxy.js +123 -0
  56. package/dist/runtime/sandbox-proxy.js.map +1 -0
  57. package/dist/runtime/sandbox-sessions.d.ts +31 -0
  58. package/dist/runtime/sandbox-sessions.d.ts.map +1 -0
  59. package/dist/runtime/sandbox-sessions.js +102 -0
  60. package/dist/runtime/sandbox-sessions.js.map +1 -0
  61. package/dist/runtime/sandbox.d.ts +21 -0
  62. package/dist/runtime/sandbox.d.ts.map +1 -0
  63. package/dist/runtime/sandbox.js +481 -0
  64. package/dist/runtime/sandbox.js.map +1 -0
  65. package/dist/utils/agent-browser.d.ts +2 -120
  66. package/dist/utils/agent-browser.d.ts.map +1 -1
  67. package/dist/utils/agent-browser.js +13 -376
  68. package/dist/utils/agent-browser.js.map +1 -1
  69. package/dist/utils/project-name.d.ts +11 -1
  70. package/dist/utils/project-name.d.ts.map +1 -1
  71. package/dist/utils/project-name.js +14 -1
  72. package/dist/utils/project-name.js.map +1 -1
  73. package/package.json +13 -22
  74. package/skills/d3k/SKILL.md +23 -1
  75. package/dist/commands/crawl.d.ts +0 -12
  76. package/dist/commands/crawl.d.ts.map +0 -1
  77. package/dist/commands/crawl.js +0 -101
  78. package/dist/commands/crawl.js.map +0 -1
  79. package/dist/commands/errors.d.ts +0 -14
  80. package/dist/commands/errors.d.ts.map +0 -1
  81. package/dist/commands/errors.js +0 -246
  82. package/dist/commands/errors.js.map +0 -1
  83. package/dist/commands/fix.d.ts +0 -13
  84. package/dist/commands/fix.d.ts.map +0 -1
  85. package/dist/commands/fix.js +0 -219
  86. package/dist/commands/fix.js.map +0 -1
  87. package/dist/commands/logs.d.ts +0 -13
  88. package/dist/commands/logs.d.ts.map +0 -1
  89. package/dist/commands/logs.js +0 -146
  90. package/dist/commands/logs.js.map +0 -1
  91. package/dist/commands/resume.d.ts +0 -11
  92. package/dist/commands/resume.d.ts.map +0 -1
  93. package/dist/commands/resume.js +0 -75
  94. package/dist/commands/resume.js.map +0 -1
  95. package/dist/commands/status.d.ts +0 -21
  96. package/dist/commands/status.d.ts.map +0 -1
  97. package/dist/commands/status.js +0 -44
  98. package/dist/commands/status.js.map +0 -1
  99. package/dist/components/AgentSelector.d.ts +0 -12
  100. package/dist/components/AgentSelector.d.ts.map +0 -1
  101. package/dist/components/AgentSelector.js +0 -99
  102. package/dist/components/AgentSelector.js.map +0 -1
  103. package/dist/components/PackageSelector.d.ts +0 -13
  104. package/dist/components/PackageSelector.d.ts.map +0 -1
  105. package/dist/components/PackageSelector.js +0 -79
  106. package/dist/components/PackageSelector.js.map +0 -1
  107. package/dist/constants/log-colors.d.ts +0 -43
  108. package/dist/constants/log-colors.d.ts.map +0 -1
  109. package/dist/constants/log-colors.js +0 -59
  110. package/dist/constants/log-colors.js.map +0 -1
  111. package/dist/dev-environment.d.ts +0 -187
  112. package/dist/dev-environment.d.ts.map +0 -1
  113. package/dist/dev-environment.js +0 -2248
  114. package/dist/dev-environment.js.map +0 -1
  115. package/dist/screencast-manager.d.ts +0 -78
  116. package/dist/screencast-manager.d.ts.map +0 -1
  117. package/dist/screencast-manager.js +0 -529
  118. package/dist/screencast-manager.js.map +0 -1
  119. package/dist/services/parsers/error-detectors/base.d.ts +0 -24
  120. package/dist/services/parsers/error-detectors/base.d.ts.map +0 -1
  121. package/dist/services/parsers/error-detectors/base.js +0 -44
  122. package/dist/services/parsers/error-detectors/base.js.map +0 -1
  123. package/dist/services/parsers/error-detectors/index.d.ts +0 -6
  124. package/dist/services/parsers/error-detectors/index.d.ts.map +0 -1
  125. package/dist/services/parsers/error-detectors/index.js +0 -6
  126. package/dist/services/parsers/error-detectors/index.js.map +0 -1
  127. package/dist/services/parsers/error-detectors/nextjs.d.ts +0 -9
  128. package/dist/services/parsers/error-detectors/nextjs.d.ts.map +0 -1
  129. package/dist/services/parsers/error-detectors/nextjs.js +0 -52
  130. package/dist/services/parsers/error-detectors/nextjs.js.map +0 -1
  131. package/dist/services/parsers/index.d.ts +0 -9
  132. package/dist/services/parsers/index.d.ts.map +0 -1
  133. package/dist/services/parsers/index.js +0 -10
  134. package/dist/services/parsers/index.js.map +0 -1
  135. package/dist/services/parsers/log-parsers/base.d.ts +0 -26
  136. package/dist/services/parsers/log-parsers/base.d.ts.map +0 -1
  137. package/dist/services/parsers/log-parsers/base.js +0 -6
  138. package/dist/services/parsers/log-parsers/base.js.map +0 -1
  139. package/dist/services/parsers/log-parsers/index.d.ts +0 -6
  140. package/dist/services/parsers/log-parsers/index.d.ts.map +0 -1
  141. package/dist/services/parsers/log-parsers/index.js +0 -5
  142. package/dist/services/parsers/log-parsers/index.js.map +0 -1
  143. package/dist/services/parsers/log-parsers/standard.d.ts +0 -9
  144. package/dist/services/parsers/log-parsers/standard.d.ts.map +0 -1
  145. package/dist/services/parsers/log-parsers/standard.js +0 -17
  146. package/dist/services/parsers/log-parsers/standard.js.map +0 -1
  147. package/dist/services/parsers/output-processor.d.ts +0 -39
  148. package/dist/services/parsers/output-processor.d.ts.map +0 -1
  149. package/dist/services/parsers/output-processor.js +0 -54
  150. package/dist/services/parsers/output-processor.js.map +0 -1
  151. package/dist/skills/d3k/internal-skill.md +0 -32
  152. package/dist/skills/index.d.ts +0 -54
  153. package/dist/skills/index.d.ts.map +0 -1
  154. package/dist/skills/index.js +0 -214
  155. package/dist/skills/index.js.map +0 -1
  156. package/dist/src/tui-interface-impl.tsx +0 -644
  157. package/dist/tui-interface-impl.d.ts +0 -30
  158. package/dist/tui-interface-impl.d.ts.map +0 -1
  159. package/dist/tui-interface-impl.js +0 -403
  160. package/dist/tui-interface-impl.js.map +0 -1
  161. package/dist/tui-interface-opentui.d.ts +0 -33
  162. package/dist/tui-interface-opentui.d.ts.map +0 -1
  163. package/dist/tui-interface-opentui.js +0 -1128
  164. package/dist/tui-interface-opentui.js.map +0 -1
  165. package/dist/tui-interface.d.ts +0 -38
  166. package/dist/tui-interface.d.ts.map +0 -1
  167. package/dist/tui-interface.js +0 -84
  168. package/dist/tui-interface.js.map +0 -1
  169. package/dist/utils/agent-detection.d.ts +0 -17
  170. package/dist/utils/agent-detection.d.ts.map +0 -1
  171. package/dist/utils/agent-detection.js +0 -81
  172. package/dist/utils/agent-detection.js.map +0 -1
  173. package/dist/utils/agent-selection.d.ts +0 -25
  174. package/dist/utils/agent-selection.d.ts.map +0 -1
  175. package/dist/utils/agent-selection.js +0 -73
  176. package/dist/utils/agent-selection.js.map +0 -1
  177. package/dist/utils/browser-command-argv.d.ts +0 -19
  178. package/dist/utils/browser-command-argv.d.ts.map +0 -1
  179. package/dist/utils/browser-command-argv.js +0 -149
  180. package/dist/utils/browser-command-argv.js.map +0 -1
  181. package/dist/utils/build-version.d.ts +0 -2
  182. package/dist/utils/build-version.d.ts.map +0 -1
  183. package/dist/utils/build-version.js +0 -8
  184. package/dist/utils/build-version.js.map +0 -1
  185. package/dist/utils/cli-options.d.ts +0 -5
  186. package/dist/utils/cli-options.d.ts.map +0 -1
  187. package/dist/utils/cli-options.js +0 -36
  188. package/dist/utils/cli-options.js.map +0 -1
  189. package/dist/utils/d3k-dir.d.ts +0 -3
  190. package/dist/utils/d3k-dir.d.ts.map +0 -1
  191. package/dist/utils/d3k-dir.js +0 -12
  192. package/dist/utils/d3k-dir.js.map +0 -1
  193. package/dist/utils/d3k-skill-installer.d.ts +0 -4
  194. package/dist/utils/d3k-skill-installer.d.ts.map +0 -1
  195. package/dist/utils/d3k-skill-installer.js +0 -30
  196. package/dist/utils/d3k-skill-installer.js.map +0 -1
  197. package/dist/utils/log-filename.d.ts +0 -30
  198. package/dist/utils/log-filename.d.ts.map +0 -1
  199. package/dist/utils/log-filename.js +0 -60
  200. package/dist/utils/log-filename.js.map +0 -1
  201. package/dist/utils/project-detector.d.ts +0 -25
  202. package/dist/utils/project-detector.d.ts.map +0 -1
  203. package/dist/utils/project-detector.js +0 -153
  204. package/dist/utils/project-detector.js.map +0 -1
  205. package/dist/utils/project-metadata.d.ts +0 -4
  206. package/dist/utils/project-metadata.d.ts.map +0 -1
  207. package/dist/utils/project-metadata.js +0 -48
  208. package/dist/utils/project-metadata.js.map +0 -1
  209. package/dist/utils/repo-validate.d.ts +0 -2
  210. package/dist/utils/repo-validate.d.ts.map +0 -1
  211. package/dist/utils/repo-validate.js +0 -10
  212. package/dist/utils/repo-validate.js.map +0 -1
  213. package/dist/utils/session.d.ts +0 -19
  214. package/dist/utils/session.d.ts.map +0 -1
  215. package/dist/utils/session.js +0 -65
  216. package/dist/utils/session.js.map +0 -1
  217. package/dist/utils/skill-installer.d.ts +0 -65
  218. package/dist/utils/skill-installer.d.ts.map +0 -1
  219. package/dist/utils/skill-installer.js +0 -265
  220. package/dist/utils/skill-installer.js.map +0 -1
  221. package/dist/utils/timestamp.d.ts +0 -20
  222. package/dist/utils/timestamp.d.ts.map +0 -1
  223. package/dist/utils/timestamp.js +0 -57
  224. package/dist/utils/timestamp.js.map +0 -1
  225. package/dist/utils/tmux-helpers.d.ts +0 -36
  226. package/dist/utils/tmux-helpers.d.ts.map +0 -1
  227. package/dist/utils/tmux-helpers.js +0 -118
  228. package/dist/utils/tmux-helpers.js.map +0 -1
  229. package/dist/utils/user-config.d.ts +0 -12
  230. package/dist/utils/user-config.d.ts.map +0 -1
  231. package/dist/utils/user-config.js +0 -46
  232. package/dist/utils/user-config.js.map +0 -1
  233. package/dist/utils/version-check.d.ts +0 -52
  234. package/dist/utils/version-check.d.ts.map +0 -1
  235. package/dist/utils/version-check.js +0 -304
  236. package/dist/utils/version-check.js.map +0 -1
  237. package/src/tui-interface-impl.tsx +0 -644
@@ -1,1128 +0,0 @@
1
- import { BoxRenderable, bold, createCliRenderer, cyan, dim, fg, green, MacOSScrollAccel, RGBA, ScrollBoxRenderable, StyledText, TextRenderable, t, yellow } from "@opentui/core";
2
- import { spawnSync } from "child_process";
3
- import { appendFileSync, createReadStream, mkdirSync, unwatchFile, watchFile, writeFileSync } from "fs";
4
- import { tmpdir } from "os";
5
- import { join } from "path";
6
- import { LOG_COLORS } from "./constants/log-colors.js";
7
- import { formatTimeDelta, parseTimestampToMs } from "./utils/timestamp.js";
8
- // Compact ASCII logo for very small terminals
9
- const COMPACT_LOGO = "d3k";
10
- // Full ASCII logo lines
11
- const FULL_LOGO = [" ▐▌▄▄▄▄ █ ▄ ", " ▐▌ █ █▄▀ ", "▗▞▀▜▌▀▀▀█ █ ▀▄ ", "▝▚▄▟▌▄▄▄█ █ █ "];
12
- // Brand purple color
13
- const BRAND_PURPLE = "#A18CE5";
14
- // Type colors map
15
- const TYPE_COLORS = {
16
- NETWORK: LOG_COLORS.NETWORK,
17
- ERROR: LOG_COLORS.ERROR,
18
- WARNING: LOG_COLORS.WARNING,
19
- INFO: LOG_COLORS.INFO,
20
- LOG: LOG_COLORS.LOG,
21
- DEBUG: LOG_COLORS.DEBUG,
22
- SCREENSHOT: LOG_COLORS.SCREENSHOT,
23
- DOM: LOG_COLORS.DOM,
24
- CDP: LOG_COLORS.CDP,
25
- CHROME: LOG_COLORS.CHROME,
26
- CRASH: LOG_COLORS.CRASH,
27
- EXIT: LOG_COLORS.EXIT,
28
- REPLAY: LOG_COLORS.REPLAY,
29
- NAVIGATION: LOG_COLORS.NAVIGATION,
30
- INTERACTION: LOG_COLORS.INTERACTION,
31
- GET: LOG_COLORS.SERVER,
32
- POST: LOG_COLORS.SERVER,
33
- PUT: LOG_COLORS.SERVER,
34
- DELETE: LOG_COLORS.SERVER,
35
- PATCH: LOG_COLORS.SERVER,
36
- HEAD: LOG_COLORS.SERVER,
37
- OPTIONS: LOG_COLORS.SERVER
38
- };
39
- // Color for delta timestamps
40
- const DELTA_COLOR = "#FFD700"; // Gold/yellow for visibility
41
- // Helper to format log line with StyledText
42
- function formatLogLine(content, isCompact, baseTimestampMs, isBaseLog) {
43
- const parts = content.match(/^\[(.*?)\] \[(.*?)\] (?:\[(.*?)\] )?(.*)$/);
44
- if (parts) {
45
- let [, timestamp, source, type, message] = parts;
46
- // Extract HTTP method from SERVER logs
47
- if (source === "SERVER" && !type && message) {
48
- const methodMatch = message.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s/);
49
- if (methodMatch) {
50
- type = methodMatch[1];
51
- message = message.slice(type.length + 1);
52
- }
53
- }
54
- // Replace warning emoji
55
- if (message && (type === "ERROR" || type === "WARNING")) {
56
- message = message.replace(/\u26A0/g, "[!]");
57
- }
58
- const sourceColor = source === "BROWSER" ? LOG_COLORS.BROWSER : LOG_COLORS.SERVER;
59
- const typeColor = TYPE_COLORS[type] || "#A0A0A0";
60
- // Format timestamp - show delta if base timestamp is set
61
- let displayTimestamp = timestamp;
62
- // Track which style to use: 0 = dim (normal), 1 = delta color, 2 = delta bold (base log)
63
- let timestampMode = 0;
64
- if (baseTimestampMs !== undefined) {
65
- const currentMs = parseTimestampToMs(timestamp);
66
- if (currentMs !== null) {
67
- if (isBaseLog) {
68
- // The base log shows "BASE" indicator (padded to match delta width)
69
- displayTimestamp = isCompact ? "BASE" : " BASE ";
70
- timestampMode = 2;
71
- }
72
- else {
73
- // Other logs show delta from base
74
- const delta = formatTimeDelta(currentMs - baseTimestampMs);
75
- displayTimestamp = isCompact ? delta : `${delta.padStart(10)} `;
76
- timestampMode = 1;
77
- }
78
- }
79
- }
80
- // Always show full timestamp (HH:MM:SS.mmm) - no truncation
81
- // Build styled timestamp based on mode
82
- const styledTimestamp = timestampMode === 2
83
- ? bold(fg(DELTA_COLOR)(displayTimestamp))
84
- : timestampMode === 1
85
- ? fg(DELTA_COLOR)(displayTimestamp)
86
- : dim(displayTimestamp);
87
- // Add leading space to account for scroll indicator overlap
88
- const pad = " ";
89
- if (isCompact) {
90
- const sourceChar = source.charAt(0);
91
- if (type) {
92
- const typeChar = type.charAt(0);
93
- // Compact mode: single char tags with space after timestamp
94
- return t `${pad}${styledTimestamp} ${fg(sourceColor)(`[${sourceChar}]`)}${fg(typeColor)(`[${typeChar}]`)} ${message || ""}`;
95
- }
96
- return t `${pad}${styledTimestamp} ${fg(sourceColor)(`[${sourceChar}]`)} ${message || ""}`;
97
- }
98
- if (type) {
99
- return t `${pad}${styledTimestamp} ${bold(fg(sourceColor)(`[${source}]`))} ${fg(typeColor)(`[${type}]`)} ${message || ""}`;
100
- }
101
- return t `${pad}${styledTimestamp} ${bold(fg(sourceColor)(`[${source}]`))} ${message || ""}`;
102
- }
103
- // Fallback for unparseable lines - still add padding
104
- return t ` ${content}`;
105
- }
106
- export function applySelectionToStyledText(content, bgColor, fgColor) {
107
- return new StyledText(content.chunks.map((chunk) => ({
108
- ...chunk,
109
- bg: bgColor,
110
- fg: fgColor ?? chunk.fg
111
- })));
112
- }
113
- class D3kTUI {
114
- renderer = null;
115
- options;
116
- logs = [];
117
- logIdCounter = 0;
118
- clearFromLogId = 0;
119
- maxLogs = 1000;
120
- // UI Components
121
- headerBox = null;
122
- logsScrollBox = null;
123
- logsContainer = null;
124
- statusText = null;
125
- // State
126
- appPort;
127
- appUrl;
128
- useHttps;
129
- portConfirmed = false;
130
- updateInfo;
131
- initStatus = null;
132
- ctrlCMessage = "";
133
- ctrlCTimeout = null;
134
- // Base timestamp for delta display (set via keyboard, not mouse click)
135
- baseTimestampMs = undefined;
136
- baseLogId = undefined;
137
- // Debug mode for tracking selection/focus issues
138
- debugMode = false;
139
- lastFocusedId = null;
140
- debugLogFile = join(process.env.HOME || tmpdir(), ".d3k", "tui-debug.log");
141
- selectionDebugEnabled = process.env.D3K_TUI_SELECTION_DEBUG !== "0";
142
- selectionDebugLogFile = join(process.env.HOME || tmpdir(), ".d3k", "tui-selection-debug.log");
143
- lastCopiedSelection = null;
144
- selectionCopyHandler = null;
145
- isCompact = false;
146
- isVeryCompact = false;
147
- isRebuilding = false;
148
- logContentWidth = 80;
149
- logRenderables = new Map();
150
- logBaseContent = new Map();
151
- logSelection = null;
152
- logSelectionBackground = RGBA.fromInts(70, 130, 180);
153
- logSelectionForeground = RGBA.fromInts(255, 255, 255);
154
- constructor(options) {
155
- this.options = options;
156
- this.appPort = options.appPort;
157
- this.appUrl = options.appUrl || null;
158
- this.useHttps = options.useHttps || false;
159
- this.updateInfo = options.updateInfo || null;
160
- }
161
- async start() {
162
- const config = {
163
- useMouse: true,
164
- exitOnCtrlC: false,
165
- // @opentui/core 0.4 replaced `useAlternateScreen: true` with `screenMode`.
166
- screenMode: "alternate-screen",
167
- targetFps: 20,
168
- maxFps: 30
169
- };
170
- // Filter out OpenTUI's terminal detection messages from stdout and stderr
171
- // These messages like "info(terminal): Terminal detected..." interfere with TUI rendering
172
- const createFilteredWrite = (originalWrite) => {
173
- return function (chunk, encodingOrCallback, callback) {
174
- const str = typeof chunk === "string" ? chunk : chunk.toString();
175
- if (str.includes("info(terminal)") || str.includes("Terminal detected")) {
176
- return true; // Silently ignore
177
- }
178
- if (typeof encodingOrCallback === "function") {
179
- return originalWrite(chunk, encodingOrCallback);
180
- }
181
- return originalWrite(chunk, encodingOrCallback, callback);
182
- };
183
- };
184
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
185
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
186
- process.stdout.write = createFilteredWrite(originalStdoutWrite);
187
- process.stderr.write = createFilteredWrite(originalStderrWrite);
188
- this.initializeSelectionDebugLog();
189
- this.renderer = await createCliRenderer(config);
190
- // Setup UI after renderer is created
191
- this.setupUI();
192
- this.setupKeyboardHandlers();
193
- this.setupFocusTracking();
194
- this.setupSelectionCopy();
195
- this.renderer.start();
196
- // Delay log file watcher start to allow layout to compute valid positions
197
- // Without this delay, initial logs get NaN X positions and selection fails
198
- setTimeout(() => {
199
- this.startLogFileWatcher();
200
- }, 100);
201
- // Force a redraw after start to ensure borders render correctly
202
- process.stdout.write("\x1b[2J\x1b[H");
203
- this.renderer.requestRender();
204
- setTimeout(() => {
205
- this.logSelectionDebug(`[START] renderer=${this.renderer?.width}x${this.renderer?.height} logContentWidth=${this.logContentWidth}`);
206
- this.logLogPaneMetrics("post-start");
207
- }, 150);
208
- return {
209
- app: { unmount: () => this.shutdown() },
210
- updateStatus: (status) => this.setStatus(status),
211
- updateAppPort: (port) => this.setAppPort(port),
212
- updateAppUrl: (url) => this.setAppUrl(url),
213
- updateUpdateInfo: (info) => this.setUpdateInfo(info),
214
- updateUseHttps: (useHttps) => this.setUseHttps(useHttps)
215
- };
216
- }
217
- setupUI() {
218
- if (!this.renderer)
219
- return;
220
- const { width, height } = this.renderer;
221
- const isCompact = width < 80;
222
- const isVeryCompact = width < 60 || height < 15;
223
- this.logContentWidth = Math.max(20, width - 6);
224
- this.isCompact = isCompact;
225
- this.isVeryCompact = isVeryCompact;
226
- // Main container
227
- const mainContainer = new BoxRenderable(this.renderer, {
228
- id: "main",
229
- width: "100%",
230
- height: "100%",
231
- flexDirection: "column"
232
- });
233
- this.renderer.root.add(mainContainer);
234
- // Header
235
- this.headerBox = this.createHeader(isCompact, isVeryCompact);
236
- mainContainer.add(this.headerBox);
237
- // Logs section (scrollable)
238
- const logsSection = new BoxRenderable(this.renderer, {
239
- id: "logs-section",
240
- flexGrow: 1,
241
- flexDirection: "column",
242
- border: true,
243
- borderStyle: "single",
244
- borderColor: "#808080",
245
- paddingRight: 1
246
- });
247
- mainContainer.add(logsSection);
248
- // Scrollable logs container
249
- this.logsScrollBox = new ScrollBoxRenderable(this.renderer, {
250
- id: "logs-scroll",
251
- flexGrow: 1,
252
- scrollY: true,
253
- scrollX: false,
254
- stickyScroll: true,
255
- stickyStart: "bottom",
256
- scrollAcceleration: new MacOSScrollAccel({ maxMultiplier: 8 }),
257
- viewportCulling: false, // Disable culling to ensure all items are in hit grid
258
- onMouse: (event) => {
259
- // Log ALL mouse events to understand the flow
260
- if (event.type === "down" || event.type === "up") {
261
- const target = event.target;
262
- const targetId = target?.id || "(none)";
263
- const targetX = target?.x ?? "?";
264
- const targetY = target?.y ?? "?";
265
- const targetW = target?.width ?? "?";
266
- const targetH = target?.height ?? "?";
267
- this.debugLog(`[MOUSE_${event.type.toUpperCase()}] screen(${event.x},${event.y}) target=${targetId} pos(${targetX},${targetY}) size(${targetW}x${targetH}) selectable=${target?.selectable} hasSelection=${this.renderer?.hasSelection}`);
268
- this.logSelectionDebug(`[MOUSE_${event.type.toUpperCase()}] screen(${event.x},${event.y}) target=${targetId} pos(${targetX},${targetY}) size(${targetW}x${targetH}) selectable=${target?.selectable} ${this.describeSelection()}`);
269
- }
270
- },
271
- onMouseDown: (event) => {
272
- if (event.button === 0) {
273
- const logLine = this.getSelectionLogLine(event);
274
- if (logLine) {
275
- this.logSelectionDebug(`[START_SELECTION] target=${logLine.id} pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height}) screen(${event.x},${event.y})`);
276
- this.beginLogSelection(logLine.id, event.x, event.y);
277
- this.logSelectionDebug(`[START_SELECTION_AFTER] ${this.describeSelection()}`);
278
- }
279
- }
280
- const target = event.target;
281
- const targetId = target?.id || "(none)";
282
- const selection = this.renderer?.getSelection();
283
- this.debugLog(`[MOUSE_DOWN_CB] x=${event.x}, y=${event.y}, target=${targetId}, hasSelection=${this.renderer?.hasSelection}, isDragging=${selection?.isDragging}`);
284
- this.logSelectionDebug(`[MOUSE_DOWN_CB] x=${event.x}, y=${event.y}, target=${targetId} ${this.describeSelection()}`);
285
- },
286
- onMouseUp: (event) => {
287
- if (event.button === 0) {
288
- const logLine = this.getSelectionLogLine(event);
289
- this.finishLogSelection(logLine?.id ?? null, event.x, event.y);
290
- this.logSelectionDebug(`[FINISH_SELECTION] target=${logLine?.id || "(none)"} screen(${event.x},${event.y}) ${this.describeSelection()}`);
291
- }
292
- const selectedText = this.getSelectedLogText() || "(none)";
293
- this.debugLog(`[MOUSE_UP] x=${event.x}, y=${event.y}, hasSelection=${Boolean(this.logSelection)}, text="${selectedText.slice(0, 30)}"`);
294
- this.logSelectionDebug(`[MOUSE_UP] x=${event.x}, y=${event.y}, text="${selectedText.slice(0, 60)}" ${this.describeSelection()}`);
295
- },
296
- onMouseDrag: (event) => {
297
- if (this.logSelection) {
298
- const logLine = this.getSelectionLogLine(event);
299
- this.updateLogSelection(logLine?.id ?? null, event.x, event.y);
300
- this.logSelectionDebug(`[UPDATE_SELECTION] target=${logLine?.id || "(none)"} screen(${event.x},${event.y}) ${this.describeSelection()}`);
301
- }
302
- const target = event.target;
303
- const targetId = target?.id || "(none)";
304
- this.debugLog(`[MOUSE_DRAG] x=${event.x}, y=${event.y}, target=${targetId}, isDragging=${event.isDragging}`);
305
- this.logSelectionDebug(`[MOUSE_DRAG] x=${event.x}, y=${event.y}, target=${targetId}, eventDragging=${event.isDragging} ${this.describeSelection()}`);
306
- }
307
- });
308
- this.logsScrollBox.content.flexDirection = "column";
309
- this.logsScrollBox.content.left = 0;
310
- this.logsScrollBox.content.top = 0;
311
- this.logsScrollBox.content.width = this.logContentWidth;
312
- logsSection.add(this.logsScrollBox);
313
- // Track focus changes on scroll box
314
- this.logsScrollBox.on("focused", () => {
315
- this.debugLog(`[FOCUS] logsScrollBox focused`);
316
- this.logSelectionDebug("[FOCUS] logsScrollBox focused");
317
- });
318
- this.logsScrollBox.on("blurred", () => {
319
- this.debugLog(`[FOCUS] logsScrollBox blurred`);
320
- this.logSelectionDebug("[FOCUS] logsScrollBox blurred");
321
- });
322
- // Container for log lines inside scroll box - provides valid X positions
323
- this.logsContainer = new BoxRenderable(this.renderer, {
324
- id: "logs-content",
325
- flexDirection: "column",
326
- width: this.logContentWidth
327
- });
328
- this.logsScrollBox.add(this.logsContainer);
329
- // Bottom status line - show "agent has access" hint in both modes
330
- const statusLine = new BoxRenderable(this.renderer, {
331
- id: "status-line",
332
- height: 1,
333
- flexDirection: "row",
334
- justifyContent: "space-between",
335
- paddingLeft: 1,
336
- paddingRight: 1
337
- });
338
- mainContainer.add(statusLine);
339
- // Help text (left side) - always show agent access hint
340
- const helpText = new TextRenderable(this.renderer, {
341
- id: "help-text",
342
- content: t `${dim("<- agent has access to ↑")}`
343
- });
344
- statusLine.add(helpText);
345
- // Status info (right side) - only in non-compact mode
346
- if (!isCompact) {
347
- this.statusText = new TextRenderable(this.renderer, {
348
- id: "status-text",
349
- content: this.buildStatusContent(false),
350
- marginLeft: "auto"
351
- });
352
- statusLine.add(this.statusText);
353
- }
354
- else {
355
- this.statusText = null;
356
- }
357
- // Handle resize - only rebuild when compact mode changes
358
- this.renderer.root.onSizeChange = () => {
359
- if (!this.renderer || this.isRebuilding)
360
- return;
361
- this.logContentWidth = Math.max(20, this.renderer.width - 6);
362
- const nextCompact = this.renderer.width < 80;
363
- const nextVeryCompact = this.renderer.width < 60 || this.renderer.height < 15;
364
- if (nextCompact !== this.isCompact || nextVeryCompact !== this.isVeryCompact) {
365
- this.rebuildUI();
366
- return;
367
- }
368
- if (this.logsContainer) {
369
- this.logsContainer.width = this.logContentWidth;
370
- }
371
- if (this.logsScrollBox) {
372
- this.logsScrollBox.content.left = 0;
373
- this.logsScrollBox.content.top = 0;
374
- this.logsScrollBox.content.width = this.logContentWidth;
375
- }
376
- for (const child of this.logsContainer?.getChildren() || []) {
377
- if (child instanceof TextRenderable) {
378
- child.width = this.logContentWidth;
379
- child.left = 0;
380
- }
381
- }
382
- this.renderer.requestRender();
383
- };
384
- }
385
- createHeader(isCompact, isVeryCompact) {
386
- if (!this.renderer)
387
- throw new Error("Renderer not initialized");
388
- // In compact mode, no box border - just logo and version
389
- if (isCompact || isVeryCompact) {
390
- const headerBox = new BoxRenderable(this.renderer, {
391
- id: "header",
392
- flexDirection: "row",
393
- paddingLeft: 1,
394
- height: 1
395
- });
396
- // Only: logo and version
397
- const logoText = new TextRenderable(this.renderer, {
398
- id: "logo-compact",
399
- content: t `${bold(fg(BRAND_PURPLE)(COMPACT_LOGO))}${dim(` v${this.options.version}`)}`
400
- });
401
- headerBox.add(logoText);
402
- return headerBox;
403
- }
404
- // Full mode: bordered box with ASCII logo and info
405
- const headerBox = new BoxRenderable(this.renderer, {
406
- id: "header",
407
- border: true,
408
- borderStyle: "single",
409
- borderColor: BRAND_PURPLE,
410
- paddingLeft: 1,
411
- paddingRight: 1,
412
- flexDirection: "row",
413
- alignItems: "flex-start",
414
- height: 6
415
- });
416
- // ASCII logo column
417
- const logoCol = new BoxRenderable(this.renderer, {
418
- id: "logo-col",
419
- flexDirection: "column",
420
- marginRight: 2
421
- });
422
- headerBox.add(logoCol);
423
- for (const line of FULL_LOGO) {
424
- const logoLine = new TextRenderable(this.renderer, {
425
- content: t `${bold(fg(BRAND_PURPLE)(line))}`
426
- });
427
- logoCol.add(logoLine);
428
- }
429
- // Info column
430
- const infoCol = new BoxRenderable(this.renderer, {
431
- id: "info-col",
432
- flexDirection: "column",
433
- flexGrow: 1
434
- });
435
- headerBox.add(infoCol);
436
- const protocol = this.useHttps ? "https" : "http";
437
- const portStatus = this.portConfirmed ? "" : " ...";
438
- const displayedAppUrl = this.appUrl || `${protocol}://localhost:${this.appPort}`;
439
- const appLine = new TextRenderable(this.renderer, {
440
- id: "app-url",
441
- content: this.portConfirmed
442
- ? t `${cyan(`🌐 App: ${displayedAppUrl}`)}`
443
- : t `${cyan(`🌐 App: ${displayedAppUrl}`)}${yellow(portStatus)}`
444
- });
445
- infoCol.add(appLine);
446
- // Show logs path with ~/ instead of full home path
447
- const logsPath = this.options.logFile.replace(process.env.HOME || "", "~");
448
- const logsLine = new TextRenderable(this.renderer, {
449
- id: "logs-file",
450
- content: t `${cyan(`📋 Logs: ${logsPath}`)}`
451
- });
452
- infoCol.add(logsLine);
453
- if (this.options.serversOnly) {
454
- const serversLine = new TextRenderable(this.renderer, {
455
- id: "servers-only",
456
- content: t `${cyan("🖥️ Servers-only mode")}`
457
- });
458
- infoCol.add(serversLine);
459
- }
460
- if (this.initStatus) {
461
- const statusLine = new TextRenderable(this.renderer, {
462
- id: "init-status",
463
- content: t `${bold(fg(BRAND_PURPLE)(this.initStatus))}`
464
- });
465
- infoCol.add(statusLine);
466
- }
467
- return headerBox;
468
- }
469
- buildStatusContent(isCompact) {
470
- const parts = [];
471
- if (isCompact) {
472
- const cleanName = this.options.projectName?.replace(/-[a-f0-9]{6}$/, "") || "d3k";
473
- parts.push(cleanName);
474
- }
475
- if (this.updateInfo?.type === "available") {
476
- return t `${yellow(`↑ v${this.updateInfo.latestVersion} available (d3k upgrade)`)}`;
477
- }
478
- if (this.updateInfo?.type === "updated") {
479
- return t `${green(`✓ Updated to v${this.updateInfo.newVersion}`)}`;
480
- }
481
- if (this.ctrlCMessage) {
482
- return t `${fg(BRAND_PURPLE)(this.ctrlCMessage)}`;
483
- }
484
- if (parts.length > 0) {
485
- return t `${fg(BRAND_PURPLE)(parts.join(" "))}`;
486
- }
487
- return t ``;
488
- }
489
- rebuildUI() {
490
- if (!this.renderer)
491
- return;
492
- if (this.isRebuilding)
493
- return;
494
- this.isRebuilding = true;
495
- // Clear and rebuild - OpenTUI handles resize
496
- const root = this.renderer.root;
497
- for (const child of root.getChildren()) {
498
- child.destroy();
499
- }
500
- this.setupUI();
501
- // Delay log refresh to allow layout to compute valid positions
502
- // Without this delay, logs get NaN X positions and selection fails
503
- setTimeout(() => {
504
- this.refreshLogs();
505
- this.isRebuilding = false;
506
- }, 100);
507
- }
508
- setupKeyboardHandlers() {
509
- if (!this.renderer)
510
- return;
511
- let ctrlCPending = false;
512
- this.renderer.keyInput.on("keypress", (key) => {
513
- // Ctrl+Shift+C or Cmd+C: copy selection to clipboard
514
- if ((key.ctrl && key.shift && key.name === "c") || (key.meta && key.name === "c")) {
515
- this.copySelectionToClipboard();
516
- return;
517
- }
518
- // Handle Ctrl+C with double-tap protection
519
- if (key.ctrl && key.name === "c") {
520
- if (ctrlCPending) {
521
- // Second Ctrl+C - call shutdown callback directly
522
- // This bypasses signal handling which can be unreliable in TUI mode
523
- if (this.options.onRequestShutdown) {
524
- this.options.onRequestShutdown();
525
- }
526
- else {
527
- // Fallback to signal if no callback provided
528
- process.emit("SIGINT");
529
- }
530
- }
531
- else {
532
- // First Ctrl+C - show warning
533
- ctrlCPending = true;
534
- this.ctrlCMessage = "⚠️ ^C again to quit";
535
- this.updateStatusDisplay();
536
- setTimeout(() => {
537
- ctrlCPending = false;
538
- this.ctrlCMessage = "";
539
- this.updateStatusDisplay();
540
- }, 3000);
541
- }
542
- return;
543
- }
544
- // Handle Ctrl+L to clear logs
545
- if (key.ctrl && key.name === "l") {
546
- this.clearFromLogId = this.logs.length > 0 ? this.logs[this.logs.length - 1].id : this.logIdCounter;
547
- this.logsScrollBox?.scrollTo({ x: 0, y: 0 });
548
- this.refreshLogs();
549
- return;
550
- }
551
- // Handle Escape to clear base timestamp (exit delta mode)
552
- if (key.name === "escape") {
553
- if (this.baseTimestampMs !== undefined) {
554
- this.baseTimestampMs = undefined;
555
- this.baseLogId = undefined;
556
- this.refreshLogs();
557
- }
558
- this.clearLogSelection();
559
- this.renderer?.clearSelection();
560
- return;
561
- }
562
- // Debug: Shift+D to toggle debug mode (verbose logging to file)
563
- if (key.name === "d" && key.shift && !key.ctrl) {
564
- this.debugMode = !this.debugMode;
565
- if (this.debugMode) {
566
- // Ensure directory exists and clear the debug log file when enabling
567
- try {
568
- mkdirSync(join(process.env.HOME || tmpdir(), ".d3k"), { recursive: true });
569
- writeFileSync(this.debugLogFile, `Debug mode enabled at ${new Date().toISOString()}\n`);
570
- }
571
- catch {
572
- // Ignore write errors
573
- }
574
- }
575
- const entry = {
576
- id: ++this.logIdCounter,
577
- content: `[DEBUG] Debug mode ${this.debugMode ? `ENABLED - logging to ${this.debugLogFile}` : "DISABLED"}`
578
- };
579
- this.logs.push(entry);
580
- this.addLogLines([entry]);
581
- return;
582
- }
583
- // Debug: Press 'd' to check selection state and focus info (shows in UI and writes to file)
584
- if (key.name === "d" && !key.ctrl && !key.shift) {
585
- const selection = this.renderer?.getSelection();
586
- const hasSelection = this.renderer?.hasSelection;
587
- const selectedText = selection?.getSelectedText() || "(none)";
588
- const focusedRenderable = this.renderer?.currentFocusedRenderable;
589
- const focusedId = focusedRenderable?.id || "(none)";
590
- const selectionContainer = this.renderer?.getSelectionContainer();
591
- const containerId = selectionContainer?.id || "(none)";
592
- const message = `hasSelection: ${hasSelection}, isDragging: ${selection?.isDragging}, renderables: ${selection?.selectedRenderables?.length ?? 0}, focus: ${focusedId}, container: ${containerId}, text: "${selectedText.slice(0, 30)}"`;
593
- // Write to file if debug mode is on
594
- this.debugLog(`[SELECTION_STATE] ${message}`);
595
- // Always show in UI
596
- const debugEntry = {
597
- id: ++this.logIdCounter,
598
- content: `[DEBUG] ${message}`
599
- };
600
- this.logs.push(debugEntry);
601
- this.addLogLines([debugEntry]);
602
- return;
603
- }
604
- // Keyboard scrolling (also works alongside mouse scroll)
605
- if (this.logsScrollBox) {
606
- if (key.name === "up") {
607
- this.logsScrollBox.scrollBy(-1);
608
- }
609
- else if (key.name === "down") {
610
- this.logsScrollBox.scrollBy(1);
611
- }
612
- else if (key.name === "pageup") {
613
- this.logsScrollBox.scrollBy(-10);
614
- }
615
- else if (key.name === "pagedown") {
616
- this.logsScrollBox.scrollBy(10);
617
- }
618
- else if (key.name === "g" && !key.shift) {
619
- // g - go to top
620
- this.logsScrollBox.scrollTo({ x: 0, y: 0 });
621
- }
622
- else if (key.name === "g" && key.shift) {
623
- // G - go to bottom
624
- this.logsScrollBox.scrollTo({ x: 0, y: this.logsScrollBox.scrollHeight });
625
- }
626
- }
627
- });
628
- }
629
- setupFocusTracking() {
630
- if (!this.renderer)
631
- return;
632
- // Track focus changes via frame callback (since there's no global focus event)
633
- this.renderer.setFrameCallback(async () => {
634
- if (!this.debugMode || !this.renderer)
635
- return;
636
- const focused = this.renderer.currentFocusedRenderable;
637
- const currentId = focused?.id || null;
638
- if (currentId !== this.lastFocusedId) {
639
- this.debugLog(`[FOCUS_CHANGE] ${this.lastFocusedId || "(none)"} -> ${currentId || "(none)"}`);
640
- this.lastFocusedId = currentId;
641
- }
642
- });
643
- }
644
- setupSelectionCopy() {
645
- if (!this.renderer || this.selectionCopyHandler)
646
- return;
647
- this.selectionCopyHandler = (selection) => {
648
- if (!selection) {
649
- this.lastCopiedSelection = null;
650
- this.logSelectionDebug("[SELECTION_EVENT] selection cleared");
651
- return;
652
- }
653
- this.logSelectionDebug(`[SELECTION_EVENT] ${this.describeSelection(selection)}`);
654
- if (selection.isDragging || !selection.isActive)
655
- return;
656
- const text = selection.getSelectedText();
657
- if (!text || text.trim().length === 0)
658
- return;
659
- if (text === this.lastCopiedSelection)
660
- return;
661
- this.lastCopiedSelection = text;
662
- this.copyToClipboard(text);
663
- this.logSelectionDebug(`[SELECTION_COPY] length=${text.length} text="${text.slice(0, 120)}"`);
664
- };
665
- this.renderer.on("selection", this.selectionCopyHandler);
666
- }
667
- copySelectionToClipboard() {
668
- const text = this.getSelectedLogText() || this.renderer?.getSelection()?.getSelectedText();
669
- if (!text || text.trim().length === 0)
670
- return;
671
- this.lastCopiedSelection = text;
672
- this.copyToClipboard(text);
673
- }
674
- copyToClipboard(text) {
675
- if (process.platform === "darwin") {
676
- try {
677
- spawnSync("pbcopy", { input: text });
678
- }
679
- catch (error) {
680
- this.debugLog(`[CLIPBOARD] Failed to copy selection: ${String(error)}`);
681
- }
682
- }
683
- }
684
- startLogFileWatcher() {
685
- let buffer = "";
686
- let pendingLogs = [];
687
- let flushTimeout = null;
688
- const flushPendingLogs = () => {
689
- if (pendingLogs.length === 0)
690
- return;
691
- const logsToAdd = pendingLogs;
692
- pendingLogs = [];
693
- flushTimeout = null;
694
- this.logs.push(...logsToAdd);
695
- if (this.logs.length > this.maxLogs) {
696
- this.logs = this.logs.slice(-this.maxLogs);
697
- }
698
- this.addLogLines(logsToAdd);
699
- };
700
- const appendLog = (line) => {
701
- // Extract timestamp from log line format: [timestamp] [source] ...
702
- const timestampMatch = line.match(/^\[(.*?)\]/);
703
- const timestamp = timestampMatch ? timestampMatch[1] : undefined;
704
- const newLog = {
705
- id: this.logIdCounter++,
706
- content: line,
707
- timestamp
708
- };
709
- pendingLogs.push(newLog);
710
- if (flushTimeout) {
711
- clearTimeout(flushTimeout);
712
- }
713
- flushTimeout = setTimeout(flushPendingLogs, 50);
714
- };
715
- // Initial read
716
- const logStream = createReadStream(this.options.logFile, {
717
- encoding: "utf8",
718
- start: 0
719
- });
720
- logStream.on("data", (chunk) => {
721
- buffer += chunk.toString();
722
- const lines = buffer.split("\n");
723
- buffer = lines.pop() || "";
724
- for (const line of lines) {
725
- if (line.trim()) {
726
- appendLog(line);
727
- }
728
- }
729
- });
730
- logStream.on("error", (error) => {
731
- appendLog(`Error reading log file: ${error.message}`);
732
- });
733
- // Watch for changes
734
- watchFile(this.options.logFile, { interval: 100 }, (curr, prev) => {
735
- if (curr.size > prev.size) {
736
- const stream = createReadStream(this.options.logFile, {
737
- encoding: "utf8",
738
- start: prev.size
739
- });
740
- let watchBuffer = "";
741
- stream.on("data", (chunk) => {
742
- watchBuffer += chunk.toString();
743
- const lines = watchBuffer.split("\n");
744
- watchBuffer = lines.pop() || "";
745
- for (const line of lines) {
746
- if (line.trim()) {
747
- appendLog(line);
748
- }
749
- }
750
- });
751
- }
752
- });
753
- }
754
- addLogLines(newLogs) {
755
- if (!this.renderer || !this.logsContainer)
756
- return;
757
- const isCompact = this.renderer.width < 100;
758
- for (const log of newLogs) {
759
- if (log.id <= this.clearFromLogId)
760
- continue;
761
- const isBaseLog = this.baseLogId === log.id;
762
- const formatted = formatLogLine(log.content, isCompact, this.baseTimestampMs, isBaseLog);
763
- this.logBaseContent.set(log.id, formatted);
764
- // Track click start position to detect clicks vs drags
765
- let clickStartX = -1;
766
- let clickStartY = -1;
767
- const logLine = new TextRenderable(this.renderer, {
768
- id: `log-${log.id}`,
769
- content: formatted,
770
- wrapMode: "none",
771
- selectable: false,
772
- selectionBg: RGBA.fromInts(70, 130, 180), // Steel blue highlight
773
- selectionFg: RGBA.fromInts(255, 255, 255), // White text on selection
774
- width: this.logContentWidth,
775
- left: 0,
776
- flexShrink: 0,
777
- onMouseDown: (event) => {
778
- // Record start position for click detection
779
- clickStartX = event.x;
780
- clickStartY = event.y;
781
- this.debugLog(`[TEXT_DOWN] id=${logLine.id} screen(${event.x},${event.y}) pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height})`);
782
- this.logSelectionDebug(`[TEXT_DOWN] id=${logLine.id} screen(${event.x},${event.y}) pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height})`);
783
- },
784
- onMouseUp: (event) => {
785
- // Check if this was a click (no movement) vs a drag
786
- const isClick = clickStartX === event.x && clickStartY === event.y;
787
- if (isClick && log.timestamp) {
788
- // Only trigger baseline on clicks within timestamp column (first ~15 chars)
789
- // Calculate position within the text (accounting for renderable's X position)
790
- const textLocalX = event.x - (logLine.x || 0);
791
- const TIMESTAMP_COLUMN_WIDTH = 15; // "[HH:MM:SS.mmm] " is ~14 chars
792
- if (textLocalX <= TIMESTAMP_COLUMN_WIDTH) {
793
- this.clearLogSelection();
794
- // Toggle baseline timestamp on click
795
- const timestampMs = parseTimestampToMs(log.timestamp);
796
- if (timestampMs !== null) {
797
- if (this.baseLogId === log.id) {
798
- // Clicking same log clears the baseline
799
- this.baseTimestampMs = undefined;
800
- this.baseLogId = undefined;
801
- }
802
- else {
803
- // Set this log as the new baseline
804
- this.baseTimestampMs = timestampMs;
805
- this.baseLogId = log.id;
806
- }
807
- this.refreshLogs();
808
- }
809
- }
810
- }
811
- // Reset click tracking
812
- clickStartX = -1;
813
- clickStartY = -1;
814
- }
815
- });
816
- this.logRenderables.set(log.id, logLine);
817
- this.logsContainer.add(logLine);
818
- this.updateLogLineSelectionStyle(log.id);
819
- }
820
- // Force render to update hit grid after adding new logs
821
- // This prevents stale hit grid when new logs trigger scroll/layout changes
822
- this.renderer.requestRender();
823
- setTimeout(() => {
824
- this.logLogPaneMetrics(`after-add-${newLogs.length}`);
825
- }, 0);
826
- }
827
- findLogLineAt(x, y) {
828
- if (!this.logsContainer)
829
- return null;
830
- for (const child of this.logsContainer.getChildren()) {
831
- if (!(child instanceof TextRenderable))
832
- continue;
833
- const withinX = x >= child.x && x < child.x + child.width;
834
- const withinY = y >= child.y && y < child.y + child.height;
835
- if (withinX && withinY)
836
- return child;
837
- }
838
- return null;
839
- }
840
- findLogLineAtRow(y) {
841
- if (!this.logsContainer)
842
- return null;
843
- for (const child of this.logsContainer.getChildren()) {
844
- if (!(child instanceof TextRenderable))
845
- continue;
846
- const withinY = y >= child.y && y < child.y + Math.max(child.height, 1);
847
- if (withinY)
848
- return child;
849
- }
850
- return null;
851
- }
852
- getSelectionLogLine(event) {
853
- const target = event.target;
854
- if (target?.id?.startsWith("log-")) {
855
- const child = this.logsContainer?.getChildren().find((entry) => entry.id === target.id);
856
- if (child instanceof TextRenderable) {
857
- this.logSelectionDebug(`[HIT] direct target=${target.id} pos(${child.x},${child.y}) size(${child.width}x${child.height})`);
858
- return child;
859
- }
860
- }
861
- const fallback = this.findLogLineAt(event.x, event.y);
862
- if (fallback) {
863
- this.logSelectionDebug(`[HIT] fallback screen(${event.x},${event.y}) -> ${fallback.id} pos(${fallback.x},${fallback.y}) size(${fallback.width}x${fallback.height})`);
864
- return fallback;
865
- }
866
- const rowFallback = this.findLogLineAtRow(event.y);
867
- this.logSelectionDebug(`[HIT] row-fallback screen(${event.x},${event.y}) -> ${rowFallback?.id || "(none)"} pos(${rowFallback?.x ?? "?"},${rowFallback?.y ?? "?"}) size(${rowFallback?.width ?? "?"}x${rowFallback?.height ?? "?"})`);
868
- return rowFallback;
869
- }
870
- refreshLogs() {
871
- if (!this.renderer || !this.logsContainer)
872
- return;
873
- // Clear existing log lines
874
- for (const child of this.logsContainer.getChildren()) {
875
- child.destroy();
876
- }
877
- this.logRenderables.clear();
878
- this.logBaseContent.clear();
879
- // Re-add filtered logs
880
- const filteredLogs = this.logs.filter((log) => log.id > this.clearFromLogId);
881
- this.addLogLines(filteredLogs);
882
- this.normalizeLogSelection();
883
- }
884
- beginLogSelection(logRenderableId, x, y) {
885
- const logId = this.parseLogRenderableId(logRenderableId);
886
- if (logId === null)
887
- return;
888
- this.logSelection = {
889
- anchorLogId: logId,
890
- focusLogId: logId,
891
- startX: x,
892
- startY: y,
893
- hasDragged: false
894
- };
895
- this.updateLogSelectionStyles();
896
- }
897
- updateLogSelection(logRenderableId, x, y) {
898
- if (!this.logSelection)
899
- return;
900
- const logId = logRenderableId ? this.parseLogRenderableId(logRenderableId) : null;
901
- if (logId !== null) {
902
- this.logSelection.focusLogId = logId;
903
- }
904
- if (x !== this.logSelection.startX || y !== this.logSelection.startY) {
905
- this.logSelection.hasDragged = true;
906
- }
907
- this.updateLogSelectionStyles();
908
- }
909
- finishLogSelection(logRenderableId, x, y) {
910
- if (!this.logSelection)
911
- return;
912
- this.updateLogSelection(logRenderableId, x, y);
913
- if (!this.logSelection?.hasDragged) {
914
- this.clearLogSelection();
915
- return;
916
- }
917
- const text = this.getSelectedLogText();
918
- if (!text || text.trim().length === 0 || text === this.lastCopiedSelection)
919
- return;
920
- this.lastCopiedSelection = text;
921
- this.copyToClipboard(text);
922
- this.logSelectionDebug(`[SELECTION_COPY] length=${text.length} text="${text.slice(0, 120)}"`);
923
- }
924
- clearLogSelection() {
925
- if (!this.logSelection)
926
- return;
927
- this.logSelection = null;
928
- this.updateLogSelectionStyles();
929
- }
930
- normalizeLogSelection() {
931
- if (!this.logSelection)
932
- return;
933
- const selectedLogIds = this.getSelectedLogIds();
934
- if (selectedLogIds.length === 0) {
935
- this.logSelection = null;
936
- this.updateLogSelectionStyles();
937
- }
938
- }
939
- getSelectedLogIds() {
940
- if (!this.logSelection)
941
- return [];
942
- const visibleLogIds = [...this.logRenderables.keys()].sort((a, b) => a - b);
943
- const anchorIndex = visibleLogIds.indexOf(this.logSelection.anchorLogId);
944
- const focusIndex = visibleLogIds.indexOf(this.logSelection.focusLogId);
945
- if (anchorIndex === -1 || focusIndex === -1)
946
- return [];
947
- const startIndex = Math.min(anchorIndex, focusIndex);
948
- const endIndex = Math.max(anchorIndex, focusIndex);
949
- return visibleLogIds.slice(startIndex, endIndex + 1);
950
- }
951
- getSelectedLogText() {
952
- const selectedIds = new Set(this.getSelectedLogIds());
953
- if (selectedIds.size === 0)
954
- return "";
955
- return this.logs
956
- .filter((log) => log.id > this.clearFromLogId && selectedIds.has(log.id))
957
- .map((log) => log.content)
958
- .join("\n");
959
- }
960
- updateLogSelectionStyles() {
961
- for (const logId of this.logRenderables.keys()) {
962
- this.updateLogLineSelectionStyle(logId);
963
- }
964
- this.renderer?.requestRender();
965
- }
966
- updateLogLineSelectionStyle(logId) {
967
- const renderable = this.logRenderables.get(logId);
968
- const baseContent = this.logBaseContent.get(logId);
969
- if (!renderable || !baseContent)
970
- return;
971
- const selectedIds = new Set(this.getSelectedLogIds());
972
- renderable.content = selectedIds.has(logId)
973
- ? applySelectionToStyledText(baseContent, this.logSelectionBackground, this.logSelectionForeground)
974
- : baseContent;
975
- }
976
- parseLogRenderableId(logRenderableId) {
977
- const match = logRenderableId.match(/^log-(\d+)$/);
978
- if (!match)
979
- return null;
980
- const logId = Number.parseInt(match[1], 10);
981
- return Number.isNaN(logId) ? null : logId;
982
- }
983
- updateStatusDisplay() {
984
- if (!this.statusText || !this.renderer)
985
- return;
986
- const isCompact = this.renderer.width < 100;
987
- this.statusText.content = this.buildStatusContent(isCompact);
988
- }
989
- // Public update methods
990
- setStatus(status) {
991
- if (status?.includes("Press Ctrl+C again")) {
992
- this.ctrlCMessage = "⚠️ ^C again to quit";
993
- this.initStatus = null;
994
- this.updateStatusDisplay();
995
- if (this.ctrlCTimeout)
996
- clearTimeout(this.ctrlCTimeout);
997
- this.ctrlCTimeout = setTimeout(() => {
998
- this.ctrlCMessage = "";
999
- this.updateStatusDisplay();
1000
- }, 3000);
1001
- }
1002
- else {
1003
- this.initStatus = status;
1004
- this.rebuildUI();
1005
- }
1006
- }
1007
- setAppPort(port) {
1008
- this.appPort = port;
1009
- this.portConfirmed = true;
1010
- this.rebuildUI();
1011
- }
1012
- setAppUrl(url) {
1013
- this.appUrl = url;
1014
- this.rebuildUI();
1015
- }
1016
- setUpdateInfo(info) {
1017
- this.updateInfo = info;
1018
- this.updateStatusDisplay();
1019
- if (info?.type === "updated" && info.autoHide !== false) {
1020
- setTimeout(() => {
1021
- this.updateInfo = null;
1022
- this.updateStatusDisplay();
1023
- }, 10000);
1024
- }
1025
- }
1026
- setUseHttps(useHttps) {
1027
- this.useHttps = useHttps;
1028
- this.rebuildUI();
1029
- }
1030
- debugLog(message) {
1031
- if (!this.debugMode)
1032
- return;
1033
- const timestamp = new Date().toISOString();
1034
- const logLine = `[${timestamp}] ${message}\n`;
1035
- try {
1036
- appendFileSync(this.debugLogFile, logLine);
1037
- }
1038
- catch {
1039
- // Ignore write errors
1040
- }
1041
- }
1042
- initializeSelectionDebugLog() {
1043
- if (!this.selectionDebugEnabled)
1044
- return;
1045
- try {
1046
- mkdirSync(join(process.env.HOME || tmpdir(), ".d3k"), { recursive: true });
1047
- writeFileSync(this.selectionDebugLogFile, [
1048
- `Selection debug started at ${new Date().toISOString()}`,
1049
- `project=${this.options.projectName || "(unknown)"}`,
1050
- `logFile=${this.options.logFile}`,
1051
- `appPort=${this.options.appPort}`,
1052
- ""
1053
- ].join("\n"));
1054
- }
1055
- catch {
1056
- // Ignore write errors
1057
- }
1058
- }
1059
- logSelectionDebug(message) {
1060
- if (!this.selectionDebugEnabled)
1061
- return;
1062
- const timestamp = new Date().toISOString();
1063
- try {
1064
- appendFileSync(this.selectionDebugLogFile, `[${timestamp}] ${message}\n`);
1065
- }
1066
- catch {
1067
- // Ignore write errors
1068
- }
1069
- }
1070
- describeSelection(selection = this.renderer?.getSelection() || null) {
1071
- const logSelectionIds = this.getSelectedLogIds();
1072
- const logSelectionText = this.getSelectedLogText();
1073
- const logSelectionDescription = logSelectionIds.length === 0
1074
- ? "logSelection=(none)"
1075
- : `logSelection={dragging:${Boolean(this.logSelection?.hasDragged)},lines:${logSelectionIds.length},textLen:${logSelectionText.length}}`;
1076
- if (!selection) {
1077
- return logSelectionDescription;
1078
- }
1079
- const text = selection.getSelectedText();
1080
- return `${logSelectionDescription} nativeSelection={active:${selection.isActive},dragging:${selection.isDragging},renderables:${selection.selectedRenderables.length},textLen:${text.length}}`;
1081
- }
1082
- logLogPaneMetrics(reason) {
1083
- if (!this.selectionDebugEnabled)
1084
- return;
1085
- const children = this.logsContainer?.getChildren() || [];
1086
- const logLines = children.filter((child) => child instanceof TextRenderable);
1087
- const sample = logLines
1088
- .slice(0, 3)
1089
- .map((line) => `${line.id}@(${line.x},${line.y}) ${line.width}x${line.height}`)
1090
- .join(", ");
1091
- const last = logLines.at(-1);
1092
- this.logSelectionDebug(`[LAYOUT:${reason}] scroll=${this.logsScrollBox?.x ?? "?"},${this.logsScrollBox?.y ?? "?"} ${this.logsScrollBox?.width ?? "?"}x${this.logsScrollBox?.height ?? "?"} content=${this.logsContainer?.x ?? "?"},${this.logsContainer?.y ?? "?"} ${this.logsContainer?.width ?? "?"}x${this.logsContainer?.height ?? "?"} lines=${logLines.length} sample=[${sample}] last=${last ? `${last.id}@(${last.x},${last.y}) ${last.width}x${last.height}` : "(none)"}`);
1093
- }
1094
- shutdown() {
1095
- unwatchFile(this.options.logFile);
1096
- if (this.renderer) {
1097
- try {
1098
- if (this.selectionCopyHandler) {
1099
- this.renderer.off("selection", this.selectionCopyHandler);
1100
- this.selectionCopyHandler = null;
1101
- }
1102
- this.renderer.destroy();
1103
- }
1104
- catch (error) {
1105
- this.debugLog(`Renderer destroy failed during shutdown: ${error}`);
1106
- }
1107
- this.renderer = null;
1108
- }
1109
- // Explicitly reset terminal state:
1110
- // - Disable mouse tracking modes (SGR, all motion, cell motion, basic)
1111
- // - Exit alternate screen buffer
1112
- // - Clear screen and scrollback
1113
- // - Show cursor
1114
- process.stdout.write("\x1b[?1006l" + // Disable SGR extended mouse mode
1115
- "\x1b[?1003l" + // Disable all motion tracking
1116
- "\x1b[?1002l" + // Disable cell motion tracking
1117
- "\x1b[?1000l" + // Disable basic mouse tracking
1118
- "\x1b[?1049l" + // Exit alternate screen buffer
1119
- "\x1b[2J\x1b[H\x1b[3J" + // Clear screen and scrollback
1120
- "\x1b[?25h" // Show cursor
1121
- );
1122
- }
1123
- }
1124
- export async function runTUI(options) {
1125
- const tui = new D3kTUI(options);
1126
- return tui.start();
1127
- }
1128
- //# sourceMappingURL=tui-interface-opentui.js.map