pilotswarm 0.0.1 → 0.4.1

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