pilotswarm-cli 0.1.12 → 0.1.13

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.
@@ -0,0 +1,899 @@
1
+ import React from "react";
2
+ import { spawnSync } from "node:child_process";
3
+ import { Box, Text } from "ink";
4
+ import { parseTerminalMarkupRuns } from "pilotswarm-ui-core";
5
+
6
+ const MAX_PROMPT_INPUT_ROWS = 3;
7
+ const SELECTION_BACKGROUND = "white";
8
+ const SELECTION_FOREGROUND = "black";
9
+
10
+ function clampValue(value, min, max) {
11
+ return Math.max(min, Math.min(max, value));
12
+ }
13
+
14
+ function createEmptySelection() {
15
+ return {
16
+ paneId: null,
17
+ anchor: null,
18
+ head: null,
19
+ dragging: false,
20
+ moved: false,
21
+ };
22
+ }
23
+
24
+ const tuiPlatformRuntime = {
25
+ paneRegistry: new Map(),
26
+ selection: createEmptySelection(),
27
+ renderInvalidator: null,
28
+ };
29
+
30
+ function trimText(value, width) {
31
+ if (width <= 0) return "";
32
+ const text = String(value || "");
33
+ return text.length > width ? text.slice(0, width) : text;
34
+ }
35
+
36
+ function trimRuns(runs, width) {
37
+ if (width <= 0) return [];
38
+ const output = [];
39
+ let remaining = width;
40
+
41
+ for (const run of runs || []) {
42
+ if (remaining <= 0) break;
43
+ const text = String(run?.text || "");
44
+ if (!text) continue;
45
+ const chunk = text.slice(0, remaining);
46
+ if (!chunk) continue;
47
+ output.push({
48
+ ...run,
49
+ text: chunk,
50
+ });
51
+ remaining -= chunk.length;
52
+ }
53
+
54
+ return output;
55
+ }
56
+
57
+ function normalizeLines(lines) {
58
+ const normalized = [];
59
+ for (const line of lines || []) {
60
+ if (line?.kind === "markup") {
61
+ const parsed = parseTerminalMarkupRuns(line.value || "");
62
+ for (const parsedLine of parsed) {
63
+ normalized.push({ kind: "runs", runs: parsedLine });
64
+ }
65
+ continue;
66
+ }
67
+ if (Array.isArray(line)) {
68
+ normalized.push({ kind: "runs", runs: line });
69
+ continue;
70
+ }
71
+ normalized.push({ kind: "text", ...line });
72
+ }
73
+ return normalized;
74
+ }
75
+
76
+ function wrapTextLine(line, width) {
77
+ const text = String(line?.text || "");
78
+ if (!text) {
79
+ return [{
80
+ kind: "text",
81
+ ...line,
82
+ text: "",
83
+ }];
84
+ }
85
+
86
+ const slices = computeWrappedSlices(text, width);
87
+ const wrapped = [];
88
+ for (const slice of slices) {
89
+ wrapped.push({
90
+ kind: "text",
91
+ ...line,
92
+ text: text.slice(slice.start, slice.end),
93
+ });
94
+ }
95
+ return wrapped;
96
+ }
97
+
98
+ function findWhitespaceWrapPoint(text, start, maxEnd) {
99
+ if (maxEnd >= text.length && maxEnd > start) return maxEnd;
100
+ if (
101
+ maxEnd < text.length
102
+ && /\s/u.test(text[maxEnd] || "")
103
+ && maxEnd > start
104
+ && /\S/u.test(text[maxEnd - 1] || "")
105
+ ) {
106
+ return maxEnd;
107
+ }
108
+
109
+ for (let index = Math.min(maxEnd - 1, text.length - 1); index > start; index -= 1) {
110
+ if (!/\s/u.test(text[index] || "")) continue;
111
+ if (!/\S/u.test(text[index - 1] || "")) continue;
112
+ return index;
113
+ }
114
+
115
+ return -1;
116
+ }
117
+
118
+ function computeWrappedSlices(text, width) {
119
+ const safeText = String(text || "");
120
+ const safeWidth = Math.max(1, Number(width) || 1);
121
+ if (!safeText) return [{ start: 0, end: 0 }];
122
+
123
+ const slices = [];
124
+ let start = 0;
125
+ while (start < safeText.length) {
126
+ const maxEnd = Math.min(start + safeWidth, safeText.length);
127
+ let end = maxEnd;
128
+
129
+ if (maxEnd < safeText.length) {
130
+ const wrapPoint = findWhitespaceWrapPoint(safeText, start, maxEnd);
131
+ if (wrapPoint > start) {
132
+ end = wrapPoint;
133
+ }
134
+ }
135
+
136
+ if (end <= start) {
137
+ end = maxEnd;
138
+ }
139
+
140
+ slices.push({ start, end });
141
+ start = end;
142
+ while (start < safeText.length && /\s/u.test(safeText[start] || "")) {
143
+ start += 1;
144
+ }
145
+ }
146
+
147
+ return slices.length > 0 ? slices : [{ start: 0, end: 0 }];
148
+ }
149
+
150
+ function sliceRunsByRange(runs, start, end) {
151
+ const output = [];
152
+ let cursor = 0;
153
+
154
+ for (const run of runs || []) {
155
+ const text = String(run?.text || "");
156
+ if (!text) continue;
157
+ const runStart = cursor;
158
+ const runEnd = cursor + text.length;
159
+ const overlapStart = Math.max(start, runStart);
160
+ const overlapEnd = Math.min(end, runEnd);
161
+ if (overlapStart < overlapEnd) {
162
+ output.push({
163
+ ...run,
164
+ text: text.slice(overlapStart - runStart, overlapEnd - runStart),
165
+ });
166
+ }
167
+ cursor = runEnd;
168
+ }
169
+
170
+ return output;
171
+ }
172
+
173
+ function wrapRunsLine(runs, width) {
174
+ const text = (runs || []).map((run) => String(run?.text || "")).join("");
175
+ if (!text) return [{ kind: "text", text: "" }];
176
+
177
+ return computeWrappedSlices(text, width).map((slice) => ({
178
+ kind: "runs",
179
+ runs: sliceRunsByRange(runs, slice.start, slice.end),
180
+ }));
181
+ }
182
+
183
+ function wrapNormalizedLines(lines, width) {
184
+ const safeWidth = Math.max(1, Number(width) || 1);
185
+ const wrapped = [];
186
+
187
+ for (const line of lines || []) {
188
+ if (!line) {
189
+ wrapped.push(null);
190
+ continue;
191
+ }
192
+ if (line.kind === "runs") {
193
+ wrapped.push(...wrapRunsLine(line.runs, safeWidth));
194
+ continue;
195
+ }
196
+ wrapped.push(...wrapTextLine(line, safeWidth));
197
+ }
198
+
199
+ return wrapped;
200
+ }
201
+
202
+ function normalizeTitleRuns(title, fallbackColor) {
203
+ if (Array.isArray(title)) {
204
+ return title.map((run) => ({
205
+ text: String(run?.text || ""),
206
+ color: run?.color || fallbackColor,
207
+ backgroundColor: run?.backgroundColor || undefined,
208
+ bold: Boolean(run?.bold),
209
+ underline: Boolean(run?.underline),
210
+ }));
211
+ }
212
+ return [{
213
+ text: String(title || ""),
214
+ color: fallbackColor,
215
+ bold: true,
216
+ }];
217
+ }
218
+
219
+ function titleRunLength(runs) {
220
+ return (runs || []).reduce((sum, run) => sum + String(run?.text || "").length, 0);
221
+ }
222
+
223
+ function flattenTitleText(title) {
224
+ if (Array.isArray(title)) {
225
+ return title.map((run) => String(run?.text || "")).join("").trim();
226
+ }
227
+ return String(title || "").trim();
228
+ }
229
+
230
+ function renderInlineRuns(runs, keyPrefix = "run") {
231
+ return runs.map((run, index) => React.createElement(Text, {
232
+ key: `${keyPrefix}:${index}`,
233
+ color: run.color || undefined,
234
+ backgroundColor: run.backgroundColor || undefined,
235
+ bold: Boolean(run.bold),
236
+ underline: Boolean(run.underline),
237
+ dimColor: run.color === "gray",
238
+ }, run.text || ""));
239
+ }
240
+
241
+ function lineToRuns(line, contentWidth) {
242
+ if (!line) return [];
243
+ if (line.kind === "runs") {
244
+ return trimRuns(line.runs, contentWidth);
245
+ }
246
+ const text = trimText(line.text || "", contentWidth);
247
+ if (!text) return [];
248
+ return [{
249
+ text,
250
+ color: line.color || undefined,
251
+ backgroundColor: line.backgroundColor || undefined,
252
+ bold: Boolean(line.bold),
253
+ underline: Boolean(line.underline),
254
+ }];
255
+ }
256
+
257
+ function flattenLineText(line, contentWidth) {
258
+ if (!line) return "";
259
+ if (line.kind === "runs") {
260
+ return trimRuns(line.runs, contentWidth).map((run) => run?.text || "").join("");
261
+ }
262
+ return trimText(line.text || "", contentWidth);
263
+ }
264
+
265
+ function normalizeRowSelection(selectionRange, lineLength) {
266
+ if (!selectionRange) return null;
267
+ const safeLength = Math.max(0, Number(lineLength) || 0);
268
+ const start = clampValue(selectionRange.start ?? 0, 0, safeLength);
269
+ const end = Number.isFinite(selectionRange.end)
270
+ ? clampValue(selectionRange.end, 0, safeLength)
271
+ : safeLength;
272
+ if (end <= start) return null;
273
+ return { start, end };
274
+ }
275
+
276
+ function applySelectionToRuns(runs, selectionRange) {
277
+ if (!selectionRange) return runs;
278
+ const selectedRuns = [];
279
+ let cursor = 0;
280
+
281
+ for (const run of runs || []) {
282
+ const text = String(run?.text || "");
283
+ if (!text) continue;
284
+ const runStart = cursor;
285
+ const runEnd = cursor + text.length;
286
+ const overlapStart = Math.max(runStart, selectionRange.start);
287
+ const overlapEnd = Math.min(runEnd, selectionRange.end);
288
+
289
+ if (overlapStart <= runStart && overlapEnd >= runEnd) {
290
+ selectedRuns.push({
291
+ ...run,
292
+ color: SELECTION_FOREGROUND,
293
+ backgroundColor: SELECTION_BACKGROUND,
294
+ });
295
+ } else if (overlapStart < overlapEnd) {
296
+ const before = text.slice(0, overlapStart - runStart);
297
+ const inside = text.slice(overlapStart - runStart, overlapEnd - runStart);
298
+ const after = text.slice(overlapEnd - runStart);
299
+ if (before) selectedRuns.push({ ...run, text: before });
300
+ if (inside) {
301
+ selectedRuns.push({
302
+ ...run,
303
+ text: inside,
304
+ color: SELECTION_FOREGROUND,
305
+ backgroundColor: SELECTION_BACKGROUND,
306
+ });
307
+ }
308
+ if (after) selectedRuns.push({ ...run, text: after });
309
+ } else {
310
+ selectedRuns.push(run);
311
+ }
312
+
313
+ cursor = runEnd;
314
+ }
315
+
316
+ return selectedRuns;
317
+ }
318
+
319
+ function buildScrollIndicator(totalLines, contentHeight, startIndex) {
320
+ if (totalLines <= contentHeight) return null;
321
+ const thumbSize = Math.max(1, Math.round((contentHeight / Math.max(1, totalLines)) * contentHeight));
322
+ const thumbTravel = Math.max(0, contentHeight - thumbSize);
323
+ const contentTravel = Math.max(1, totalLines - contentHeight);
324
+ const thumbStart = thumbTravel === 0
325
+ ? 0
326
+ : Math.round((Math.max(0, startIndex) / contentTravel) * thumbTravel);
327
+ return {
328
+ thumbStart,
329
+ thumbEnd: thumbStart + thumbSize - 1,
330
+ };
331
+ }
332
+
333
+ function renderPanelRow(line, rowKey, contentWidth, borderColor, scrollIndicator, fillColor, selectionRange, scrollRowIndex = rowKey) {
334
+ const scrollChar = scrollIndicator
335
+ ? (scrollRowIndex >= scrollIndicator.thumbStart && scrollRowIndex <= scrollIndicator.thumbEnd ? "█" : "░")
336
+ : " ";
337
+ const lineText = flattenLineText(line, contentWidth);
338
+ const normalizedSelection = normalizeRowSelection(selectionRange, lineText.length);
339
+ const selectedRuns = applySelectionToRuns(lineToRuns(line, contentWidth), normalizedSelection);
340
+
341
+ return React.createElement(Box, { key: `row:${rowKey}`, flexDirection: "row" },
342
+ React.createElement(Text, { color: borderColor }, "│ "),
343
+ React.createElement(Box, { width: contentWidth, backgroundColor: fillColor || undefined },
344
+ !line
345
+ ? React.createElement(Text, null, " ".repeat(contentWidth))
346
+ : selectedRuns.length > 0
347
+ ? renderInlineRuns(selectedRuns, `inline:${rowKey}`)
348
+ : React.createElement(Text, null, "")),
349
+ React.createElement(Text, { color: scrollIndicator ? "gray" : undefined, dimColor: Boolean(scrollIndicator) }, scrollChar),
350
+ React.createElement(Text, { color: borderColor }, "│"));
351
+ }
352
+
353
+ function renderBorderTop(title, color, width) {
354
+ const safeWidth = Math.max(8, Number(width) || 40);
355
+ const safeTitleRuns = trimRuns(normalizeTitleRuns(title, color), Math.max(1, safeWidth - 6));
356
+ const fill = Math.max(0, safeWidth - titleRunLength(safeTitleRuns) - 5);
357
+
358
+ return React.createElement(Box, null,
359
+ React.createElement(Text, { color }, "╭─ "),
360
+ renderInlineRuns(safeTitleRuns, "title"),
361
+ React.createElement(Text, { color }, ` ${"─".repeat(fill)}╮`));
362
+ }
363
+
364
+ function renderBorderBottom(color, width) {
365
+ const safeWidth = Math.max(8, Number(width) || 40);
366
+ return React.createElement(Text, { color }, `╰${"─".repeat(Math.max(0, safeWidth - 2))}╯`);
367
+ }
368
+
369
+ function compareSelectionPoints(left, right) {
370
+ if ((left?.row ?? 0) !== (right?.row ?? 0)) {
371
+ return (left?.row ?? 0) - (right?.row ?? 0);
372
+ }
373
+ return (left?.col ?? 0) - (right?.col ?? 0);
374
+ }
375
+
376
+ function normalizeSelectionEndpoints(anchor, head) {
377
+ if (!anchor || !head) return null;
378
+ return compareSelectionPoints(anchor, head) <= 0
379
+ ? { start: anchor, end: head }
380
+ : { start: head, end: anchor };
381
+ }
382
+
383
+ function getSelectionRangeForPaneRow(paneId, rowIndex) {
384
+ const selection = tuiPlatformRuntime.selection;
385
+ if (!selection?.moved || selection.paneId !== paneId) return null;
386
+ const ordered = normalizeSelectionEndpoints(selection.anchor, selection.head);
387
+ if (!ordered) return null;
388
+ if (rowIndex < ordered.start.row || rowIndex > ordered.end.row) return null;
389
+ if (ordered.start.row === ordered.end.row) {
390
+ return {
391
+ start: Math.min(ordered.start.col, ordered.end.col),
392
+ end: Math.max(ordered.start.col, ordered.end.col) + 1,
393
+ };
394
+ }
395
+ if (rowIndex === ordered.start.row) {
396
+ return { start: ordered.start.col, end: Number.POSITIVE_INFINITY };
397
+ }
398
+ if (rowIndex === ordered.end.row) {
399
+ return { start: 0, end: ordered.end.col + 1 };
400
+ }
401
+ return { start: 0, end: Number.POSITIVE_INFINITY };
402
+ }
403
+
404
+ function normalizePaneFrame(frame, width, height) {
405
+ if (!frame) return null;
406
+ return {
407
+ x: Math.max(0, Number(frame.x) || 0),
408
+ y: Math.max(0, Number(frame.y) || 0),
409
+ width: Math.max(8, Number(frame.width) || width || 8),
410
+ height: Math.max(4, Number(frame.height) || height || 4),
411
+ };
412
+ }
413
+
414
+ function getPaneInnerBounds(pane) {
415
+ const frame = pane?.frame;
416
+ if (!frame) return null;
417
+ const width = Math.max(1, Number(pane?.contentWidth) || Math.max(1, frame.width - 4));
418
+ const height = Math.max(1, Number(pane?.contentHeight) || Math.max(1, frame.height - 2));
419
+ return {
420
+ left: frame.x + 2,
421
+ right: frame.x + 1 + width,
422
+ top: frame.y + 1,
423
+ bottom: frame.y + height,
424
+ width,
425
+ height,
426
+ };
427
+ }
428
+
429
+ function registerSelectablePaneSnapshot(snapshot) {
430
+ if (!snapshot?.paneId) return;
431
+ tuiPlatformRuntime.paneRegistry.set(snapshot.paneId, snapshot);
432
+ }
433
+
434
+ function requestTuiRender() {
435
+ try {
436
+ tuiPlatformRuntime.renderInvalidator?.();
437
+ } catch {}
438
+ }
439
+
440
+ function findPaneHit(x, y) {
441
+ const panes = Array.from(tuiPlatformRuntime.paneRegistry.values()).reverse();
442
+ for (const pane of panes) {
443
+ const bounds = getPaneInnerBounds(pane);
444
+ if (!bounds) continue;
445
+ if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) continue;
446
+ return {
447
+ pane,
448
+ point: {
449
+ row: clampValue(y - bounds.top, 0, Math.max(0, bounds.height - 1)),
450
+ col: clampValue(x - bounds.left, 0, Math.max(0, bounds.width - 1)),
451
+ },
452
+ };
453
+ }
454
+ return null;
455
+ }
456
+
457
+ function projectPointIntoPane(pane, x, y) {
458
+ const bounds = getPaneInnerBounds(pane);
459
+ if (!bounds) return null;
460
+ return {
461
+ row: clampValue(y - bounds.top, 0, Math.max(0, bounds.height - 1)),
462
+ col: clampValue(x - bounds.left, 0, Math.max(0, bounds.width - 1)),
463
+ };
464
+ }
465
+
466
+ function normalizeExtractedSelectionText(lines) {
467
+ const normalized = [...(lines || [])];
468
+ while (normalized.length > 0 && !normalized[normalized.length - 1]) {
469
+ normalized.pop();
470
+ }
471
+ return normalized.join("\n");
472
+ }
473
+
474
+ function extractSelectionTextFromPane(pane, anchor, head) {
475
+ const ordered = normalizeSelectionEndpoints(anchor, head);
476
+ if (!ordered) return "";
477
+ const visibleLines = Array.isArray(pane?.visibleLines) ? pane.visibleLines : [];
478
+ const contentWidth = Math.max(1, Number(pane?.contentWidth) || 1);
479
+ const output = [];
480
+
481
+ for (let rowIndex = ordered.start.row; rowIndex <= ordered.end.row; rowIndex += 1) {
482
+ const lineText = flattenLineText(visibleLines[rowIndex], contentWidth);
483
+ let start = 0;
484
+ let end = lineText.length;
485
+ if (rowIndex === ordered.start.row) start = clampValue(ordered.start.col, 0, lineText.length);
486
+ if (rowIndex === ordered.end.row) end = clampValue(ordered.end.col + 1, 0, lineText.length);
487
+ output.push(lineText.slice(start, end).replace(/\s+$/u, ""));
488
+ }
489
+
490
+ return normalizeExtractedSelectionText(output);
491
+ }
492
+
493
+ function copyTextToClipboard(text) {
494
+ if (!text) {
495
+ return { ok: false, error: "Selection is empty." };
496
+ }
497
+ const commands = process.platform === "darwin"
498
+ ? [["pbcopy", []]]
499
+ : process.platform === "win32"
500
+ ? [["clip", []]]
501
+ : process.env.WAYLAND_DISPLAY
502
+ ? [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]]
503
+ : [["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]], ["wl-copy", []]];
504
+
505
+ let lastError = null;
506
+ for (const [command, args] of commands) {
507
+ try {
508
+ const result = spawnSync(command, args, {
509
+ input: text,
510
+ encoding: "utf8",
511
+ stdio: ["pipe", "ignore", "pipe"],
512
+ });
513
+ if (result.status === 0) return { ok: true };
514
+ lastError = result.stderr?.trim?.() || `${command} exited with status ${result.status}`;
515
+ } catch (error) {
516
+ lastError = error?.message || String(error);
517
+ }
518
+ }
519
+
520
+ return { ok: false, error: lastError || "No clipboard command succeeded." };
521
+ }
522
+
523
+ function linesToElements(lines) {
524
+ return normalizeLines(lines).map((line, index) => {
525
+ if (line.kind === "runs") {
526
+ return React.createElement(Box, { key: `line:${index}` }, renderInlineRuns(line.runs, `inline:${index}`));
527
+ }
528
+ return React.createElement(Text, {
529
+ key: `text:${index}`,
530
+ color: line.color || undefined,
531
+ backgroundColor: line.backgroundColor || undefined,
532
+ bold: Boolean(line.bold),
533
+ dimColor: line.color === "gray",
534
+ }, line.text || "");
535
+ });
536
+ }
537
+
538
+ function Root({ children }) {
539
+ return React.createElement(Box, {
540
+ flexDirection: "column",
541
+ height: process.stdout.rows || 40,
542
+ width: process.stdout.columns || 120,
543
+ }, children);
544
+ }
545
+
546
+ function Row({ children, ...props }) {
547
+ return React.createElement(Box, { flexDirection: "row", ...props }, children);
548
+ }
549
+
550
+ function Column({ children, ...props }) {
551
+ return React.createElement(Box, { flexDirection: "column", ...props }, children);
552
+ }
553
+
554
+ function Header({ title, subtitle }) {
555
+ return React.createElement(Box, {
556
+ borderStyle: "round",
557
+ borderColor: "cyan",
558
+ paddingX: 1,
559
+ marginBottom: 1,
560
+ justifyContent: "space-between",
561
+ },
562
+ React.createElement(Text, { bold: true, color: "cyan" }, title),
563
+ React.createElement(Text, { color: "gray" }, subtitle || ""));
564
+ }
565
+
566
+ function Panel({
567
+ title,
568
+ color = "white",
569
+ focused = false,
570
+ width,
571
+ height,
572
+ minHeight,
573
+ flexGrow = 0,
574
+ flexBasis,
575
+ marginRight = 0,
576
+ marginBottom = 0,
577
+ children,
578
+ lines,
579
+ stickyLines,
580
+ scrollOffset = 0,
581
+ scrollMode = "top",
582
+ fillColor,
583
+ paneId,
584
+ paneLabel,
585
+ frame,
586
+ }) {
587
+ const safeWidth = Math.max(8, Number(width) || 40);
588
+ const safeHeight = Math.max(4, Number(height) || 8);
589
+ const borderColor = focused ? "red" : color;
590
+ const contentWidth = Math.max(1, safeWidth - 4);
591
+ const contentHeight = Math.max(1, safeHeight - 2);
592
+ const wrappedStickyLines = wrapNormalizedLines(normalizeLines(stickyLines), contentWidth);
593
+ const visibleStickyLines = wrappedStickyLines.slice(0, contentHeight);
594
+ const scrollableHeight = Math.max(0, contentHeight - visibleStickyLines.length);
595
+ const wrappedLines = wrapNormalizedLines(normalizeLines(lines), contentWidth);
596
+ const maxOffset = Math.max(0, wrappedLines.length - scrollableHeight);
597
+ const clampedOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
598
+ const startIndex = scrollableHeight <= 0
599
+ ? 0
600
+ : scrollMode === "bottom"
601
+ ? Math.max(0, wrappedLines.length - scrollableHeight - clampedOffset)
602
+ : clampedOffset;
603
+ const normalizedLines = scrollableHeight > 0
604
+ ? wrappedLines.slice(startIndex, startIndex + scrollableHeight)
605
+ : [];
606
+ const scrollIndicator = scrollableHeight > 0
607
+ ? buildScrollIndicator(wrappedLines.length, scrollableHeight, startIndex)
608
+ : null;
609
+
610
+ while (normalizedLines.length + visibleStickyLines.length < contentHeight) {
611
+ normalizedLines.push(null);
612
+ }
613
+
614
+ const visibleLines = [...visibleStickyLines, ...normalizedLines];
615
+ if (paneId && lines) {
616
+ registerSelectablePaneSnapshot({
617
+ paneId,
618
+ paneLabel: paneLabel || flattenTitleText(title) || paneId,
619
+ frame: normalizePaneFrame(frame, safeWidth, safeHeight),
620
+ contentWidth,
621
+ contentHeight,
622
+ visibleLines,
623
+ });
624
+ }
625
+
626
+ return React.createElement(Box, {
627
+ flexDirection: "column",
628
+ marginRight,
629
+ marginBottom,
630
+ width: safeWidth,
631
+ height: safeHeight,
632
+ minHeight,
633
+ flexGrow,
634
+ flexBasis,
635
+ },
636
+ renderBorderTop(title, borderColor, safeWidth),
637
+ lines
638
+ ? React.createElement(Box, { flexDirection: "column", flexGrow: 1, backgroundColor: fillColor || undefined },
639
+ [
640
+ ...visibleStickyLines.map((line, index) => renderPanelRow(
641
+ line,
642
+ `sticky:${index}`,
643
+ contentWidth,
644
+ borderColor,
645
+ null,
646
+ fillColor,
647
+ getSelectionRangeForPaneRow(paneId, index),
648
+ )),
649
+ ...normalizedLines.map((line, index) => renderPanelRow(
650
+ line,
651
+ `body:${index}`,
652
+ contentWidth,
653
+ borderColor,
654
+ scrollIndicator,
655
+ fillColor,
656
+ getSelectionRangeForPaneRow(paneId, visibleStickyLines.length + index),
657
+ index,
658
+ )),
659
+ ])
660
+ : React.createElement(Box, {
661
+ flexDirection: "column",
662
+ borderStyle: "round",
663
+ borderColor: borderColor,
664
+ paddingX: 1,
665
+ flexGrow: 1,
666
+ backgroundColor: fillColor || undefined,
667
+ }, children),
668
+ renderBorderBottom(borderColor, safeWidth));
669
+ }
670
+
671
+ function Lines({ lines }) {
672
+ return React.createElement(Box, { flexDirection: "column" }, linesToElements(lines));
673
+ }
674
+
675
+ function clampPromptCursorIndex(value, cursorIndex) {
676
+ return clampValue(Number(cursorIndex) || 0, 0, String(value || "").length);
677
+ }
678
+
679
+ function getPromptCursorPosition(value, cursorIndex) {
680
+ const safeValue = String(value || "");
681
+ const prefix = safeValue.slice(0, clampPromptCursorIndex(safeValue, cursorIndex));
682
+ const lines = prefix.split("\n");
683
+ return {
684
+ line: Math.max(0, lines.length - 1),
685
+ column: (lines[lines.length - 1] || "").length,
686
+ };
687
+ }
688
+
689
+ function getPromptVisibleWindow(lines, rows, cursorLine, focused) {
690
+ const safeLines = Array.isArray(lines) ? lines : [""];
691
+ const safeRows = clampValue(Number(rows) || 1, 1, MAX_PROMPT_INPUT_ROWS);
692
+ const anchorLine = focused
693
+ ? clampValue(Number(cursorLine) || 0, 0, Math.max(0, safeLines.length - 1))
694
+ : Math.max(0, safeLines.length - 1);
695
+ const maxStart = Math.max(0, safeLines.length - safeRows);
696
+ const start = clampValue(anchorLine - safeRows + 1, 0, maxStart);
697
+ return {
698
+ start,
699
+ lines: safeLines.slice(start, start + safeRows),
700
+ };
701
+ }
702
+
703
+ function renderPromptRow(lineText, cursorColumn, { color, showCursor, keyPrefix, prefix = null, dimColor = false }) {
704
+ const safeText = String(lineText || "");
705
+ const safeCursorColumn = Math.max(0, Math.min(Number(cursorColumn) || 0, safeText.length));
706
+ const before = safeText.slice(0, safeCursorColumn);
707
+ const cursorChar = showCursor && safeCursorColumn < safeText.length
708
+ ? safeText[safeCursorColumn]
709
+ : "";
710
+ const after = showCursor && safeCursorColumn < safeText.length
711
+ ? safeText.slice(safeCursorColumn + 1)
712
+ : safeText.slice(safeCursorColumn);
713
+
714
+ return React.createElement(Box, { key: keyPrefix, flexDirection: "row" },
715
+ prefix,
716
+ before ? React.createElement(Text, { color, dimColor }, before) : null,
717
+ showCursor
718
+ ? cursorChar
719
+ ? React.createElement(Text, {
720
+ color: "black",
721
+ backgroundColor: "green",
722
+ dimColor,
723
+ }, cursorChar)
724
+ : React.createElement(Text, { color: "green" }, "█")
725
+ : null,
726
+ after ? React.createElement(Text, { color, dimColor }, after) : null,
727
+ );
728
+ }
729
+
730
+ function Input({ label, value, focused, placeholder, rows = 1, cursorIndex = 0 }) {
731
+ const safeValue = String(value || "");
732
+ const isEmpty = safeValue.length === 0;
733
+ const safeRows = clampValue(Number(rows) || 1, 1, MAX_PROMPT_INPUT_ROWS);
734
+ const labelPrefix = React.createElement(Text, {
735
+ color: focused ? "red" : "green",
736
+ bold: true,
737
+ }, `${label}: `);
738
+ const cursorPosition = getPromptCursorPosition(safeValue, cursorIndex);
739
+ const promptLines = safeValue.split("\n");
740
+ const visibleWindow = getPromptVisibleWindow(promptLines, safeRows, cursorPosition.line, focused);
741
+ const displayLines = visibleWindow.lines;
742
+ const visibleCursorLine = cursorPosition.line - visibleWindow.start;
743
+
744
+ while (displayLines.length < safeRows) {
745
+ displayLines.push("");
746
+ }
747
+
748
+ return React.createElement(Box, {
749
+ borderStyle: "round",
750
+ borderColor: focused ? "red" : "green",
751
+ paddingX: 1,
752
+ marginTop: 0,
753
+ height: safeRows + 2,
754
+ },
755
+ React.createElement(Box, { flexDirection: "column" },
756
+ isEmpty
757
+ ? [
758
+ renderPromptRow(placeholder || "Type a message and press Enter", focused ? 0 : null, {
759
+ color: "gray",
760
+ dimColor: true,
761
+ showCursor: Boolean(focused),
762
+ keyPrefix: "prompt-line:0",
763
+ prefix: labelPrefix,
764
+ }),
765
+ ...Array.from({ length: Math.max(0, safeRows - 1) }, (_, index) => React.createElement(Box, {
766
+ key: `prompt-empty:${index}`,
767
+ flexDirection: "row",
768
+ }, React.createElement(Text, null, ""))),
769
+ ]
770
+ : displayLines.map((line, index) => renderPromptRow(line, focused && visibleCursorLine === index ? cursorPosition.column : null, {
771
+ color: "white",
772
+ showCursor: Boolean(focused && visibleCursorLine === index),
773
+ keyPrefix: `prompt-line:${index}`,
774
+ prefix: index === 0 ? labelPrefix : null,
775
+ })),
776
+ ));
777
+ }
778
+
779
+ function StatusLine({ left, right }) {
780
+ return React.createElement(Box, {
781
+ borderStyle: "round",
782
+ borderColor: "gray",
783
+ paddingX: 1,
784
+ justifyContent: "space-between",
785
+ },
786
+ React.createElement(Text, { color: "white" }, left || ""),
787
+ React.createElement(Text, { color: "gray", dimColor: true }, right || ""));
788
+ }
789
+
790
+ function Overlay({ children }) {
791
+ const viewportWidth = process.stdout.columns || 120;
792
+ const viewportHeight = process.stdout.rows || 40;
793
+
794
+ return React.createElement(Box, {
795
+ position: "absolute",
796
+ top: 1,
797
+ left: 0,
798
+ width: viewportWidth,
799
+ height: viewportHeight,
800
+ alignItems: "center",
801
+ justifyContent: "center",
802
+ flexDirection: "column",
803
+ }, children);
804
+ }
805
+
806
+ export function createTuiPlatform() {
807
+ tuiPlatformRuntime.paneRegistry.clear();
808
+ tuiPlatformRuntime.selection = createEmptySelection();
809
+ tuiPlatformRuntime.renderInvalidator = null;
810
+
811
+ return {
812
+ Root,
813
+ Row,
814
+ Column,
815
+ Header,
816
+ Panel,
817
+ Overlay,
818
+ Lines,
819
+ Input,
820
+ StatusLine,
821
+ setRenderInvalidator(fn) {
822
+ tuiPlatformRuntime.renderInvalidator = typeof fn === "function" ? fn : null;
823
+ },
824
+ clearSelectablePanes() {
825
+ tuiPlatformRuntime.paneRegistry.clear();
826
+ },
827
+ beginPointerSelection(x, y) {
828
+ const hit = findPaneHit(x, y);
829
+ if (!hit) {
830
+ tuiPlatformRuntime.selection = createEmptySelection();
831
+ requestTuiRender();
832
+ return false;
833
+ }
834
+ tuiPlatformRuntime.selection = {
835
+ paneId: hit.pane.paneId,
836
+ anchor: hit.point,
837
+ head: hit.point,
838
+ dragging: true,
839
+ moved: false,
840
+ };
841
+ requestTuiRender();
842
+ return true;
843
+ },
844
+ updatePointerSelection(x, y) {
845
+ const current = tuiPlatformRuntime.selection;
846
+ if (!current?.dragging || !current.paneId) return false;
847
+ const pane = tuiPlatformRuntime.paneRegistry.get(current.paneId);
848
+ if (!pane) return false;
849
+ const head = projectPointIntoPane(pane, x, y);
850
+ if (!head) return false;
851
+ tuiPlatformRuntime.selection = {
852
+ ...current,
853
+ head,
854
+ moved: Boolean(head.row !== current.anchor?.row || head.col !== current.anchor?.col),
855
+ };
856
+ requestTuiRender();
857
+ return true;
858
+ },
859
+ clearPointerSelection() {
860
+ if (!tuiPlatformRuntime.selection?.paneId && !tuiPlatformRuntime.selection?.dragging) return;
861
+ tuiPlatformRuntime.selection = createEmptySelection();
862
+ requestTuiRender();
863
+ },
864
+ finalizePointerSelection({ copy = true } = {}) {
865
+ const current = tuiPlatformRuntime.selection;
866
+ const pane = current?.paneId ? tuiPlatformRuntime.paneRegistry.get(current.paneId) : null;
867
+ tuiPlatformRuntime.selection = createEmptySelection();
868
+ requestTuiRender();
869
+ if (!current?.dragging || !current?.moved || !pane) {
870
+ return { attempted: false, copied: false };
871
+ }
872
+ const text = extractSelectionTextFromPane(pane, current.anchor, current.head);
873
+ if (!text) {
874
+ return { attempted: false, copied: false, paneLabel: pane.paneLabel || pane.paneId || "pane" };
875
+ }
876
+ if (!copy) {
877
+ return { attempted: true, copied: false, text, paneLabel: pane.paneLabel || pane.paneId || "pane" };
878
+ }
879
+ const result = copyTextToClipboard(text);
880
+ return {
881
+ attempted: true,
882
+ copied: result.ok,
883
+ text,
884
+ paneLabel: pane.paneLabel || pane.paneId || "pane",
885
+ error: result.ok ? null : result.error,
886
+ };
887
+ },
888
+ getViewport: () => ({
889
+ width: process.stdout.columns || 120,
890
+ height: process.stdout.rows || 40,
891
+ }),
892
+ };
893
+ }
894
+
895
+ export const __testing = {
896
+ computeWrappedSlices,
897
+ wrapTextLine,
898
+ wrapRunsLine,
899
+ };