dev3000 0.0.170 → 0.0.173

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 (52) hide show
  1. package/dist/cdp-monitor.d.ts +3 -1
  2. package/dist/cdp-monitor.d.ts.map +1 -1
  3. package/dist/cdp-monitor.js +44 -9
  4. package/dist/cdp-monitor.js.map +1 -1
  5. package/dist/cli.js +196 -81
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/crawl.d.ts.map +1 -1
  8. package/dist/commands/crawl.js +1 -1
  9. package/dist/commands/crawl.js.map +1 -1
  10. package/dist/dev-environment.d.ts +10 -1
  11. package/dist/dev-environment.d.ts.map +1 -1
  12. package/dist/dev-environment.js +72 -32
  13. package/dist/dev-environment.js.map +1 -1
  14. package/dist/portless.d.ts +15 -0
  15. package/dist/portless.d.ts.map +1 -0
  16. package/dist/portless.js +180 -0
  17. package/dist/portless.js.map +1 -0
  18. package/dist/screencast-manager.d.ts +3 -2
  19. package/dist/screencast-manager.d.ts.map +1 -1
  20. package/dist/screencast-manager.js +14 -5
  21. package/dist/screencast-manager.js.map +1 -1
  22. package/dist/skills/d3k/SKILL.md +40 -0
  23. package/dist/src/loading.html +2 -2
  24. package/dist/src/tui-interface-impl.tsx +24 -4
  25. package/dist/tui-interface-impl.d.ts +2 -0
  26. package/dist/tui-interface-impl.d.ts.map +1 -1
  27. package/dist/tui-interface-impl.js +18 -3
  28. package/dist/tui-interface-impl.js.map +1 -1
  29. package/dist/tui-interface-opentui.d.ts +2 -0
  30. package/dist/tui-interface-opentui.d.ts.map +1 -1
  31. package/dist/tui-interface-opentui.js +265 -24
  32. package/dist/tui-interface-opentui.js.map +1 -1
  33. package/dist/tui-interface.d.ts +3 -0
  34. package/dist/tui-interface.d.ts.map +1 -1
  35. package/dist/tui-interface.js +12 -4
  36. package/dist/tui-interface.js.map +1 -1
  37. package/dist/utils/agent-browser.d.ts.map +1 -1
  38. package/dist/utils/agent-browser.js +43 -19
  39. package/dist/utils/agent-browser.js.map +1 -1
  40. package/dist/utils/agent-selection.d.ts.map +1 -1
  41. package/dist/utils/agent-selection.js +1 -0
  42. package/dist/utils/agent-selection.js.map +1 -1
  43. package/dist/utils/browser-command-argv.d.ts +8 -0
  44. package/dist/utils/browser-command-argv.d.ts.map +1 -0
  45. package/dist/utils/browser-command-argv.js +41 -0
  46. package/dist/utils/browser-command-argv.js.map +1 -0
  47. package/dist/utils/build-version.d.ts +2 -0
  48. package/dist/utils/build-version.d.ts.map +1 -0
  49. package/dist/utils/build-version.js +8 -0
  50. package/dist/utils/build-version.js.map +1 -0
  51. package/package.json +24 -21
  52. package/src/tui-interface-impl.tsx +24 -4
@@ -117,6 +117,7 @@ class D3kTUI {
117
117
  statusText = null;
118
118
  // State
119
119
  appPort;
120
+ appUrl;
120
121
  useHttps;
121
122
  portConfirmed = false;
122
123
  updateInfo;
@@ -130,14 +131,21 @@ class D3kTUI {
130
131
  debugMode = false;
131
132
  lastFocusedId = null;
132
133
  debugLogFile = join(process.env.HOME || tmpdir(), ".d3k", "tui-debug.log");
134
+ selectionDebugEnabled = process.env.D3K_TUI_SELECTION_DEBUG !== "0";
135
+ selectionDebugLogFile = join(process.env.HOME || tmpdir(), ".d3k", "tui-selection-debug.log");
133
136
  lastCopiedSelection = null;
134
137
  selectionCopyHandler = null;
135
138
  isCompact = false;
136
139
  isVeryCompact = false;
137
140
  isRebuilding = false;
141
+ logContentWidth = 80;
142
+ logRenderables = new Map();
143
+ logSelection = null;
144
+ logSelectionBackground = RGBA.fromInts(70, 130, 180);
138
145
  constructor(options) {
139
146
  this.options = options;
140
147
  this.appPort = options.appPort;
148
+ this.appUrl = options.appUrl || null;
141
149
  this.useHttps = options.useHttps || false;
142
150
  this.updateInfo = options.updateInfo || null;
143
151
  }
@@ -167,6 +175,7 @@ class D3kTUI {
167
175
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
168
176
  process.stdout.write = createFilteredWrite(originalStdoutWrite);
169
177
  process.stderr.write = createFilteredWrite(originalStderrWrite);
178
+ this.initializeSelectionDebugLog();
170
179
  this.renderer = await createCliRenderer(config);
171
180
  // Setup UI after renderer is created
172
181
  this.setupUI();
@@ -182,10 +191,15 @@ class D3kTUI {
182
191
  // Force a redraw after start to ensure borders render correctly
183
192
  process.stdout.write("\x1b[2J\x1b[H");
184
193
  this.renderer.requestRender();
194
+ setTimeout(() => {
195
+ this.logSelectionDebug(`[START] renderer=${this.renderer?.width}x${this.renderer?.height} logContentWidth=${this.logContentWidth}`);
196
+ this.logLogPaneMetrics("post-start");
197
+ }, 150);
185
198
  return {
186
199
  app: { unmount: () => this.shutdown() },
187
200
  updateStatus: (status) => this.setStatus(status),
188
201
  updateAppPort: (port) => this.setAppPort(port),
202
+ updateAppUrl: (url) => this.setAppUrl(url),
189
203
  updateUpdateInfo: (info) => this.setUpdateInfo(info),
190
204
  updateUseHttps: (useHttps) => this.setUseHttps(useHttps)
191
205
  };
@@ -196,6 +210,7 @@ class D3kTUI {
196
210
  const { width, height } = this.renderer;
197
211
  const isCompact = width < 80;
198
212
  const isVeryCompact = width < 60 || height < 15;
213
+ this.logContentWidth = Math.max(20, width - 6);
199
214
  this.isCompact = isCompact;
200
215
  this.isVeryCompact = isVeryCompact;
201
216
  // Main container
@@ -230,7 +245,6 @@ class D3kTUI {
230
245
  stickyStart: "bottom",
231
246
  scrollAcceleration: new MacOSScrollAccel({ maxMultiplier: 8 }),
232
247
  viewportCulling: false, // Disable culling to ensure all items are in hit grid
233
- paddingLeft: 1, // Force left position calculation for selection hit testing
234
248
  onMouse: (event) => {
235
249
  // Log ALL mouse events to understand the flow
236
250
  if (event.type === "down" || event.type === "up") {
@@ -241,47 +255,65 @@ class D3kTUI {
241
255
  const targetW = target?.width ?? "?";
242
256
  const targetH = target?.height ?? "?";
243
257
  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}`);
258
+ 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()}`);
244
259
  }
245
260
  },
246
261
  onMouseDown: (event) => {
247
- if (this.renderer && !this.renderer.hasSelection && event.button === 0) {
248
- const target = event.target;
249
- if (!target?.selectable) {
250
- const logLine = this.findLogLineAt(event.x, event.y);
251
- if (logLine) {
252
- this.renderer.startSelection(logLine, event.x, event.y);
253
- }
262
+ if (event.button === 0) {
263
+ const logLine = this.getSelectionLogLine(event);
264
+ if (logLine) {
265
+ this.logSelectionDebug(`[START_SELECTION] target=${logLine.id} pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height}) screen(${event.x},${event.y})`);
266
+ this.beginLogSelection(logLine.id, event.x, event.y);
267
+ this.logSelectionDebug(`[START_SELECTION_AFTER] ${this.describeSelection()}`);
254
268
  }
255
269
  }
256
270
  const target = event.target;
257
271
  const targetId = target?.id || "(none)";
258
272
  const selection = this.renderer?.getSelection();
259
- this.debugLog(`[MOUSE_DOWN_CB] x=${event.x}, y=${event.y}, target=${targetId}, hasSelection=${this.renderer?.hasSelection}, isSelecting=${selection?.isSelecting}`);
273
+ this.debugLog(`[MOUSE_DOWN_CB] x=${event.x}, y=${event.y}, target=${targetId}, hasSelection=${this.renderer?.hasSelection}, isDragging=${selection?.isDragging}`);
274
+ this.logSelectionDebug(`[MOUSE_DOWN_CB] x=${event.x}, y=${event.y}, target=${targetId} ${this.describeSelection()}`);
260
275
  },
261
276
  onMouseUp: (event) => {
262
- const selection = this.renderer?.getSelection();
263
- const selectedText = selection?.getSelectedText() || "(none)";
264
- this.debugLog(`[MOUSE_UP] x=${event.x}, y=${event.y}, hasSelection=${this.renderer?.hasSelection}, text="${selectedText.slice(0, 30)}"`);
277
+ if (event.button === 0) {
278
+ const logLine = this.getSelectionLogLine(event);
279
+ this.finishLogSelection(logLine?.id ?? null, event.x, event.y);
280
+ this.logSelectionDebug(`[FINISH_SELECTION] target=${logLine?.id || "(none)"} screen(${event.x},${event.y}) ${this.describeSelection()}`);
281
+ }
282
+ const selectedText = this.getSelectedLogText() || "(none)";
283
+ this.debugLog(`[MOUSE_UP] x=${event.x}, y=${event.y}, hasSelection=${Boolean(this.logSelection)}, text="${selectedText.slice(0, 30)}"`);
284
+ this.logSelectionDebug(`[MOUSE_UP] x=${event.x}, y=${event.y}, text="${selectedText.slice(0, 60)}" ${this.describeSelection()}`);
265
285
  },
266
286
  onMouseDrag: (event) => {
287
+ if (this.logSelection) {
288
+ const logLine = this.getSelectionLogLine(event);
289
+ this.updateLogSelection(logLine?.id ?? null, event.x, event.y);
290
+ this.logSelectionDebug(`[UPDATE_SELECTION] target=${logLine?.id || "(none)"} screen(${event.x},${event.y}) ${this.describeSelection()}`);
291
+ }
267
292
  const target = event.target;
268
293
  const targetId = target?.id || "(none)";
269
- this.debugLog(`[MOUSE_DRAG] x=${event.x}, y=${event.y}, target=${targetId}, isSelecting=${event.isSelecting}`);
294
+ this.debugLog(`[MOUSE_DRAG] x=${event.x}, y=${event.y}, target=${targetId}, isDragging=${event.isDragging}`);
295
+ this.logSelectionDebug(`[MOUSE_DRAG] x=${event.x}, y=${event.y}, target=${targetId}, eventDragging=${event.isDragging} ${this.describeSelection()}`);
270
296
  }
271
297
  });
298
+ this.logsScrollBox.content.flexDirection = "column";
299
+ this.logsScrollBox.content.left = 0;
300
+ this.logsScrollBox.content.top = 0;
301
+ this.logsScrollBox.content.width = this.logContentWidth;
272
302
  logsSection.add(this.logsScrollBox);
273
303
  // Track focus changes on scroll box
274
304
  this.logsScrollBox.on("focused", () => {
275
305
  this.debugLog(`[FOCUS] logsScrollBox focused`);
306
+ this.logSelectionDebug("[FOCUS] logsScrollBox focused");
276
307
  });
277
308
  this.logsScrollBox.on("blurred", () => {
278
309
  this.debugLog(`[FOCUS] logsScrollBox blurred`);
310
+ this.logSelectionDebug("[FOCUS] logsScrollBox blurred");
279
311
  });
280
312
  // Container for log lines inside scroll box - provides valid X positions
281
313
  this.logsContainer = new BoxRenderable(this.renderer, {
282
314
  id: "logs-content",
283
315
  flexDirection: "column",
284
- width: "100%"
316
+ width: this.logContentWidth
285
317
  });
286
318
  this.logsScrollBox.add(this.logsContainer);
287
319
  // Bottom status line - show "agent has access" hint in both modes
@@ -316,12 +348,27 @@ class D3kTUI {
316
348
  this.renderer.root.onSizeChange = () => {
317
349
  if (!this.renderer || this.isRebuilding)
318
350
  return;
351
+ this.logContentWidth = Math.max(20, this.renderer.width - 6);
319
352
  const nextCompact = this.renderer.width < 80;
320
353
  const nextVeryCompact = this.renderer.width < 60 || this.renderer.height < 15;
321
354
  if (nextCompact !== this.isCompact || nextVeryCompact !== this.isVeryCompact) {
322
355
  this.rebuildUI();
323
356
  return;
324
357
  }
358
+ if (this.logsContainer) {
359
+ this.logsContainer.width = this.logContentWidth;
360
+ }
361
+ if (this.logsScrollBox) {
362
+ this.logsScrollBox.content.left = 0;
363
+ this.logsScrollBox.content.top = 0;
364
+ this.logsScrollBox.content.width = this.logContentWidth;
365
+ }
366
+ for (const child of this.logsContainer?.getChildren() || []) {
367
+ if (child instanceof TextRenderable) {
368
+ child.width = this.logContentWidth;
369
+ child.left = 0;
370
+ }
371
+ }
325
372
  this.renderer.requestRender();
326
373
  };
327
374
  }
@@ -378,11 +425,12 @@ class D3kTUI {
378
425
  headerBox.add(infoCol);
379
426
  const protocol = this.useHttps ? "https" : "http";
380
427
  const portStatus = this.portConfirmed ? "" : " ...";
428
+ const displayedAppUrl = this.appUrl || `${protocol}://localhost:${this.appPort}`;
381
429
  const appLine = new TextRenderable(this.renderer, {
382
430
  id: "app-url",
383
431
  content: this.portConfirmed
384
- ? t `${cyan(`🌐 App: ${protocol}://localhost:${this.appPort}`)}`
385
- : t `${cyan(`🌐 App: ${protocol}://localhost:${this.appPort}`)}${yellow(portStatus)}`
432
+ ? t `${cyan(`🌐 App: ${displayedAppUrl}`)}`
433
+ : t `${cyan(`🌐 App: ${displayedAppUrl}`)}${yellow(portStatus)}`
386
434
  });
387
435
  infoCol.add(appLine);
388
436
  // Show logs path with ~/ instead of full home path
@@ -497,6 +545,7 @@ class D3kTUI {
497
545
  this.baseLogId = undefined;
498
546
  this.refreshLogs();
499
547
  }
548
+ this.clearLogSelection();
500
549
  this.renderer?.clearSelection();
501
550
  return;
502
551
  }
@@ -530,7 +579,7 @@ class D3kTUI {
530
579
  const focusedId = focusedRenderable?.id || "(none)";
531
580
  const selectionContainer = this.renderer?.getSelectionContainer();
532
581
  const containerId = selectionContainer?.id || "(none)";
533
- const message = `hasSelection: ${hasSelection}, isSelecting: ${selection?.isSelecting}, renderables: ${selection?.selectedRenderables?.length ?? 0}, focus: ${focusedId}, container: ${containerId}, text: "${selectedText.slice(0, 30)}"`;
582
+ const message = `hasSelection: ${hasSelection}, isDragging: ${selection?.isDragging}, renderables: ${selection?.selectedRenderables?.length ?? 0}, focus: ${focusedId}, container: ${containerId}, text: "${selectedText.slice(0, 30)}"`;
534
583
  // Write to file if debug mode is on
535
584
  this.debugLog(`[SELECTION_STATE] ${message}`);
536
585
  // Always show in UI
@@ -588,9 +637,11 @@ class D3kTUI {
588
637
  this.selectionCopyHandler = (selection) => {
589
638
  if (!selection) {
590
639
  this.lastCopiedSelection = null;
640
+ this.logSelectionDebug("[SELECTION_EVENT] selection cleared");
591
641
  return;
592
642
  }
593
- if (selection.isSelecting || !selection.isActive)
643
+ this.logSelectionDebug(`[SELECTION_EVENT] ${this.describeSelection(selection)}`);
644
+ if (selection.isDragging || !selection.isActive)
594
645
  return;
595
646
  const text = selection.getSelectedText();
596
647
  if (!text || text.trim().length === 0)
@@ -599,14 +650,12 @@ class D3kTUI {
599
650
  return;
600
651
  this.lastCopiedSelection = text;
601
652
  this.copyToClipboard(text);
653
+ this.logSelectionDebug(`[SELECTION_COPY] length=${text.length} text="${text.slice(0, 120)}"`);
602
654
  };
603
655
  this.renderer.on("selection", this.selectionCopyHandler);
604
656
  }
605
657
  copySelectionToClipboard() {
606
- if (!this.renderer)
607
- return;
608
- const selection = this.renderer.getSelection();
609
- const text = selection?.getSelectedText();
658
+ const text = this.getSelectedLogText() || this.renderer?.getSelection()?.getSelectedText();
610
659
  if (!text || text.trim().length === 0)
611
660
  return;
612
661
  this.lastCopiedSelection = text;
@@ -708,16 +757,18 @@ class D3kTUI {
708
757
  id: `log-${log.id}`,
709
758
  content: formatted,
710
759
  wrapMode: "none",
711
- selectable: true,
760
+ selectable: false,
712
761
  selectionBg: RGBA.fromInts(70, 130, 180), // Steel blue highlight
713
762
  selectionFg: RGBA.fromInts(255, 255, 255), // White text on selection
714
- width: "100%",
763
+ width: this.logContentWidth,
764
+ left: 0,
715
765
  flexShrink: 0,
716
766
  onMouseDown: (event) => {
717
767
  // Record start position for click detection
718
768
  clickStartX = event.x;
719
769
  clickStartY = event.y;
720
770
  this.debugLog(`[TEXT_DOWN] id=${logLine.id} screen(${event.x},${event.y}) pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height})`);
771
+ this.logSelectionDebug(`[TEXT_DOWN] id=${logLine.id} screen(${event.x},${event.y}) pos(${logLine.x},${logLine.y}) size(${logLine.width}x${logLine.height})`);
721
772
  },
722
773
  onMouseUp: (event) => {
723
774
  // Check if this was a click (no movement) vs a drag
@@ -728,6 +779,7 @@ class D3kTUI {
728
779
  const textLocalX = event.x - (logLine.x || 0);
729
780
  const TIMESTAMP_COLUMN_WIDTH = 15; // "[HH:MM:SS.mmm] " is ~14 chars
730
781
  if (textLocalX <= TIMESTAMP_COLUMN_WIDTH) {
782
+ this.clearLogSelection();
731
783
  // Toggle baseline timestamp on click
732
784
  const timestampMs = parseTimestampToMs(log.timestamp);
733
785
  if (timestampMs !== null) {
@@ -750,11 +802,16 @@ class D3kTUI {
750
802
  clickStartY = -1;
751
803
  }
752
804
  });
805
+ this.logRenderables.set(log.id, logLine);
753
806
  this.logsContainer.add(logLine);
807
+ this.updateLogLineSelectionStyle(log.id);
754
808
  }
755
809
  // Force render to update hit grid after adding new logs
756
810
  // This prevents stale hit grid when new logs trigger scroll/layout changes
757
811
  this.renderer.requestRender();
812
+ setTimeout(() => {
813
+ this.logLogPaneMetrics(`after-add-${newLogs.length}`);
814
+ }, 0);
758
815
  }
759
816
  findLogLineAt(x, y) {
760
817
  if (!this.logsContainer)
@@ -769,6 +826,36 @@ class D3kTUI {
769
826
  }
770
827
  return null;
771
828
  }
829
+ findLogLineAtRow(y) {
830
+ if (!this.logsContainer)
831
+ return null;
832
+ for (const child of this.logsContainer.getChildren()) {
833
+ if (!(child instanceof TextRenderable))
834
+ continue;
835
+ const withinY = y >= child.y && y < child.y + Math.max(child.height, 1);
836
+ if (withinY)
837
+ return child;
838
+ }
839
+ return null;
840
+ }
841
+ getSelectionLogLine(event) {
842
+ const target = event.target;
843
+ if (target?.id?.startsWith("log-")) {
844
+ const child = this.logsContainer?.getChildren().find((entry) => entry.id === target.id);
845
+ if (child instanceof TextRenderable) {
846
+ this.logSelectionDebug(`[HIT] direct target=${target.id} pos(${child.x},${child.y}) size(${child.width}x${child.height})`);
847
+ return child;
848
+ }
849
+ }
850
+ const fallback = this.findLogLineAt(event.x, event.y);
851
+ if (fallback) {
852
+ this.logSelectionDebug(`[HIT] fallback screen(${event.x},${event.y}) -> ${fallback.id} pos(${fallback.x},${fallback.y}) size(${fallback.width}x${fallback.height})`);
853
+ return fallback;
854
+ }
855
+ const rowFallback = this.findLogLineAtRow(event.y);
856
+ this.logSelectionDebug(`[HIT] row-fallback screen(${event.x},${event.y}) -> ${rowFallback?.id || "(none)"} pos(${rowFallback?.x ?? "?"},${rowFallback?.y ?? "?"}) size(${rowFallback?.width ?? "?"}x${rowFallback?.height ?? "?"})`);
857
+ return rowFallback;
858
+ }
772
859
  refreshLogs() {
773
860
  if (!this.renderer || !this.logsContainer)
774
861
  return;
@@ -776,9 +863,107 @@ class D3kTUI {
776
863
  for (const child of this.logsContainer.getChildren()) {
777
864
  child.destroy();
778
865
  }
866
+ this.logRenderables.clear();
779
867
  // Re-add filtered logs
780
868
  const filteredLogs = this.logs.filter((log) => log.id > this.clearFromLogId);
781
869
  this.addLogLines(filteredLogs);
870
+ this.normalizeLogSelection();
871
+ }
872
+ beginLogSelection(logRenderableId, x, y) {
873
+ const logId = this.parseLogRenderableId(logRenderableId);
874
+ if (logId === null)
875
+ return;
876
+ this.logSelection = {
877
+ anchorLogId: logId,
878
+ focusLogId: logId,
879
+ startX: x,
880
+ startY: y,
881
+ hasDragged: false
882
+ };
883
+ this.updateLogSelectionStyles();
884
+ }
885
+ updateLogSelection(logRenderableId, x, y) {
886
+ if (!this.logSelection)
887
+ return;
888
+ const logId = logRenderableId ? this.parseLogRenderableId(logRenderableId) : null;
889
+ if (logId !== null) {
890
+ this.logSelection.focusLogId = logId;
891
+ }
892
+ if (x !== this.logSelection.startX || y !== this.logSelection.startY) {
893
+ this.logSelection.hasDragged = true;
894
+ }
895
+ this.updateLogSelectionStyles();
896
+ }
897
+ finishLogSelection(logRenderableId, x, y) {
898
+ if (!this.logSelection)
899
+ return;
900
+ this.updateLogSelection(logRenderableId, x, y);
901
+ if (!this.logSelection?.hasDragged) {
902
+ this.clearLogSelection();
903
+ return;
904
+ }
905
+ const text = this.getSelectedLogText();
906
+ if (!text || text.trim().length === 0 || text === this.lastCopiedSelection)
907
+ return;
908
+ this.lastCopiedSelection = text;
909
+ this.copyToClipboard(text);
910
+ this.logSelectionDebug(`[SELECTION_COPY] length=${text.length} text="${text.slice(0, 120)}"`);
911
+ }
912
+ clearLogSelection() {
913
+ if (!this.logSelection)
914
+ return;
915
+ this.logSelection = null;
916
+ this.updateLogSelectionStyles();
917
+ }
918
+ normalizeLogSelection() {
919
+ if (!this.logSelection)
920
+ return;
921
+ const selectedLogIds = this.getSelectedLogIds();
922
+ if (selectedLogIds.length === 0) {
923
+ this.logSelection = null;
924
+ this.updateLogSelectionStyles();
925
+ }
926
+ }
927
+ getSelectedLogIds() {
928
+ if (!this.logSelection)
929
+ return [];
930
+ const visibleLogIds = [...this.logRenderables.keys()].sort((a, b) => a - b);
931
+ const anchorIndex = visibleLogIds.indexOf(this.logSelection.anchorLogId);
932
+ const focusIndex = visibleLogIds.indexOf(this.logSelection.focusLogId);
933
+ if (anchorIndex === -1 || focusIndex === -1)
934
+ return [];
935
+ const startIndex = Math.min(anchorIndex, focusIndex);
936
+ const endIndex = Math.max(anchorIndex, focusIndex);
937
+ return visibleLogIds.slice(startIndex, endIndex + 1);
938
+ }
939
+ getSelectedLogText() {
940
+ const selectedIds = new Set(this.getSelectedLogIds());
941
+ if (selectedIds.size === 0)
942
+ return "";
943
+ return this.logs
944
+ .filter((log) => log.id > this.clearFromLogId && selectedIds.has(log.id))
945
+ .map((log) => log.content)
946
+ .join("\n");
947
+ }
948
+ updateLogSelectionStyles() {
949
+ for (const logId of this.logRenderables.keys()) {
950
+ this.updateLogLineSelectionStyle(logId);
951
+ }
952
+ this.renderer?.requestRender();
953
+ }
954
+ updateLogLineSelectionStyle(logId) {
955
+ const renderable = this.logRenderables.get(logId);
956
+ if (!renderable)
957
+ return;
958
+ const selectedIds = new Set(this.getSelectedLogIds());
959
+ renderable.bg = selectedIds.has(logId) ? this.logSelectionBackground : undefined;
960
+ }
961
+ parseLogRenderableId(logRenderableId) {
962
+ const match = logRenderableId.match(/^log-(\d+)$/);
963
+ if (!match)
964
+ return null;
965
+ const logId = Number.parseInt(match[1], 10);
966
+ return Number.isNaN(logId) ? null : logId;
782
967
  }
783
968
  updateStatusDisplay() {
784
969
  if (!this.statusText || !this.renderer)
@@ -809,6 +994,10 @@ class D3kTUI {
809
994
  this.portConfirmed = true;
810
995
  this.rebuildUI();
811
996
  }
997
+ setAppUrl(url) {
998
+ this.appUrl = url;
999
+ this.rebuildUI();
1000
+ }
812
1001
  setUpdateInfo(info) {
813
1002
  this.updateInfo = info;
814
1003
  this.updateStatusDisplay();
@@ -835,6 +1024,58 @@ class D3kTUI {
835
1024
  // Ignore write errors
836
1025
  }
837
1026
  }
1027
+ initializeSelectionDebugLog() {
1028
+ if (!this.selectionDebugEnabled)
1029
+ return;
1030
+ try {
1031
+ mkdirSync(join(process.env.HOME || tmpdir(), ".d3k"), { recursive: true });
1032
+ writeFileSync(this.selectionDebugLogFile, [
1033
+ `Selection debug started at ${new Date().toISOString()}`,
1034
+ `project=${this.options.projectName || "(unknown)"}`,
1035
+ `logFile=${this.options.logFile}`,
1036
+ `appPort=${this.options.appPort}`,
1037
+ ""
1038
+ ].join("\n"));
1039
+ }
1040
+ catch {
1041
+ // Ignore write errors
1042
+ }
1043
+ }
1044
+ logSelectionDebug(message) {
1045
+ if (!this.selectionDebugEnabled)
1046
+ return;
1047
+ const timestamp = new Date().toISOString();
1048
+ try {
1049
+ appendFileSync(this.selectionDebugLogFile, `[${timestamp}] ${message}\n`);
1050
+ }
1051
+ catch {
1052
+ // Ignore write errors
1053
+ }
1054
+ }
1055
+ describeSelection(selection = this.renderer?.getSelection() || null) {
1056
+ const logSelectionIds = this.getSelectedLogIds();
1057
+ const logSelectionText = this.getSelectedLogText();
1058
+ const logSelectionDescription = logSelectionIds.length === 0
1059
+ ? "logSelection=(none)"
1060
+ : `logSelection={dragging:${Boolean(this.logSelection?.hasDragged)},lines:${logSelectionIds.length},textLen:${logSelectionText.length}}`;
1061
+ if (!selection) {
1062
+ return logSelectionDescription;
1063
+ }
1064
+ const text = selection.getSelectedText();
1065
+ return `${logSelectionDescription} nativeSelection={active:${selection.isActive},dragging:${selection.isDragging},renderables:${selection.selectedRenderables.length},textLen:${text.length}}`;
1066
+ }
1067
+ logLogPaneMetrics(reason) {
1068
+ if (!this.selectionDebugEnabled)
1069
+ return;
1070
+ const children = this.logsContainer?.getChildren() || [];
1071
+ const logLines = children.filter((child) => child instanceof TextRenderable);
1072
+ const sample = logLines
1073
+ .slice(0, 3)
1074
+ .map((line) => `${line.id}@(${line.x},${line.y}) ${line.width}x${line.height}`)
1075
+ .join(", ");
1076
+ const last = logLines.at(-1);
1077
+ 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)"}`);
1078
+ }
838
1079
  shutdown() {
839
1080
  unwatchFile(this.options.logFile);
840
1081
  if (this.renderer) {