pulseed 0.4.12 → 0.4.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.
Files changed (94) hide show
  1. package/dist/base/config/global-config.js +2 -2
  2. package/dist/base/config/global-config.js.map +1 -1
  3. package/dist/base/config/tool-metadata.js +1 -1
  4. package/dist/base/config/tool-metadata.js.map +1 -1
  5. package/dist/base/llm/provider-config.d.ts +1 -1
  6. package/dist/base/llm/provider-config.d.ts.map +1 -1
  7. package/dist/base/llm/provider-config.js +21 -14
  8. package/dist/base/llm/provider-config.js.map +1 -1
  9. package/dist/interface/cli/commands/setup/import/apply.d.ts.map +1 -1
  10. package/dist/interface/cli/commands/setup/import/apply.js +3 -0
  11. package/dist/interface/cli/commands/setup/import/apply.js.map +1 -1
  12. package/dist/interface/cli/commands/setup/import/discovery.d.ts.map +1 -1
  13. package/dist/interface/cli/commands/setup/import/discovery.js +35 -0
  14. package/dist/interface/cli/commands/setup/import/discovery.js.map +1 -1
  15. package/dist/interface/cli/commands/setup/import/flow.d.ts.map +1 -1
  16. package/dist/interface/cli/commands/setup/import/flow.js +19 -8
  17. package/dist/interface/cli/commands/setup/import/flow.js.map +1 -1
  18. package/dist/interface/cli/commands/setup/import/provider.d.ts.map +1 -1
  19. package/dist/interface/cli/commands/setup/import/provider.js +10 -3
  20. package/dist/interface/cli/commands/setup/import/provider.js.map +1 -1
  21. package/dist/interface/cli/commands/setup/import/types.d.ts +6 -1
  22. package/dist/interface/cli/commands/setup/import/types.d.ts.map +1 -1
  23. package/dist/interface/cli/commands/setup/steps-runtime.d.ts +1 -1
  24. package/dist/interface/cli/commands/setup/steps-runtime.d.ts.map +1 -1
  25. package/dist/interface/cli/commands/setup/steps-runtime.js +5 -1
  26. package/dist/interface/cli/commands/setup/steps-runtime.js.map +1 -1
  27. package/dist/interface/cli/commands/setup-wizard.d.ts.map +1 -1
  28. package/dist/interface/cli/commands/setup-wizard.js +22 -4
  29. package/dist/interface/cli/commands/setup-wizard.js.map +1 -1
  30. package/dist/interface/tui/app.d.ts +2 -0
  31. package/dist/interface/tui/app.d.ts.map +1 -1
  32. package/dist/interface/tui/app.js +19 -15
  33. package/dist/interface/tui/app.js.map +1 -1
  34. package/dist/interface/tui/chat/scroll.d.ts +12 -0
  35. package/dist/interface/tui/chat/scroll.d.ts.map +1 -1
  36. package/dist/interface/tui/chat/scroll.js +51 -15
  37. package/dist/interface/tui/chat/scroll.js.map +1 -1
  38. package/dist/interface/tui/chat/suggestions.d.ts.map +1 -1
  39. package/dist/interface/tui/chat/suggestions.js +0 -6
  40. package/dist/interface/tui/chat/suggestions.js.map +1 -1
  41. package/dist/interface/tui/chat/viewport.d.ts +1 -1
  42. package/dist/interface/tui/chat/viewport.d.ts.map +1 -1
  43. package/dist/interface/tui/chat/viewport.js +2 -3
  44. package/dist/interface/tui/chat/viewport.js.map +1 -1
  45. package/dist/interface/tui/chat.d.ts +14 -2
  46. package/dist/interface/tui/chat.d.ts.map +1 -1
  47. package/dist/interface/tui/chat.js +176 -36
  48. package/dist/interface/tui/chat.js.map +1 -1
  49. package/dist/interface/tui/cursor-tracker.d.ts +18 -3
  50. package/dist/interface/tui/cursor-tracker.d.ts.map +1 -1
  51. package/dist/interface/tui/cursor-tracker.js +109 -20
  52. package/dist/interface/tui/cursor-tracker.js.map +1 -1
  53. package/dist/interface/tui/debug-log.d.ts +4 -0
  54. package/dist/interface/tui/debug-log.d.ts.map +1 -0
  55. package/dist/interface/tui/debug-log.js +39 -0
  56. package/dist/interface/tui/debug-log.js.map +1 -0
  57. package/dist/interface/tui/entry.d.ts +4 -0
  58. package/dist/interface/tui/entry.d.ts.map +1 -1
  59. package/dist/interface/tui/entry.js +72 -25
  60. package/dist/interface/tui/entry.js.map +1 -1
  61. package/dist/interface/tui/flicker/MouseTracking.d.ts +10 -0
  62. package/dist/interface/tui/flicker/MouseTracking.d.ts.map +1 -0
  63. package/dist/interface/tui/flicker/MouseTracking.js +22 -0
  64. package/dist/interface/tui/flicker/MouseTracking.js.map +1 -0
  65. package/dist/interface/tui/flicker/dec.d.ts +12 -0
  66. package/dist/interface/tui/flicker/dec.d.ts.map +1 -1
  67. package/dist/interface/tui/flicker/dec.js +14 -0
  68. package/dist/interface/tui/flicker/dec.js.map +1 -1
  69. package/dist/interface/tui/flicker/frame-writer.d.ts.map +1 -1
  70. package/dist/interface/tui/flicker/frame-writer.js +78 -6
  71. package/dist/interface/tui/flicker/frame-writer.js.map +1 -1
  72. package/dist/interface/tui/flicker/index.d.ts +2 -1
  73. package/dist/interface/tui/flicker/index.d.ts.map +1 -1
  74. package/dist/interface/tui/flicker/index.js +2 -1
  75. package/dist/interface/tui/flicker/index.js.map +1 -1
  76. package/dist/interface/tui/fullscreen-chat.d.ts +13 -0
  77. package/dist/interface/tui/fullscreen-chat.d.ts.map +1 -0
  78. package/dist/interface/tui/fullscreen-chat.js +846 -0
  79. package/dist/interface/tui/fullscreen-chat.js.map +1 -0
  80. package/dist/interface/tui/help-overlay.d.ts.map +1 -1
  81. package/dist/interface/tui/help-overlay.js +1 -1
  82. package/dist/interface/tui/help-overlay.js.map +1 -1
  83. package/dist/interface/tui/settings-overlay.d.ts.map +1 -1
  84. package/dist/interface/tui/settings-overlay.js +0 -6
  85. package/dist/interface/tui/settings-overlay.js.map +1 -1
  86. package/dist/interface/tui/test-app.d.ts +8 -0
  87. package/dist/interface/tui/test-app.d.ts.map +1 -0
  88. package/dist/interface/tui/test-app.js +84 -0
  89. package/dist/interface/tui/test-app.js.map +1 -0
  90. package/dist/interface/tui/test-entry.d.ts +3 -0
  91. package/dist/interface/tui/test-entry.d.ts.map +1 -0
  92. package/dist/interface/tui/test-entry.js +75 -0
  93. package/dist/interface/tui/test-entry.js.map +1 -0
  94. package/package.json +4 -3
@@ -0,0 +1,846 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useCallback, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { getClipboardContent } from "./clipboard.js";
5
+ import { logTuiDebug } from "./debug-log.js";
6
+ import { theme } from "./theme.js";
7
+ import { pickSpinnerVerb } from "./spinner-verbs.js";
8
+ import { CARET_MARKER, PROTECTED_ROW_MARKER, setActiveCursorEscape, setActivePromptCursorAnchor, } from "./cursor-tracker.js";
9
+ import { isBashModeInput } from "./bash-mode.js";
10
+ import { buildChatViewport } from "./chat/viewport.js";
11
+ import { getScrollRequest, parseMouseEvent, stripMouseEscapeSequences, } from "./chat/scroll.js";
12
+ import { getMatchingSuggestions } from "./chat/suggestions.js";
13
+ const SCROLL_LINE_STEP = 3;
14
+ const DEFAULT_PROMPT = "◉";
15
+ const BASH_PROMPT = "!";
16
+ const SUGGESTION_HINT = " arrows to navigate, tab/enter to select, esc to dismiss";
17
+ const INPUT_MARGIN = 4;
18
+ const ZERO_WIDTH_CHARS = new Set(["\u200B", "\u2060", "\u2061"]);
19
+ const SELECTION_BACKGROUND = theme.text;
20
+ const SELECTION_FOREGROUND = "#1F2329";
21
+ const FAKE_CURSOR_GLYPH = "█";
22
+ function charWidth(ch) {
23
+ if (ZERO_WIDTH_CHARS.has(ch))
24
+ return 0;
25
+ const cp = ch.codePointAt(0) ?? 0;
26
+ return cp > 0x2e7f ? 2 : 1;
27
+ }
28
+ function stringWidth(text) {
29
+ let width = 0;
30
+ for (const ch of text) {
31
+ width += charWidth(ch);
32
+ }
33
+ return width;
34
+ }
35
+ function trimToWidth(text, width) {
36
+ if (width <= 0)
37
+ return "";
38
+ let out = "";
39
+ let used = 0;
40
+ for (const ch of text) {
41
+ const next = charWidth(ch);
42
+ if (used + next > width)
43
+ break;
44
+ out += ch;
45
+ used += next;
46
+ }
47
+ return out;
48
+ }
49
+ function padToWidth(text, width) {
50
+ const trimmed = trimToWidth(text, width);
51
+ const padding = Math.max(0, width - stringWidth(trimmed));
52
+ return trimmed + " ".repeat(padding);
53
+ }
54
+ function getPreviousOffset(text, offset) {
55
+ if (offset <= 0)
56
+ return 0;
57
+ const previous = offset - 1;
58
+ const previousCode = text.charCodeAt(previous);
59
+ if (previous > 0 &&
60
+ previousCode >= 0xdc00 &&
61
+ previousCode <= 0xdfff) {
62
+ const lead = text.charCodeAt(previous - 1);
63
+ if (lead >= 0xd800 && lead <= 0xdbff) {
64
+ return previous - 1;
65
+ }
66
+ }
67
+ return previous;
68
+ }
69
+ function getNextOffset(text, offset) {
70
+ if (offset >= text.length)
71
+ return text.length;
72
+ const code = text.charCodeAt(offset);
73
+ if (offset + 1 < text.length &&
74
+ code >= 0xd800 &&
75
+ code <= 0xdbff) {
76
+ const trail = text.charCodeAt(offset + 1);
77
+ if (trail >= 0xdc00 && trail <= 0xdfff) {
78
+ return offset + 2;
79
+ }
80
+ }
81
+ return offset + 1;
82
+ }
83
+ function isBackspaceInput(inputChar, key) {
84
+ return (key.backspace === true ||
85
+ inputChar === "\u007f" ||
86
+ inputChar === "\b" ||
87
+ (key.ctrl === true && inputChar === "h") ||
88
+ (key.delete === true && inputChar === ""));
89
+ }
90
+ function isDeleteInput(inputChar, key) {
91
+ return inputChar === "[3~" || inputChar === "\u001b[3~";
92
+ }
93
+ function summarizeKey(key) {
94
+ return Object.fromEntries(Object.entries(key).filter(([, value]) => value === true));
95
+ }
96
+ function getPromptLabel(bashMode) {
97
+ return bashMode ? BASH_PROMPT : DEFAULT_PROMPT;
98
+ }
99
+ function getPlaceholder(bashMode) {
100
+ return bashMode ? "! for bash mode" : "/ for commands";
101
+ }
102
+ function formatSuggestionLabel(suggestion) {
103
+ return suggestion.type === "goal"
104
+ ? ` ${suggestion.name} ${suggestion.description.padEnd(20)} [goal]`
105
+ : ` ${suggestion.name.padEnd(20)}${suggestion.description}`;
106
+ }
107
+ function normalizeSelection(selection) {
108
+ if (!selection || selection.anchor === selection.focus) {
109
+ return null;
110
+ }
111
+ return {
112
+ start: Math.min(selection.anchor, selection.focus),
113
+ end: Math.max(selection.anchor, selection.focus),
114
+ };
115
+ }
116
+ function pushSegment(segments, text, style = {}) {
117
+ if (text.length === 0)
118
+ return;
119
+ const previous = segments[segments.length - 1];
120
+ if (previous &&
121
+ previous.color === style.color &&
122
+ previous.backgroundColor === style.backgroundColor &&
123
+ previous.bold === style.bold &&
124
+ previous.dim === style.dim) {
125
+ previous.text += text;
126
+ return;
127
+ }
128
+ segments.push({ text, ...style });
129
+ }
130
+ function buildInputRows(input, cursorOffset, contentWidth, placeholder, selection) {
131
+ if (contentWidth <= 0) {
132
+ return {
133
+ rows: [{
134
+ cells: [{
135
+ text: CARET_MARKER,
136
+ width: 0,
137
+ offsetBefore: cursorOffset,
138
+ offsetAfter: cursorOffset,
139
+ }],
140
+ startOffset: cursorOffset,
141
+ endOffset: cursorOffset,
142
+ }],
143
+ };
144
+ }
145
+ if (input.length === 0) {
146
+ const cells = [{
147
+ text: CARET_MARKER,
148
+ width: 0,
149
+ offsetBefore: 0,
150
+ offsetAfter: 0,
151
+ }];
152
+ for (const ch of trimToWidth(placeholder, Math.max(0, contentWidth - 1))) {
153
+ cells.push({
154
+ text: ch,
155
+ width: charWidth(ch),
156
+ offsetBefore: 0,
157
+ offsetAfter: 0,
158
+ placeholder: true,
159
+ });
160
+ }
161
+ return {
162
+ rows: [{
163
+ cells,
164
+ startOffset: 0,
165
+ endOffset: 0,
166
+ }],
167
+ };
168
+ }
169
+ const rows = [];
170
+ let currentCells = [];
171
+ let currentWidth = 0;
172
+ let rowStartOffset = 0;
173
+ let rowEndOffset = 0;
174
+ const pushRow = () => {
175
+ rows.push({
176
+ cells: currentCells,
177
+ startOffset: rowStartOffset,
178
+ endOffset: rowEndOffset,
179
+ });
180
+ currentCells = [];
181
+ currentWidth = 0;
182
+ };
183
+ let offset = 0;
184
+ while (offset <= input.length) {
185
+ if (offset === cursorOffset) {
186
+ if (currentWidth >= contentWidth && currentCells.length > 0) {
187
+ pushRow();
188
+ rowStartOffset = offset;
189
+ rowEndOffset = offset;
190
+ }
191
+ currentCells.push({
192
+ text: CARET_MARKER,
193
+ width: 0,
194
+ offsetBefore: offset,
195
+ offsetAfter: offset,
196
+ });
197
+ }
198
+ if (offset === input.length) {
199
+ break;
200
+ }
201
+ const codePoint = input.codePointAt(offset) ?? 0;
202
+ const ch = String.fromCodePoint(codePoint);
203
+ const nextOffset = offset + ch.length;
204
+ if (ch === "\n") {
205
+ pushRow();
206
+ rowStartOffset = nextOffset;
207
+ rowEndOffset = nextOffset;
208
+ offset = nextOffset;
209
+ continue;
210
+ }
211
+ const width = charWidth(ch);
212
+ if (currentWidth + width > contentWidth && currentCells.length > 0) {
213
+ pushRow();
214
+ rowStartOffset = offset;
215
+ rowEndOffset = offset;
216
+ }
217
+ currentCells.push({
218
+ text: ch,
219
+ width,
220
+ offsetBefore: offset,
221
+ offsetAfter: nextOffset,
222
+ selected: selection !== null &&
223
+ offset < selection.end &&
224
+ nextOffset > selection.start,
225
+ });
226
+ currentWidth += width;
227
+ rowEndOffset = nextOffset;
228
+ offset = nextOffset;
229
+ }
230
+ rows.push({
231
+ cells: currentCells,
232
+ startOffset: rowStartOffset,
233
+ endOffset: rowEndOffset,
234
+ });
235
+ return { rows };
236
+ }
237
+ function buildInputContentSegments(row, contentWidth, bashMode) {
238
+ const segments = [];
239
+ const defaultColor = bashMode ? theme.command : undefined;
240
+ let usedWidth = 0;
241
+ for (const cell of row.cells) {
242
+ if (cell.text === CARET_MARKER) {
243
+ pushSegment(segments, FAKE_CURSOR_GLYPH, {
244
+ color: theme.text,
245
+ bold: true,
246
+ });
247
+ usedWidth += 1;
248
+ continue;
249
+ }
250
+ usedWidth += cell.width;
251
+ if (cell.selected) {
252
+ pushSegment(segments, cell.text, {
253
+ color: SELECTION_FOREGROUND,
254
+ backgroundColor: SELECTION_BACKGROUND,
255
+ });
256
+ continue;
257
+ }
258
+ pushSegment(segments, cell.text, {
259
+ color: defaultColor,
260
+ dim: cell.placeholder,
261
+ });
262
+ }
263
+ if (usedWidth < contentWidth) {
264
+ pushSegment(segments, " ".repeat(contentWidth - usedWidth), {
265
+ color: defaultColor,
266
+ });
267
+ }
268
+ return segments;
269
+ }
270
+ function buildCursorEscapeFromComposerLayout(layout) {
271
+ for (let rowIndex = 0; rowIndex < layout.rows.length; rowIndex += 1) {
272
+ const row = layout.rows[rowIndex];
273
+ if (!row)
274
+ continue;
275
+ let colOffset = 0;
276
+ for (const cell of row.cells) {
277
+ if (cell.text === CARET_MARKER) {
278
+ return `\x1b[${layout.startLine + rowIndex};${layout.contentStartCol + colOffset}H\x1b[?25l`;
279
+ }
280
+ colOffset += cell.width;
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ function getCursorAnchorFromComposerLayout(layout) {
286
+ for (let rowIndex = 0; rowIndex < layout.rows.length; rowIndex += 1) {
287
+ const row = layout.rows[rowIndex];
288
+ if (!row)
289
+ continue;
290
+ let colOffset = 0;
291
+ for (const cell of row.cells) {
292
+ if (cell.text === CARET_MARKER) {
293
+ return {
294
+ caretRowOffset: rowIndex,
295
+ caretColumnOffset: colOffset,
296
+ };
297
+ }
298
+ colOffset += cell.width;
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+ function buildComposerLines(args) {
304
+ const { cols, input, cursorOffset, bashMode, emptyHint, matches, selectedIdx, copyToast, selection, } = args;
305
+ const lines = [];
306
+ lines.push({
307
+ key: "copy-toast",
308
+ text: padToWidth(copyToast ?? "", cols),
309
+ color: copyToast ? "cyan" : undefined,
310
+ });
311
+ const innerWidth = Math.max(1, cols - 2);
312
+ const promptLabel = getPromptLabel(bashMode);
313
+ const prompt = `${promptLabel} `;
314
+ const promptWidth = stringWidth(prompt);
315
+ const contentWidth = Math.max(1, innerWidth - INPUT_MARGIN - promptWidth);
316
+ const inputRender = buildInputRows(input, cursorOffset, contentWidth, getPlaceholder(bashMode), selection);
317
+ const inputRows = inputRender.rows;
318
+ lines.push({
319
+ key: "composer-top",
320
+ text: padToWidth(`┌${"─".repeat(Math.max(0, cols - 2))}┐`, cols),
321
+ color: bashMode ? theme.command : theme.border,
322
+ });
323
+ inputRows.forEach((row, index) => {
324
+ const segments = [];
325
+ const borderColor = bashMode ? theme.command : undefined;
326
+ const promptColor = bashMode ? theme.command : theme.userPrompt;
327
+ pushSegment(segments, "│ ", { color: borderColor });
328
+ if (index === 0) {
329
+ pushSegment(segments, promptLabel, { color: promptColor, bold: true });
330
+ pushSegment(segments, " ", { color: promptColor, bold: true });
331
+ }
332
+ else {
333
+ pushSegment(segments, " ".repeat(promptWidth), { color: borderColor });
334
+ }
335
+ segments.push(...buildInputContentSegments(row, contentWidth, bashMode));
336
+ pushSegment(segments, " │", { color: borderColor });
337
+ lines.push({
338
+ key: `composer-row-${index}`,
339
+ segments,
340
+ protected: true,
341
+ });
342
+ });
343
+ lines.push({
344
+ key: "composer-bottom",
345
+ text: padToWidth(`└${"─".repeat(Math.max(0, cols - 2))}┘`, cols),
346
+ color: bashMode ? theme.command : theme.border,
347
+ });
348
+ if (bashMode) {
349
+ lines.push({
350
+ key: "bash-hint",
351
+ text: padToWidth("! for bash mode", cols),
352
+ color: theme.command,
353
+ });
354
+ }
355
+ if (emptyHint) {
356
+ lines.push({
357
+ key: "empty-hint",
358
+ text: padToWidth(" Type a message or /help for commands", cols),
359
+ dim: true,
360
+ });
361
+ }
362
+ if (matches.length > 0) {
363
+ matches.forEach((suggestion, index) => {
364
+ lines.push({
365
+ key: `suggestion-${index}`,
366
+ text: padToWidth(formatSuggestionLabel(suggestion), cols),
367
+ color: index === selectedIdx ? theme.selected : undefined,
368
+ bold: index === selectedIdx,
369
+ dim: index !== selectedIdx,
370
+ });
371
+ });
372
+ lines.push({
373
+ key: "suggestion-hint",
374
+ text: padToWidth(SUGGESTION_HINT, cols),
375
+ dim: true,
376
+ });
377
+ }
378
+ return {
379
+ lines,
380
+ inputRows,
381
+ inputRowStartIndex: 2,
382
+ contentStartCol: 3 + promptWidth,
383
+ };
384
+ }
385
+ function renderMessageRow(row, cols) {
386
+ if (row.kind === "spacer") {
387
+ return { key: row.key, text: " ".repeat(cols) };
388
+ }
389
+ return {
390
+ key: row.key,
391
+ text: padToWidth(row.text, cols),
392
+ color: row.color,
393
+ backgroundColor: row.backgroundColor,
394
+ bold: row.bold,
395
+ dim: row.dim,
396
+ };
397
+ }
398
+ function getMouseOffsetFromComposer(layout, x, y, clampOutside) {
399
+ if (layout.rows.length === 0) {
400
+ return null;
401
+ }
402
+ let rowIndex = y - layout.startLine;
403
+ if (rowIndex < 0) {
404
+ if (!clampOutside)
405
+ return null;
406
+ rowIndex = 0;
407
+ }
408
+ if (rowIndex >= layout.rows.length) {
409
+ if (!clampOutside)
410
+ return null;
411
+ rowIndex = layout.rows.length - 1;
412
+ }
413
+ const row = layout.rows[rowIndex];
414
+ if (!row) {
415
+ return null;
416
+ }
417
+ if (row.startOffset === row.endOffset) {
418
+ return row.startOffset;
419
+ }
420
+ const localCol = x - layout.contentStartCol;
421
+ if (localCol <= 0) {
422
+ return row.startOffset;
423
+ }
424
+ let usedWidth = 0;
425
+ for (const cell of row.cells) {
426
+ if (cell.placeholder || cell.width <= 0) {
427
+ continue;
428
+ }
429
+ const midpoint = usedWidth + cell.width / 2;
430
+ if (localCol <= midpoint) {
431
+ return cell.offsetBefore;
432
+ }
433
+ usedWidth += cell.width;
434
+ if (localCol <= usedWidth) {
435
+ return cell.offsetAfter;
436
+ }
437
+ }
438
+ return row.endOffset;
439
+ }
440
+ export function FullscreenChat({ messages, onSubmit, onClear, isProcessing, goalNames = [], availableRows, availableCols, }) {
441
+ const [input, setInput] = useState("");
442
+ const [cursorOffset, setCursorOffset] = useState(0);
443
+ const [selection, setSelection] = useState(null);
444
+ const selectionAnchor = React.useRef(null);
445
+ const [selectedIdx, setSelectedIdx] = useState(0);
446
+ const justSelected = React.useRef(false);
447
+ const [history, setHistory] = React.useState([]);
448
+ const [historyIdx, setHistoryIdx] = React.useState(-1);
449
+ const [draft, setDraft] = React.useState("");
450
+ const [emptyHint, setEmptyHint] = React.useState(false);
451
+ const [copyToast, setCopyToast] = useState(null);
452
+ const emptyHintTimer = React.useRef(null);
453
+ const [scrollOffset, setScrollOffset] = React.useState(0);
454
+ const [spinnerVerb, setSpinnerVerb] = React.useState(() => pickSpinnerVerb());
455
+ React.useEffect(() => {
456
+ let lastClipboard = "";
457
+ let mounted = true;
458
+ getClipboardContent().then((content) => {
459
+ if (mounted)
460
+ lastClipboard = content;
461
+ });
462
+ const interval = setInterval(async () => {
463
+ if (!mounted)
464
+ return;
465
+ const current = await getClipboardContent();
466
+ if (current !== lastClipboard && current.length > 0) {
467
+ lastClipboard = current;
468
+ setCopyToast(`copied ${current.length} chars to clipboard`);
469
+ setTimeout(() => {
470
+ if (mounted)
471
+ setCopyToast(null);
472
+ }, 2000);
473
+ }
474
+ }, 500);
475
+ return () => {
476
+ mounted = false;
477
+ clearInterval(interval);
478
+ };
479
+ }, []);
480
+ React.useEffect(() => {
481
+ if (!isProcessing)
482
+ return;
483
+ const interval = setInterval(() => {
484
+ setSpinnerVerb(pickSpinnerVerb());
485
+ }, 5000);
486
+ return () => clearInterval(interval);
487
+ }, [isProcessing]);
488
+ const clearSelection = useCallback(() => {
489
+ selectionAnchor.current = null;
490
+ setSelection(null);
491
+ }, []);
492
+ const replaceInputRange = useCallback((start, end, replacement) => {
493
+ const next = input.slice(0, start) + replacement + input.slice(end);
494
+ setInput(next);
495
+ setCursorOffset(start + replacement.length);
496
+ clearSelection();
497
+ }, [clearSelection, input]);
498
+ const insertText = useCallback((text) => {
499
+ justSelected.current = false;
500
+ const selectedRange = normalizeSelection(selection);
501
+ if (selectedRange) {
502
+ replaceInputRange(selectedRange.start, selectedRange.end, text);
503
+ return;
504
+ }
505
+ const next = input.slice(0, cursorOffset) + text + input.slice(cursorOffset);
506
+ setInput(next);
507
+ setCursorOffset(cursorOffset + text.length);
508
+ clearSelection();
509
+ }, [clearSelection, cursorOffset, input, replaceInputRange, selection]);
510
+ const deleteSelection = useCallback(() => {
511
+ const selectedRange = normalizeSelection(selection);
512
+ if (!selectedRange) {
513
+ return false;
514
+ }
515
+ replaceInputRange(selectedRange.start, selectedRange.end, "");
516
+ return true;
517
+ }, [replaceInputRange, selection]);
518
+ const matches = justSelected.current ? [] : getMatchingSuggestions(input, goalNames);
519
+ const hasMatches = matches.length > 0;
520
+ const bashMode = isBashModeInput(input);
521
+ const normalizedSelection = normalizeSelection(selection);
522
+ const composer = buildComposerLines({
523
+ cols: availableCols,
524
+ input,
525
+ cursorOffset,
526
+ bashMode,
527
+ emptyHint,
528
+ matches,
529
+ selectedIdx,
530
+ copyToast,
531
+ selection: normalizedSelection,
532
+ });
533
+ const messageRows = Math.max(1, availableRows - composer.lines.length - 3);
534
+ const viewport = buildChatViewport(messages, availableCols, messageRows, scrollOffset);
535
+ const composerLayout = {
536
+ startLine: viewport.maxVisibleRows + 3 + composer.inputRowStartIndex + 1,
537
+ contentStartCol: composer.contentStartCol,
538
+ rows: composer.inputRows,
539
+ };
540
+ setActiveCursorEscape(buildCursorEscapeFromComposerLayout(composerLayout));
541
+ const promptCursorAnchor = getCursorAnchorFromComposerLayout(composerLayout);
542
+ setActivePromptCursorAnchor(promptCursorAnchor
543
+ ? {
544
+ promptLabel: getPromptLabel(bashMode),
545
+ ...promptCursorAnchor,
546
+ }
547
+ : null);
548
+ React.useEffect(() => {
549
+ return () => {
550
+ setActiveCursorEscape(null);
551
+ setActivePromptCursorAnchor(null);
552
+ };
553
+ }, []);
554
+ const applyScroll = useCallback((direction, kind) => {
555
+ setScrollOffset((prev) => {
556
+ const maxOffset = Math.max(0, viewport.totalRows - 1);
557
+ const amount = kind === "page" ? viewport.maxVisibleRows : SCROLL_LINE_STEP;
558
+ const delta = direction === "up" ? amount : -amount;
559
+ return Math.max(0, Math.min(maxOffset, prev + delta));
560
+ });
561
+ }, [viewport.maxVisibleRows, viewport.totalRows]);
562
+ const handleSubmit = useCallback((value) => {
563
+ logTuiDebug("fullscreen-chat", "submit-attempt", {
564
+ value,
565
+ hasMatches,
566
+ isProcessing,
567
+ });
568
+ if (hasMatches || isProcessing)
569
+ return;
570
+ if (!value.trim()) {
571
+ setEmptyHint(true);
572
+ if (emptyHintTimer.current)
573
+ clearTimeout(emptyHintTimer.current);
574
+ emptyHintTimer.current = setTimeout(() => setEmptyHint(false), 1500);
575
+ return;
576
+ }
577
+ const trimmed = value.trim();
578
+ if (trimmed === "/clear") {
579
+ onClear?.();
580
+ setInput("");
581
+ setCursorOffset(0);
582
+ clearSelection();
583
+ setHistory((prev) => [...prev, trimmed]);
584
+ setHistoryIdx(-1);
585
+ setScrollOffset(0);
586
+ return;
587
+ }
588
+ onSubmit(trimmed);
589
+ setInput("");
590
+ setCursorOffset(0);
591
+ clearSelection();
592
+ setHistory((prev) => [...prev, trimmed]);
593
+ setHistoryIdx(-1);
594
+ setScrollOffset(0);
595
+ }, [clearSelection, hasMatches, isProcessing, onClear, onSubmit]);
596
+ useInput((inputChar, key) => {
597
+ logTuiDebug("fullscreen-chat", "input-event", {
598
+ inputChar,
599
+ key: summarizeKey(key),
600
+ input,
601
+ cursorOffset,
602
+ selection: normalizedSelection,
603
+ historyIdx,
604
+ });
605
+ const scrollRequest = getScrollRequest(inputChar, key);
606
+ if (scrollRequest) {
607
+ logTuiDebug("fullscreen-chat", "scroll-request", {
608
+ direction: scrollRequest.direction,
609
+ kind: scrollRequest.kind,
610
+ });
611
+ applyScroll(scrollRequest.direction, scrollRequest.kind);
612
+ return;
613
+ }
614
+ const mouseEvent = parseMouseEvent(inputChar);
615
+ if (mouseEvent && mouseEvent.kind !== "wheel" && mouseEvent.button === "left") {
616
+ const offset = getMouseOffsetFromComposer(composerLayout, mouseEvent.x, mouseEvent.y, mouseEvent.kind !== "press" && selectionAnchor.current !== null);
617
+ if (mouseEvent.kind === "release" && offset === null) {
618
+ selectionAnchor.current = null;
619
+ return;
620
+ }
621
+ if (offset !== null) {
622
+ justSelected.current = false;
623
+ setCursorOffset(offset);
624
+ if (mouseEvent.kind === "press") {
625
+ selectionAnchor.current = offset;
626
+ setSelection({ anchor: offset, focus: offset });
627
+ }
628
+ else if (mouseEvent.kind === "drag" && selectionAnchor.current !== null) {
629
+ setSelection({ anchor: selectionAnchor.current, focus: offset });
630
+ }
631
+ else if (mouseEvent.kind === "release" && selectionAnchor.current !== null) {
632
+ const nextSelection = { anchor: selectionAnchor.current, focus: offset };
633
+ selectionAnchor.current = null;
634
+ setSelection(nextSelection.anchor === nextSelection.focus ? null : nextSelection);
635
+ }
636
+ return;
637
+ }
638
+ }
639
+ if (key.return && key.shift) {
640
+ logTuiDebug("fullscreen-chat", "insert-newline", { cursorOffset });
641
+ insertText("\n");
642
+ return;
643
+ }
644
+ if (hasMatches) {
645
+ if (key.upArrow) {
646
+ setSelectedIdx((prev) => (prev <= 0 ? matches.length - 1 : prev - 1));
647
+ return;
648
+ }
649
+ if (key.downArrow) {
650
+ setSelectedIdx((prev) => (prev >= matches.length - 1 ? 0 : prev + 1));
651
+ return;
652
+ }
653
+ if (key.tab || key.return) {
654
+ const selected = matches[selectedIdx];
655
+ if (selected) {
656
+ const value = selected.type === "goal"
657
+ ? `${selected.name} ${selected.description}`
658
+ : selected.name;
659
+ setInput(value);
660
+ setCursorOffset(value.length);
661
+ clearSelection();
662
+ setSelectedIdx(0);
663
+ justSelected.current = true;
664
+ }
665
+ return;
666
+ }
667
+ if (key.escape) {
668
+ setSelectedIdx(0);
669
+ setInput("");
670
+ setCursorOffset(0);
671
+ clearSelection();
672
+ return;
673
+ }
674
+ }
675
+ if (key.return) {
676
+ handleSubmit(input);
677
+ return;
678
+ }
679
+ if (key.leftArrow) {
680
+ if (normalizedSelection) {
681
+ setCursorOffset(normalizedSelection.start);
682
+ clearSelection();
683
+ return;
684
+ }
685
+ setCursorOffset((prev) => Math.max(0, prev - 1));
686
+ return;
687
+ }
688
+ if (key.rightArrow) {
689
+ if (normalizedSelection) {
690
+ setCursorOffset(normalizedSelection.end);
691
+ clearSelection();
692
+ return;
693
+ }
694
+ setCursorOffset((prev) => Math.min(input.length, prev + 1));
695
+ return;
696
+ }
697
+ if ((key.ctrl && inputChar === "a") || key.home) {
698
+ setCursorOffset(0);
699
+ clearSelection();
700
+ return;
701
+ }
702
+ if ((key.ctrl && inputChar === "e") || key.end) {
703
+ setCursorOffset(input.length);
704
+ clearSelection();
705
+ return;
706
+ }
707
+ if (isBackspaceInput(inputChar, key)) {
708
+ logTuiDebug("fullscreen-chat", "backspace-detected", {
709
+ inputChar,
710
+ key: summarizeKey(key),
711
+ cursorOffset,
712
+ input,
713
+ selection: normalizedSelection,
714
+ });
715
+ if (deleteSelection()) {
716
+ logTuiDebug("fullscreen-chat", "backspace-delete-selection", {});
717
+ return;
718
+ }
719
+ if (cursorOffset > 0) {
720
+ const previousOffset = getPreviousOffset(input, cursorOffset);
721
+ const next = input.slice(0, previousOffset) + input.slice(cursorOffset);
722
+ setInput(next);
723
+ setCursorOffset(previousOffset);
724
+ logTuiDebug("fullscreen-chat", "backspace-applied", {
725
+ previousOffset,
726
+ next,
727
+ });
728
+ }
729
+ else {
730
+ logTuiDebug("fullscreen-chat", "backspace-at-start", {});
731
+ }
732
+ return;
733
+ }
734
+ if (isDeleteInput(inputChar, key)) {
735
+ logTuiDebug("fullscreen-chat", "delete-detected", {
736
+ inputChar,
737
+ key: summarizeKey(key),
738
+ cursorOffset,
739
+ input,
740
+ selection: normalizedSelection,
741
+ });
742
+ if (deleteSelection()) {
743
+ logTuiDebug("fullscreen-chat", "delete-selection", {});
744
+ return;
745
+ }
746
+ if (cursorOffset < input.length) {
747
+ const nextOffset = getNextOffset(input, cursorOffset);
748
+ const next = input.slice(0, cursorOffset) + input.slice(nextOffset);
749
+ setInput(next);
750
+ logTuiDebug("fullscreen-chat", "delete-applied", {
751
+ nextOffset,
752
+ next,
753
+ });
754
+ }
755
+ else {
756
+ logTuiDebug("fullscreen-chat", "delete-at-end", {});
757
+ }
758
+ return;
759
+ }
760
+ if (key.upArrow) {
761
+ if (history.length > 0) {
762
+ clearSelection();
763
+ if (historyIdx === -1) {
764
+ setDraft(input);
765
+ const idx = history.length - 1;
766
+ setHistoryIdx(idx);
767
+ setInput(history[idx]);
768
+ setCursorOffset(history[idx].length);
769
+ }
770
+ else if (historyIdx > 0) {
771
+ const idx = historyIdx - 1;
772
+ setHistoryIdx(idx);
773
+ setInput(history[idx]);
774
+ setCursorOffset(history[idx].length);
775
+ }
776
+ }
777
+ return;
778
+ }
779
+ if (key.downArrow && historyIdx !== -1) {
780
+ clearSelection();
781
+ if (historyIdx < history.length - 1) {
782
+ const idx = historyIdx + 1;
783
+ setHistoryIdx(idx);
784
+ setInput(history[idx]);
785
+ setCursorOffset(history[idx].length);
786
+ }
787
+ else {
788
+ setHistoryIdx(-1);
789
+ setInput(draft);
790
+ setCursorOffset(draft.length);
791
+ }
792
+ return;
793
+ }
794
+ if (inputChar && !key.ctrl && !key.meta) {
795
+ const clean = stripMouseEscapeSequences(inputChar);
796
+ if (clean.length === 0)
797
+ return;
798
+ insertText(clean);
799
+ }
800
+ }, { isActive: !isProcessing });
801
+ React.useEffect(() => {
802
+ setSelectedIdx(0);
803
+ }, [matches.map((match) => match.name).join(",")]);
804
+ React.useEffect(() => {
805
+ return () => {
806
+ if (emptyHintTimer.current)
807
+ clearTimeout(emptyHintTimer.current);
808
+ };
809
+ }, []);
810
+ const lines = [];
811
+ lines.push({
812
+ key: "indicator-top",
813
+ text: padToWidth(viewport.hiddenAboveRows > 0 ? `↑ ${viewport.hiddenAboveRows} earlier lines` : "", availableCols),
814
+ dim: true,
815
+ });
816
+ const renderedRows = viewport.rows.map((row) => renderMessageRow(row, availableCols));
817
+ const fillerCount = Math.max(0, viewport.maxVisibleRows - renderedRows.length);
818
+ for (let index = 0; index < fillerCount; index += 1) {
819
+ lines.push({ key: `filler-${index}`, text: " ".repeat(availableCols) });
820
+ }
821
+ lines.push(...renderedRows);
822
+ lines.push({
823
+ key: "processing",
824
+ text: padToWidth(isProcessing ? `⠋ ${spinnerVerb}...` : "", availableCols),
825
+ dim: !isProcessing,
826
+ });
827
+ lines.push({
828
+ key: "indicator-bottom",
829
+ text: padToWidth(viewport.hiddenBelowRows > 0 ? `↓ ${viewport.hiddenBelowRows} newer lines` : "", availableCols),
830
+ dim: true,
831
+ });
832
+ lines.push(...composer.lines);
833
+ while (lines.length < availableRows) {
834
+ lines.push({
835
+ key: `tail-filler-${lines.length}`,
836
+ text: " ".repeat(availableCols),
837
+ });
838
+ }
839
+ const visibleLines = lines.slice(0, availableRows);
840
+ return (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleLines.map((line) => (_jsx(Box, { height: 1, overflow: "hidden", children: line.segments ? (line.segments.map((segment, index) => (_jsx(Text, { color: segment.color ?? line.color, backgroundColor: segment.backgroundColor ?? line.backgroundColor, bold: segment.bold ?? line.bold, dimColor: segment.dim ?? line.dim, children: index === 0 && line.protected
841
+ ? `${PROTECTED_ROW_MARKER}${segment.text}`
842
+ : segment.text }, `${line.key}-${index}`)))) : (_jsx(Text, { color: line.color, backgroundColor: line.backgroundColor, bold: line.bold, dimColor: line.dim, children: line.protected
843
+ ? `${PROTECTED_ROW_MARKER}${line.text ?? ""}`
844
+ : (line.text ?? "") })) }, line.key))) }));
845
+ }
846
+ //# sourceMappingURL=fullscreen-chat.js.map