cvc-tui 0.4.0 → 0.4.2

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 (133) hide show
  1. package/dist/entry.js +71148 -61
  2. package/package.json +2 -2
  3. package/dist/app/completion.js +0 -102
  4. package/dist/app/createGatewayEventHandler.js +0 -508
  5. package/dist/app/createSlashHandler.js +0 -101
  6. package/dist/app/delegationStore.js +0 -51
  7. package/dist/app/gatewayContext.js +0 -17
  8. package/dist/app/historyStore.js +0 -123
  9. package/dist/app/inputBuffer.js +0 -120
  10. package/dist/app/inputSelectionStore.js +0 -8
  11. package/dist/app/inputStore.js +0 -28
  12. package/dist/app/interfaces.js +0 -6
  13. package/dist/app/overlayStore.js +0 -40
  14. package/dist/app/promptStore.js +0 -44
  15. package/dist/app/queueStore.js +0 -25
  16. package/dist/app/scroll.js +0 -44
  17. package/dist/app/setupHandoff.js +0 -28
  18. package/dist/app/slash/commands/core.js +0 -479
  19. package/dist/app/slash/commands/debug.js +0 -44
  20. package/dist/app/slash/commands/ops.js +0 -498
  21. package/dist/app/slash/commands/session.js +0 -431
  22. package/dist/app/slash/commands/setup.js +0 -20
  23. package/dist/app/slash/commands/toggles.js +0 -40
  24. package/dist/app/slash/registry.js +0 -18
  25. package/dist/app/slash/types.js +0 -1
  26. package/dist/app/spawnHistoryStore.js +0 -105
  27. package/dist/app/turnController.js +0 -650
  28. package/dist/app/turnStore.js +0 -48
  29. package/dist/app/uiStore.js +0 -36
  30. package/dist/app/useComposerState.js +0 -265
  31. package/dist/app/useConfigSync.js +0 -144
  32. package/dist/app/useInputHandlers.js +0 -403
  33. package/dist/app/useLongRunToolCharms.js +0 -50
  34. package/dist/app/useMainApp.js +0 -629
  35. package/dist/app/useSessionLifecycle.js +0 -175
  36. package/dist/app/useSubmission.js +0 -287
  37. package/dist/app.js +0 -15
  38. package/dist/banner.js +0 -57
  39. package/dist/components/agentsOverlay.js +0 -474
  40. package/dist/components/appChrome.js +0 -252
  41. package/dist/components/appLayout.js +0 -121
  42. package/dist/components/appOverlays.js +0 -65
  43. package/dist/components/branding.js +0 -97
  44. package/dist/components/fpsOverlay.js +0 -22
  45. package/dist/components/helpHint.js +0 -21
  46. package/dist/components/markdown.js +0 -501
  47. package/dist/components/maskedPrompt.js +0 -12
  48. package/dist/components/messageLine.js +0 -82
  49. package/dist/components/modelPicker.js +0 -254
  50. package/dist/components/overlayControls.js +0 -30
  51. package/dist/components/overlays/confirmPrompt.js +0 -25
  52. package/dist/components/overlays/helpOverlay.js +0 -76
  53. package/dist/components/overlays/historySearch.js +0 -49
  54. package/dist/components/overlays/modelPicker.js +0 -60
  55. package/dist/components/overlays/overlayUtils.js +0 -19
  56. package/dist/components/overlays/secretPrompt.js +0 -36
  57. package/dist/components/overlays/sessionPicker.js +0 -93
  58. package/dist/components/overlays/skillsHub.js +0 -71
  59. package/dist/components/prompts.js +0 -95
  60. package/dist/components/queuedMessages.js +0 -24
  61. package/dist/components/sessionPicker.js +0 -130
  62. package/dist/components/skillsHub.js +0 -165
  63. package/dist/components/streamingAssistant.js +0 -35
  64. package/dist/components/streamingMarkdown.js +0 -144
  65. package/dist/components/textInput.js +0 -794
  66. package/dist/components/themed.js +0 -12
  67. package/dist/components/thinking.js +0 -496
  68. package/dist/components/todoPanel.js +0 -40
  69. package/dist/components/transcript.js +0 -22
  70. package/dist/config/env.js +0 -18
  71. package/dist/config/limits.js +0 -22
  72. package/dist/config/timing.js +0 -18
  73. package/dist/content/charms.js +0 -5
  74. package/dist/content/faces.js +0 -21
  75. package/dist/content/fortunes.js +0 -29
  76. package/dist/content/hotkeys.js +0 -38
  77. package/dist/content/placeholders.js +0 -15
  78. package/dist/content/setup.js +0 -14
  79. package/dist/content/verbs.js +0 -41
  80. package/dist/domain/details.js +0 -53
  81. package/dist/domain/messages.js +0 -63
  82. package/dist/domain/paths.js +0 -16
  83. package/dist/domain/providers.js +0 -11
  84. package/dist/domain/roles.js +0 -6
  85. package/dist/domain/slash.js +0 -11
  86. package/dist/domain/usage.js +0 -1
  87. package/dist/domain/viewport.js +0 -33
  88. package/dist/gateway/client.js +0 -312
  89. package/dist/gatewayClient.js +0 -574
  90. package/dist/gatewayTypes.js +0 -1
  91. package/dist/hooks/useCompletion.js +0 -86
  92. package/dist/hooks/useGitBranch.js +0 -58
  93. package/dist/hooks/useInputHistory.js +0 -12
  94. package/dist/hooks/useQueue.js +0 -57
  95. package/dist/hooks/useVirtualHistory.js +0 -401
  96. package/dist/lib/circularBuffer.js +0 -43
  97. package/dist/lib/clipboard.js +0 -126
  98. package/dist/lib/editor.js +0 -41
  99. package/dist/lib/editor.test.js +0 -58
  100. package/dist/lib/emoji.js +0 -49
  101. package/dist/lib/externalCli.js +0 -11
  102. package/dist/lib/forceTruecolor.js +0 -26
  103. package/dist/lib/fpsStore.js +0 -36
  104. package/dist/lib/gracefulExit.js +0 -29
  105. package/dist/lib/history.js +0 -69
  106. package/dist/lib/inputMetrics.js +0 -143
  107. package/dist/lib/liveProgress.js +0 -51
  108. package/dist/lib/liveProgress.test.js +0 -89
  109. package/dist/lib/mathUnicode.js +0 -685
  110. package/dist/lib/memory.js +0 -123
  111. package/dist/lib/memoryMonitor.js +0 -76
  112. package/dist/lib/messages.js +0 -3
  113. package/dist/lib/messages.test.js +0 -25
  114. package/dist/lib/osc52.js +0 -53
  115. package/dist/lib/perfPane.js +0 -94
  116. package/dist/lib/platform.js +0 -312
  117. package/dist/lib/precisionWheel.js +0 -25
  118. package/dist/lib/reasoning.js +0 -39
  119. package/dist/lib/rpc.js +0 -26
  120. package/dist/lib/subagentTree.js +0 -287
  121. package/dist/lib/syntax.js +0 -89
  122. package/dist/lib/terminalModes.js +0 -46
  123. package/dist/lib/terminalParity.js +0 -48
  124. package/dist/lib/terminalSetup.js +0 -321
  125. package/dist/lib/text.js +0 -203
  126. package/dist/lib/text.test.js +0 -18
  127. package/dist/lib/todo.js +0 -2
  128. package/dist/lib/todo.test.js +0 -22
  129. package/dist/lib/viewportStore.js +0 -82
  130. package/dist/lib/virtualHeights.js +0 -61
  131. package/dist/lib/wheelAccel.js +0 -143
  132. package/dist/theme.js +0 -398
  133. package/dist/types.js +0 -1
@@ -1,794 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import * as Ink from '@cvc/ink';
3
- import { useEffect, useMemo, useRef, useState } from 'react';
4
- import { setInputSelection } from '../app/inputSelectionStore.js';
5
- import { readClipboardText, writeClipboardText } from '../lib/clipboard.js';
6
- import { cursorLayout, offsetFromPosition } from '../lib/inputMetrics.js';
7
- import { DEFAULT_VOICE_RECORD_KEY, isActionMod, isMac, isMacActionFallback, isVoiceToggleKey } from '../lib/platform.js';
8
- const ink = Ink;
9
- const { Box, Text, useStdin, useInput, useStdout, stringWidth, useDeclaredCursor, useTerminalFocus } = ink;
10
- const ESC = '\x1b';
11
- const INV = `${ESC}[7m`;
12
- const INV_OFF = `${ESC}[27m`;
13
- const DIM = `${ESC}[2m`;
14
- const DIM_OFF = `${ESC}[22m`;
15
- const FWD_DEL_RE = new RegExp(`${ESC}\\[3(?:[~$^]|;)`);
16
- const PRINTABLE = /^[ -~\u00a0-\uffff]+$/;
17
- const BRACKET_PASTE = new RegExp(`${ESC}?\\[20[01]~`, 'g');
18
- const MULTI_CLICK_MS = 500;
19
- const invert = (s) => INV + s + INV_OFF;
20
- const dim = (s) => DIM + s + DIM_OFF;
21
- let _seg = null;
22
- const seg = () => (_seg ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' }));
23
- const STOP_CACHE_MAX = 32;
24
- const stopCache = new Map();
25
- function graphemeStops(s) {
26
- const hit = stopCache.get(s);
27
- if (hit) {
28
- return hit;
29
- }
30
- const stops = [0];
31
- for (const { index } of seg().segment(s)) {
32
- if (index > 0) {
33
- stops.push(index);
34
- }
35
- }
36
- if (stops.at(-1) !== s.length) {
37
- stops.push(s.length);
38
- }
39
- stopCache.set(s, stops);
40
- if (stopCache.size > STOP_CACHE_MAX) {
41
- const oldest = stopCache.keys().next().value;
42
- if (oldest !== undefined) {
43
- stopCache.delete(oldest);
44
- }
45
- }
46
- return stops;
47
- }
48
- function snapPos(s, p) {
49
- const pos = Math.max(0, Math.min(p, s.length));
50
- let last = 0;
51
- for (const stop of graphemeStops(s)) {
52
- if (stop > pos) {
53
- break;
54
- }
55
- last = stop;
56
- }
57
- return last;
58
- }
59
- function prevPos(s, p) {
60
- const pos = snapPos(s, p);
61
- let prev = 0;
62
- for (const stop of graphemeStops(s)) {
63
- if (stop >= pos) {
64
- return prev;
65
- }
66
- prev = stop;
67
- }
68
- return prev;
69
- }
70
- function nextPos(s, p) {
71
- const pos = snapPos(s, p);
72
- for (const stop of graphemeStops(s)) {
73
- if (stop > pos) {
74
- return stop;
75
- }
76
- }
77
- return s.length;
78
- }
79
- function wordLeft(s, p) {
80
- let i = snapPos(s, p) - 1;
81
- while (i > 0 && /\s/.test(s[i])) {
82
- i--;
83
- }
84
- while (i > 0 && !/\s/.test(s[i - 1])) {
85
- i--;
86
- }
87
- return Math.max(0, i);
88
- }
89
- function wordRight(s, p) {
90
- let i = snapPos(s, p);
91
- while (i < s.length && !/\s/.test(s[i])) {
92
- i++;
93
- }
94
- while (i < s.length && /\s/.test(s[i])) {
95
- i++;
96
- }
97
- return i;
98
- }
99
- /**
100
- * Move cursor one logical line up or down inside `s` while preserving the
101
- * column offset from the current line's start. Returns `null` when the cursor
102
- * is already on the first line (up) or last line (down) — callers use that
103
- * signal to fall through to history cycling instead of eating the arrow key.
104
- */
105
- export function lineNav(s, p, dir) {
106
- const pos = snapPos(s, p);
107
- const curStart = s.lastIndexOf('\n', pos - 1) + 1;
108
- const col = pos - curStart;
109
- if (dir < 0) {
110
- if (curStart === 0) {
111
- return null;
112
- }
113
- const prevStart = s.lastIndexOf('\n', curStart - 2) + 1;
114
- return snapPos(s, Math.min(prevStart + col, curStart - 1));
115
- }
116
- const nextBreak = s.indexOf('\n', pos);
117
- if (nextBreak < 0) {
118
- return null;
119
- }
120
- const nextEnd = s.indexOf('\n', nextBreak + 1);
121
- const lineEnd = nextEnd < 0 ? s.length : nextEnd;
122
- return snapPos(s, Math.min(nextBreak + 1 + col, lineEnd));
123
- }
124
- export { offsetFromPosition };
125
- function renderWithCursor(value, cursor) {
126
- const pos = Math.max(0, Math.min(cursor, value.length));
127
- let out = '', done = false;
128
- for (const { segment, index } of seg().segment(value)) {
129
- if (!done && index >= pos) {
130
- out += invert(index === pos && segment !== '\n' ? segment : ' ');
131
- done = true;
132
- if (index === pos && segment !== '\n') {
133
- continue;
134
- }
135
- }
136
- out += segment;
137
- }
138
- return done ? out : out + invert(' ');
139
- }
140
- function renderWithSelection(value, start, end) {
141
- if (start >= end) {
142
- return value;
143
- }
144
- return value.slice(0, start) + invert(value.slice(start, end) || ' ') + value.slice(end);
145
- }
146
- function useFwdDelete(active) {
147
- const ref = useRef(false);
148
- const { inputEmitter: ee } = useStdin();
149
- useEffect(() => {
150
- if (!active) {
151
- return;
152
- }
153
- const h = (d) => {
154
- ref.current = FWD_DEL_RE.test(d);
155
- };
156
- ee.prependListener('input', h);
157
- return () => {
158
- ee.removeListener('input', h);
159
- };
160
- }, [active, ee]);
161
- return ref;
162
- }
163
- const isPasteResultPromise = (value) => !!value && typeof value.then === 'function';
164
- export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, mask, mouseApiRef, voiceRecordKey = DEFAULT_VOICE_RECORD_KEY, placeholder = '', focus = true }) {
165
- const [cur, setCur] = useState(value.length);
166
- const [sel, setSel] = useState(null);
167
- const fwdDel = useFwdDelete(focus);
168
- const termFocus = useTerminalFocus();
169
- const { stdout } = useStdout();
170
- const curRef = useRef(cur);
171
- const selRef = useRef(null);
172
- const vRef = useRef(value);
173
- const self = useRef(false);
174
- const pasteBuf = useRef('');
175
- const pasteEnd = useRef(null);
176
- const pasteTimer = useRef(null);
177
- const pastePos = useRef(0);
178
- const editVersionRef = useRef(0);
179
- const parentChangeTimer = useRef(null);
180
- const pendingParentValue = useRef(null);
181
- const localRenderTimer = useRef(null);
182
- const lineWidthRef = useRef(stringWidth(value.includes('\n') ? value.slice(value.lastIndexOf('\n') + 1) : value));
183
- const mouseAnchorRef = useRef(null);
184
- const lastClickRef = useRef({ at: 0, offset: -1 });
185
- const undo = useRef([]);
186
- const redo = useRef([]);
187
- const cbChange = useRef(onChange);
188
- const cbSubmit = useRef(onSubmit);
189
- const cbPaste = useRef(onPaste);
190
- cbChange.current = onChange;
191
- cbSubmit.current = onSubmit;
192
- cbPaste.current = onPaste;
193
- const raw = self.current ? vRef.current : value;
194
- const display = mask ? raw.replace(/[^\n]/g, mask[0] ?? '*') : raw;
195
- const selected = useMemo(() => sel && sel.start !== sel.end ? { end: Math.max(sel.start, sel.end), start: Math.min(sel.start, sel.end) } : null, [sel]);
196
- const layout = useMemo(() => cursorLayout(display, cur, columns), [columns, cur, display]);
197
- const boxRef = useDeclaredCursor({
198
- line: layout.line,
199
- column: layout.column,
200
- active: focus && termFocus && !selected
201
- });
202
- // Hide the hardware cursor while a selection is active (prevents
203
- // auto-wrap onto the next row when inverted text fills the column
204
- // exactly) or when the terminal loses focus (suppresses the hollow-rect
205
- // ghost most terminals draw at the parked position).
206
- const hideHardwareCursor = focus && !!stdout?.isTTY && (!!selected || !termFocus);
207
- useEffect(() => {
208
- if (!hideHardwareCursor || !stdout) {
209
- return;
210
- }
211
- stdout.write('\x1b[?25l');
212
- return () => {
213
- stdout.write('\x1b[?25h');
214
- };
215
- }, [hideHardwareCursor, stdout]);
216
- const nativeCursor = focus && termFocus && !selected && !!stdout?.isTTY;
217
- // Placeholder text is just a hint, not a selection — render it dim
218
- // without inverse styling. In a TTY the hardware cursor parks at column
219
- // 0 and visually marks the input start. Non-TTY surfaces still need the
220
- // synthetic inverse first-char to draw a cursor at all.
221
- const rendered = useMemo(() => {
222
- if (!focus) {
223
- return display || dim(placeholder);
224
- }
225
- if (!display && placeholder) {
226
- return nativeCursor ? dim(placeholder) : invert(placeholder[0] ?? ' ') + dim(placeholder.slice(1));
227
- }
228
- if (selected) {
229
- return renderWithSelection(display, selected.start, selected.end);
230
- }
231
- return nativeCursor ? display || ' ' : renderWithCursor(display, cur);
232
- }, [cur, display, focus, nativeCursor, placeholder, selected]);
233
- useEffect(() => {
234
- if (self.current) {
235
- self.current = false;
236
- }
237
- else {
238
- setCur(value.length);
239
- setSel(null);
240
- curRef.current = value.length;
241
- selRef.current = null;
242
- vRef.current = value;
243
- lineWidthRef.current = stringWidth(value.includes('\n') ? value.slice(value.lastIndexOf('\n') + 1) : value);
244
- undo.current = [];
245
- redo.current = [];
246
- }
247
- }, [value]);
248
- useEffect(() => {
249
- if (!focus) {
250
- return;
251
- }
252
- const dropSel = () => {
253
- if (!selRef.current) {
254
- return;
255
- }
256
- selRef.current = null;
257
- setSel(null);
258
- };
259
- setInputSelection({
260
- clear: dropSel,
261
- collapseToEnd: () => {
262
- dropSel();
263
- setCur(vRef.current.length);
264
- curRef.current = vRef.current.length;
265
- },
266
- end: selected?.end ?? curRef.current,
267
- start: selected?.start ?? curRef.current,
268
- value: vRef.current
269
- });
270
- return () => setInputSelection(null);
271
- }, [cur, focus, selected]);
272
- useEffect(() => () => {
273
- if (pasteTimer.current) {
274
- clearTimeout(pasteTimer.current);
275
- }
276
- if (parentChangeTimer.current) {
277
- clearTimeout(parentChangeTimer.current);
278
- }
279
- if (localRenderTimer.current) {
280
- clearTimeout(localRenderTimer.current);
281
- }
282
- }, []);
283
- const flushParentChange = () => {
284
- if (parentChangeTimer.current) {
285
- clearTimeout(parentChangeTimer.current);
286
- parentChangeTimer.current = null;
287
- }
288
- const next = pendingParentValue.current;
289
- pendingParentValue.current = null;
290
- if (next !== null) {
291
- self.current = true;
292
- cbChange.current(next);
293
- }
294
- };
295
- const scheduleParentChange = (next) => {
296
- pendingParentValue.current = next;
297
- if (parentChangeTimer.current) {
298
- return;
299
- }
300
- parentChangeTimer.current = setTimeout(flushParentChange, 16);
301
- };
302
- const cancelLocalRender = () => {
303
- if (localRenderTimer.current) {
304
- clearTimeout(localRenderTimer.current);
305
- localRenderTimer.current = null;
306
- }
307
- };
308
- const scheduleLocalRender = () => {
309
- if (localRenderTimer.current) {
310
- return;
311
- }
312
- localRenderTimer.current = setTimeout(() => {
313
- localRenderTimer.current = null;
314
- setCur(curRef.current);
315
- }, 16);
316
- };
317
- const canFastEchoBase = () => focus && termFocus && !selected && !mask && !!stdout?.isTTY;
318
- const canFastAppend = (current, cursor, text) => {
319
- const sw = stringWidth(text);
320
- return (canFastEchoBase() &&
321
- cursor === current.length &&
322
- current.length > 0 &&
323
- !current.includes('\n') &&
324
- sw === text.length &&
325
- lineWidthRef.current + sw < Math.max(1, columns));
326
- };
327
- const canFastBackspace = (current, cursor) => {
328
- if (!canFastEchoBase() || cursor !== current.length || cursor <= 0 || current.includes('\n')) {
329
- return false;
330
- }
331
- return stringWidth(current.slice(prevPos(current, cursor), cursor)) === 1;
332
- };
333
- const commit = (next, nextCur, track = true, syncParent = true, syncLocal = true, nextLineWidth) => {
334
- const prev = vRef.current;
335
- const c = snapPos(next, nextCur);
336
- editVersionRef.current += 1;
337
- if (selRef.current) {
338
- selRef.current = null;
339
- setSel(null);
340
- }
341
- if (track && next !== prev) {
342
- undo.current.push({ cursor: curRef.current, value: prev });
343
- if (undo.current.length > 200) {
344
- undo.current.shift();
345
- }
346
- redo.current = [];
347
- }
348
- if (syncLocal) {
349
- cancelLocalRender();
350
- setCur(c);
351
- }
352
- else {
353
- scheduleLocalRender();
354
- }
355
- curRef.current = c;
356
- vRef.current = next;
357
- lineWidthRef.current =
358
- nextLineWidth ?? stringWidth(next.includes('\n') ? next.slice(next.lastIndexOf('\n') + 1) : next);
359
- if (next !== prev) {
360
- if (syncParent) {
361
- flushParentChange();
362
- self.current = true;
363
- cbChange.current(next);
364
- }
365
- else {
366
- self.current = true;
367
- scheduleParentChange(next);
368
- }
369
- }
370
- };
371
- const swap = (from, to) => {
372
- const entry = from.current.pop();
373
- if (!entry) {
374
- return;
375
- }
376
- to.current.push({ cursor: curRef.current, value: vRef.current });
377
- commit(entry.value, entry.cursor, false);
378
- };
379
- const emitPaste = (e) => {
380
- const startVersion = editVersionRef.current;
381
- const h = cbPaste.current?.(e);
382
- if (isPasteResultPromise(h)) {
383
- const fallbackText = e.text;
384
- void h
385
- .then(result => {
386
- if (result && editVersionRef.current === startVersion) {
387
- commit(result.value, result.cursor);
388
- }
389
- else if (result && fallbackText && PRINTABLE.test(fallbackText)) {
390
- // User typed while async paste was in-flight — fall back to raw text insert
391
- // so the pasted content is not silently lost.
392
- const cur = curRef.current;
393
- const v = vRef.current;
394
- commit(v.slice(0, cur) + fallbackText + v.slice(cur), cur + fallbackText.length);
395
- }
396
- })
397
- .catch(() => { });
398
- return true;
399
- }
400
- if (h) {
401
- commit(h.value, h.cursor);
402
- }
403
- return !!h;
404
- };
405
- const flushPaste = () => {
406
- const text = pasteBuf.current;
407
- const at = pastePos.current;
408
- const end = pasteEnd.current ?? at;
409
- pasteBuf.current = '';
410
- pasteEnd.current = null;
411
- pasteTimer.current = null;
412
- if (!text) {
413
- return;
414
- }
415
- if (!emitPaste({ cursor: at, text, value: vRef.current }) && PRINTABLE.test(text)) {
416
- commit(vRef.current.slice(0, at) + text + vRef.current.slice(end), at + text.length);
417
- }
418
- };
419
- const clearSel = () => {
420
- if (!selRef.current) {
421
- return;
422
- }
423
- selRef.current = null;
424
- setSel(null);
425
- };
426
- const selectAll = () => {
427
- const end = vRef.current.length;
428
- if (!end) {
429
- return;
430
- }
431
- const next = { end, start: 0 };
432
- selRef.current = next;
433
- setSel(next);
434
- setCur(end);
435
- curRef.current = end;
436
- };
437
- const moveCursor = (next, extend = false) => {
438
- const c = snapPos(vRef.current, next);
439
- const anchor = selRef.current?.start ?? curRef.current;
440
- if (!extend || anchor === c) {
441
- clearSel();
442
- }
443
- else {
444
- const nextSel = { end: c, start: anchor };
445
- selRef.current = nextSel;
446
- setSel(nextSel);
447
- }
448
- setCur(c);
449
- curRef.current = c;
450
- };
451
- const selRange = () => {
452
- const range = selRef.current;
453
- return range && range.start !== range.end
454
- ? { end: Math.max(range.start, range.end), start: Math.min(range.start, range.end) }
455
- : null;
456
- };
457
- const ins = (v, c, s) => v.slice(0, c) + s + v.slice(c);
458
- const pastePlainText = (text) => {
459
- const cleaned = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
460
- if (!cleaned) {
461
- return;
462
- }
463
- const range = selRange();
464
- const nextValue = range
465
- ? vRef.current.slice(0, range.start) + cleaned + vRef.current.slice(range.end)
466
- : vRef.current.slice(0, curRef.current) + cleaned + vRef.current.slice(curRef.current);
467
- const nextCursor = range ? range.start + cleaned.length : curRef.current + cleaned.length;
468
- commit(nextValue, nextCursor);
469
- };
470
- const startMouseSelection = (next) => {
471
- const c = snapPos(vRef.current, next);
472
- mouseAnchorRef.current = c;
473
- selRef.current = { end: c, start: c };
474
- setSel(null);
475
- setCur(c);
476
- curRef.current = c;
477
- };
478
- const dragMouseSelection = (next) => {
479
- if (mouseAnchorRef.current === null) {
480
- return;
481
- }
482
- const c = snapPos(vRef.current, next);
483
- const range = { end: c, start: mouseAnchorRef.current };
484
- selRef.current = range;
485
- setSel(range.start === range.end ? null : range);
486
- setCur(c);
487
- curRef.current = c;
488
- };
489
- const endMouseSelection = () => {
490
- mouseAnchorRef.current = null;
491
- const range = selRef.current;
492
- if (range && range.start === range.end) {
493
- selRef.current = null;
494
- setSel(null);
495
- return;
496
- }
497
- const normalized = selRange();
498
- if (isMac && normalized) {
499
- void writeClipboardText(vRef.current.slice(normalized.start, normalized.end));
500
- }
501
- };
502
- const offsetAt = (e) => offsetFromPosition(display, e.localRow ?? 0, e.localCol ?? 0, columns);
503
- const isMultiClickAt = (offset) => {
504
- const now = Date.now();
505
- const last = lastClickRef.current;
506
- lastClickRef.current = { at: now, offset };
507
- return now - last.at < MULTI_CLICK_MS && offset === last.offset;
508
- };
509
- if (mouseApiRef) {
510
- mouseApiRef.current = {
511
- dragAt: (row, col) => dragMouseSelection(offsetFromPosition(display, row, col, columns)),
512
- end: endMouseSelection,
513
- startAtBeginning: () => startMouseSelection(0)
514
- };
515
- }
516
- useInput((inp, k, event) => {
517
- const eventRaw = event.keypress.raw;
518
- // Configured voice shortcut wins over composer-level defaults like
519
- // paste/copy so users who bind voice to ctrl+v / alt+v / cmd+v
520
- // actually get voice toggled instead of a paste (Copilot round-7
521
- // follow-up on #19835). The pass-through predicate is a no-op for
522
- // ordinary typing and plain paste when voice is unbound to 'v'.
523
- if (shouldPassThroughToGlobalHandler(inp, k, voiceRecordKey)) {
524
- return;
525
- }
526
- if (eventRaw === '\x1bv' ||
527
- eventRaw === '\x1bV' ||
528
- eventRaw === '\x16' ||
529
- (isMac && isActionMod(k) && inp.toLowerCase() === 'v')) {
530
- if (cbPaste.current) {
531
- return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current });
532
- }
533
- if (isMac) {
534
- void readClipboardText().then(text => {
535
- if (text) {
536
- pastePlainText(text);
537
- }
538
- });
539
- }
540
- return;
541
- }
542
- if (isMac && isActionMod(k) && inp.toLowerCase() === 'c') {
543
- const range = selRange();
544
- if (range) {
545
- const text = vRef.current.slice(range.start, range.end);
546
- void writeClipboardText(text);
547
- }
548
- return;
549
- }
550
- if (k.upArrow || k.downArrow) {
551
- const next = lineNav(vRef.current, curRef.current, k.upArrow ? -1 : 1);
552
- if (next !== null) {
553
- moveCursor(next, k.shift);
554
- return;
555
- }
556
- return;
557
- }
558
- if (k.return) {
559
- if (k.shift || k.ctrl || (isMac ? isActionMod(k) : k.meta)) {
560
- flushParentChange();
561
- commit(ins(vRef.current, curRef.current, '\n'), curRef.current + 1);
562
- }
563
- else {
564
- flushParentChange();
565
- cbSubmit.current?.(vRef.current);
566
- }
567
- return;
568
- }
569
- let c = curRef.current;
570
- let v = vRef.current;
571
- const mod = isActionMod(k);
572
- const wordMod = mod || k.meta;
573
- const actionHome = k.home || (!isMac && mod && inp === 'a') || isMacActionFallback(k, inp, 'a');
574
- const actionEnd = k.end || (mod && inp === 'e') || isMacActionFallback(k, inp, 'e');
575
- const actionDeleteToStart = (mod && inp === 'u') || isMacActionFallback(k, inp, 'u');
576
- const actionKillToEnd = (mod && inp === 'k') || isMacActionFallback(k, inp, 'k');
577
- const actionDeleteWord = (mod && inp === 'w') || isMacActionFallback(k, inp, 'w');
578
- const range = selRange();
579
- const delFwd = k.delete || fwdDel.current;
580
- if (mod && inp === 'z') {
581
- return swap(undo, redo);
582
- }
583
- if ((mod && inp === 'y') || (mod && k.shift && inp === 'z')) {
584
- return swap(redo, undo);
585
- }
586
- if (isMac && mod && inp === 'a') {
587
- return selectAll();
588
- }
589
- if (actionHome) {
590
- c = 0;
591
- moveCursor(c, k.shift);
592
- return;
593
- }
594
- else if (actionEnd) {
595
- c = v.length;
596
- moveCursor(c, k.shift);
597
- return;
598
- }
599
- else if (k.leftArrow) {
600
- if (range && !wordMod && !k.shift) {
601
- clearSel();
602
- c = range.start;
603
- }
604
- else {
605
- c = wordMod ? wordLeft(v, c) : prevPos(v, c);
606
- }
607
- moveCursor(c, k.shift);
608
- return;
609
- }
610
- else if (k.rightArrow) {
611
- if (range && !wordMod && !k.shift) {
612
- clearSel();
613
- c = range.end;
614
- }
615
- else {
616
- c = wordMod ? wordRight(v, c) : nextPos(v, c);
617
- }
618
- moveCursor(c, k.shift);
619
- return;
620
- }
621
- else if (wordMod && inp === 'b') {
622
- clearSel();
623
- c = wordLeft(v, c);
624
- }
625
- else if (wordMod && inp === 'f') {
626
- clearSel();
627
- c = wordRight(v, c);
628
- }
629
- else if (range && (k.backspace || delFwd)) {
630
- v = v.slice(0, range.start) + v.slice(range.end);
631
- c = range.start;
632
- }
633
- else if (k.backspace && c > 0) {
634
- if (wordMod) {
635
- const t = wordLeft(v, c);
636
- v = v.slice(0, t) + v.slice(c);
637
- c = t;
638
- }
639
- else if (canFastBackspace(v, c)) {
640
- const t = prevPos(v, c);
641
- v = v.slice(0, t) + v.slice(c);
642
- c = t;
643
- stdout.write('\b \b');
644
- commit(v, c, true, false, false, Math.max(0, lineWidthRef.current - 1));
645
- return;
646
- }
647
- else {
648
- const t = prevPos(v, c);
649
- v = v.slice(0, t) + v.slice(c);
650
- c = t;
651
- }
652
- }
653
- else if (delFwd && c < v.length) {
654
- if (wordMod) {
655
- const t = wordRight(v, c);
656
- v = v.slice(0, c) + v.slice(t);
657
- }
658
- else {
659
- v = v.slice(0, c) + v.slice(nextPos(v, c));
660
- }
661
- }
662
- else if (actionDeleteWord) {
663
- if (range) {
664
- v = v.slice(0, range.start) + v.slice(range.end);
665
- c = range.start;
666
- }
667
- else if (c > 0) {
668
- clearSel();
669
- const t = wordLeft(v, c);
670
- v = v.slice(0, t) + v.slice(c);
671
- c = t;
672
- }
673
- else {
674
- return;
675
- }
676
- }
677
- else if (actionDeleteToStart) {
678
- if (range) {
679
- v = v.slice(0, range.start) + v.slice(range.end);
680
- c = range.start;
681
- }
682
- else {
683
- v = v.slice(c);
684
- c = 0;
685
- }
686
- }
687
- else if (actionKillToEnd) {
688
- if (range) {
689
- v = v.slice(0, range.start) + v.slice(range.end);
690
- c = range.start;
691
- }
692
- else {
693
- v = v.slice(0, c);
694
- }
695
- }
696
- else if (event.keypress.isPasted || inp.length > 0) {
697
- const bracketed = event.keypress.isPasted || inp.includes('[200~');
698
- const text = inp.replace(BRACKET_PASTE, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
699
- if (bracketed && emitPaste({ bracketed: true, cursor: c, text, value: v })) {
700
- return;
701
- }
702
- if (!text) {
703
- return;
704
- }
705
- if (text === '\n') {
706
- return commit(ins(v, c, '\n'), c + 1);
707
- }
708
- if (text.length > 1 || text.includes('\n')) {
709
- if (!pasteBuf.current) {
710
- pastePos.current = range ? range.start : c;
711
- pasteEnd.current = range ? range.end : pastePos.current;
712
- }
713
- pasteBuf.current += text;
714
- if (pasteTimer.current) {
715
- clearTimeout(pasteTimer.current);
716
- }
717
- pasteTimer.current = setTimeout(flushPaste, 50);
718
- return;
719
- }
720
- if (PRINTABLE.test(text)) {
721
- if (range) {
722
- v = v.slice(0, range.start) + text + v.slice(range.end);
723
- c = range.start + text.length;
724
- }
725
- else {
726
- const simpleAppend = canFastAppend(v, c, text);
727
- v = v.slice(0, c) + text + v.slice(c);
728
- c += text.length;
729
- if (simpleAppend) {
730
- stdout.write(text);
731
- commit(v, c, true, false, false, lineWidthRef.current + stringWidth(text));
732
- return;
733
- }
734
- }
735
- }
736
- else {
737
- return;
738
- }
739
- }
740
- else {
741
- return;
742
- }
743
- commit(v, c);
744
- }, { isActive: focus });
745
- return (_jsx(Box, { onClick: (e) => {
746
- if (!focus) {
747
- return;
748
- }
749
- e.stopImmediatePropagation?.();
750
- clearSel();
751
- const next = offsetAt(e);
752
- setCur(next);
753
- curRef.current = next;
754
- }, onMouseDown: (e) => {
755
- if (!focus) {
756
- return;
757
- }
758
- // Right-click → route through the same path as Alt+V so the composer
759
- // clipboard RPC (text or image) handles it.
760
- if (e.button === 2) {
761
- e.stopImmediatePropagation?.();
762
- emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current });
763
- return;
764
- }
765
- if (e.button !== 0) {
766
- return;
767
- }
768
- e.stopImmediatePropagation?.();
769
- const offset = offsetAt(e);
770
- if (isMultiClickAt(offset)) {
771
- mouseAnchorRef.current = null;
772
- selectAll();
773
- return;
774
- }
775
- startMouseSelection(offset);
776
- }, onMouseDrag: (e) => {
777
- if (!focus || e.button !== 0 || mouseAnchorRef.current === null) {
778
- return;
779
- }
780
- e.stopImmediatePropagation?.();
781
- dragMouseSelection(offsetAt(e));
782
- }, onMouseUp: (e) => {
783
- e.stopImmediatePropagation?.();
784
- endMouseSelection();
785
- }, ref: boxRef, width: columns, children: _jsx(Text, { wrap: "wrap", children: rendered }) }));
786
- }
787
- export const shouldPassThroughToGlobalHandler = (input, key, voiceRecordKey = DEFAULT_VOICE_RECORD_KEY) => (key.ctrl && input === 'c') ||
788
- (key.ctrl && input === 'x') ||
789
- key.tab ||
790
- (key.shift && key.tab) ||
791
- key.pageUp ||
792
- key.pageDown ||
793
- key.escape ||
794
- isVoiceToggleKey(key, input, voiceRecordKey);