@supatest/cli 0.0.4 → 0.0.5

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 (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. package/package.json +3 -4
@@ -0,0 +1,527 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useStdin } from 'ink';
7
+ import React, { createContext, useCallback, useContext, useEffect, useRef, } from 'react';
8
+ // Simple debug logger stub
9
+ const debugLogger = {
10
+ warn: (...args) => console.warn(...args),
11
+ error: (...args) => console.error(...args),
12
+ debug: (...args) => console.debug(...args),
13
+ log: (...args) => console.log(...args),
14
+ };
15
+ const createStubConfig = () => ({
16
+ getDebugMode: () => false,
17
+ });
18
+ import { AppEvent, appEvents } from '../../utils/events';
19
+ import { FOCUS_IN, FOCUS_OUT } from '../hooks/useFocus';
20
+ import { ESC } from '../utils/input.js';
21
+ import { parseMouseEvent } from '../utils/mouse';
22
+ export const BACKSLASH_ENTER_TIMEOUT = 5;
23
+ export const ESC_TIMEOUT = 50;
24
+ export const PASTE_TIMEOUT = 30_000;
25
+ // Parse the key itself
26
+ const KEY_INFO_MAP = {
27
+ '[200~': { name: 'paste-start' },
28
+ '[201~': { name: 'paste-end' },
29
+ '[[A': { name: 'f1' },
30
+ '[[B': { name: 'f2' },
31
+ '[[C': { name: 'f3' },
32
+ '[[D': { name: 'f4' },
33
+ '[[E': { name: 'f5' },
34
+ '[1~': { name: 'home' },
35
+ '[2~': { name: 'insert' },
36
+ '[3~': { name: 'delete' },
37
+ '[4~': { name: 'end' },
38
+ '[5~': { name: 'pageup' },
39
+ '[6~': { name: 'pagedown' },
40
+ '[7~': { name: 'home' },
41
+ '[8~': { name: 'end' },
42
+ '[11~': { name: 'f1' },
43
+ '[12~': { name: 'f2' },
44
+ '[13~': { name: 'f3' },
45
+ '[14~': { name: 'f4' },
46
+ '[15~': { name: 'f5' },
47
+ '[17~': { name: 'f6' },
48
+ '[18~': { name: 'f7' },
49
+ '[19~': { name: 'f8' },
50
+ '[20~': { name: 'f9' },
51
+ '[21~': { name: 'f10' },
52
+ '[23~': { name: 'f11' },
53
+ '[24~': { name: 'f12' },
54
+ '[A': { name: 'up' },
55
+ '[B': { name: 'down' },
56
+ '[C': { name: 'right' },
57
+ '[D': { name: 'left' },
58
+ '[E': { name: 'clear' },
59
+ '[F': { name: 'end' },
60
+ '[H': { name: 'home' },
61
+ '[P': { name: 'f1' },
62
+ '[Q': { name: 'f2' },
63
+ '[R': { name: 'f3' },
64
+ '[S': { name: 'f4' },
65
+ OA: { name: 'up' },
66
+ OB: { name: 'down' },
67
+ OC: { name: 'right' },
68
+ OD: { name: 'left' },
69
+ OE: { name: 'clear' },
70
+ OF: { name: 'end' },
71
+ OH: { name: 'home' },
72
+ OP: { name: 'f1' },
73
+ OQ: { name: 'f2' },
74
+ OR: { name: 'f3' },
75
+ OS: { name: 'f4' },
76
+ '[[5~': { name: 'pageup' },
77
+ '[[6~': { name: 'pagedown' },
78
+ '[9u': { name: 'tab' },
79
+ '[13u': { name: 'return' },
80
+ '[27u': { name: 'escape' },
81
+ '[127u': { name: 'backspace' },
82
+ '[57414u': { name: 'return' }, // Numpad Enter
83
+ '[a': { name: 'up', shift: true },
84
+ '[b': { name: 'down', shift: true },
85
+ '[c': { name: 'right', shift: true },
86
+ '[d': { name: 'left', shift: true },
87
+ '[e': { name: 'clear', shift: true },
88
+ '[2$': { name: 'insert', shift: true },
89
+ '[3$': { name: 'delete', shift: true },
90
+ '[5$': { name: 'pageup', shift: true },
91
+ '[6$': { name: 'pagedown', shift: true },
92
+ '[7$': { name: 'home', shift: true },
93
+ '[8$': { name: 'end', shift: true },
94
+ '[Z': { name: 'tab', shift: true },
95
+ Oa: { name: 'up', ctrl: true },
96
+ Ob: { name: 'down', ctrl: true },
97
+ Oc: { name: 'right', ctrl: true },
98
+ Od: { name: 'left', ctrl: true },
99
+ Oe: { name: 'clear', ctrl: true },
100
+ '[2^': { name: 'insert', ctrl: true },
101
+ '[3^': { name: 'delete', ctrl: true },
102
+ '[5^': { name: 'pageup', ctrl: true },
103
+ '[6^': { name: 'pagedown', ctrl: true },
104
+ '[7^': { name: 'home', ctrl: true },
105
+ '[8^': { name: 'end', ctrl: true },
106
+ };
107
+ const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
108
+ function charLengthAt(str, i) {
109
+ if (str.length <= i) {
110
+ // Pretend to move to the right. This is necessary to autocomplete while
111
+ // moving to the right.
112
+ return 1;
113
+ }
114
+ const code = str.codePointAt(i);
115
+ return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
116
+ }
117
+ const MAC_ALT_KEY_CHARACTER_MAP = {
118
+ '\u222B': 'b', // "∫" back one word
119
+ '\u0192': 'f', // "ƒ" forward one word
120
+ '\u00B5': 'm', // "µ" toggle markup view
121
+ };
122
+ function nonKeyboardEventFilter(keypressHandler) {
123
+ return (key) => {
124
+ if (!parseMouseEvent(key.sequence) &&
125
+ key.sequence !== FOCUS_IN &&
126
+ key.sequence !== FOCUS_OUT) {
127
+ keypressHandler(key);
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Buffers "/" keys to see if they are followed return.
133
+ * Will flush the buffer if no data is received for DRAG_COMPLETION_TIMEOUT_MS
134
+ * or when a null key is received.
135
+ */
136
+ function bufferBackslashEnter(keypressHandler) {
137
+ const bufferer = (function* () {
138
+ while (true) {
139
+ const key = yield;
140
+ if (key == null) {
141
+ continue;
142
+ }
143
+ else if (key.sequence !== '\\') {
144
+ keypressHandler(key);
145
+ continue;
146
+ }
147
+ const timeoutId = setTimeout(() => bufferer.next(null), BACKSLASH_ENTER_TIMEOUT);
148
+ const nextKey = yield;
149
+ clearTimeout(timeoutId);
150
+ if (nextKey === null) {
151
+ keypressHandler(key);
152
+ }
153
+ else if (nextKey.name === 'return') {
154
+ keypressHandler({
155
+ ...nextKey,
156
+ shift: true,
157
+ sequence: '\r', // Corrected escaping for newline
158
+ });
159
+ }
160
+ else {
161
+ keypressHandler(key);
162
+ keypressHandler(nextKey);
163
+ }
164
+ }
165
+ })();
166
+ bufferer.next(); // prime the generator so it starts listening.
167
+ return (key) => bufferer.next(key);
168
+ }
169
+ /**
170
+ * Buffers paste events between paste-start and paste-end sequences.
171
+ * Will flush the buffer if no data is received for PASTE_TIMEOUT ms or
172
+ * when a null key is received.
173
+ */
174
+ function bufferPaste(keypressHandler) {
175
+ const bufferer = (function* () {
176
+ while (true) {
177
+ let key = yield;
178
+ if (key === null) {
179
+ continue;
180
+ }
181
+ else if (key.name !== 'paste-start') {
182
+ keypressHandler(key);
183
+ continue;
184
+ }
185
+ let buffer = '';
186
+ while (true) {
187
+ const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT);
188
+ key = yield;
189
+ clearTimeout(timeoutId);
190
+ if (key === null) {
191
+ appEvents.emit(AppEvent.PasteTimeout);
192
+ break;
193
+ }
194
+ if (key.name === 'paste-end') {
195
+ break;
196
+ }
197
+ buffer += key.sequence;
198
+ }
199
+ if (buffer.length > 0) {
200
+ keypressHandler({
201
+ name: '',
202
+ ctrl: false,
203
+ meta: false,
204
+ shift: false,
205
+ paste: true,
206
+ insertable: true,
207
+ sequence: buffer,
208
+ });
209
+ }
210
+ }
211
+ })();
212
+ bufferer.next(); // prime the generator so it starts listening.
213
+ return (key) => bufferer.next(key);
214
+ }
215
+ /**
216
+ * Turns raw data strings into keypress events sent to the provided handler.
217
+ * Buffers escape sequences until a full sequence is received or
218
+ * until a timeout occurs.
219
+ */
220
+ function createDataListener(keypressHandler) {
221
+ const parser = emitKeys(keypressHandler);
222
+ parser.next(); // prime the generator so it starts listening.
223
+ let timeoutId;
224
+ return (data) => {
225
+ clearTimeout(timeoutId);
226
+ for (const char of data) {
227
+ parser.next(char);
228
+ }
229
+ if (data.length !== 0) {
230
+ timeoutId = setTimeout(() => parser.next(''), ESC_TIMEOUT);
231
+ }
232
+ };
233
+ }
234
+ /**
235
+ * Translates raw keypress characters into key events.
236
+ * Buffers escape sequences until a full sequence is received or
237
+ * until an empty string is sent to indicate a timeout.
238
+ */
239
+ function* emitKeys(keypressHandler) {
240
+ while (true) {
241
+ let ch = yield;
242
+ let sequence = ch;
243
+ let escaped = false;
244
+ let name = undefined;
245
+ let ctrl = false;
246
+ let meta = false;
247
+ let shift = false;
248
+ let code = undefined;
249
+ let insertable = false;
250
+ if (ch === ESC) {
251
+ escaped = true;
252
+ ch = yield;
253
+ sequence += ch;
254
+ if (ch === ESC) {
255
+ ch = yield;
256
+ sequence += ch;
257
+ }
258
+ }
259
+ if (escaped && (ch === 'O' || ch === '[')) {
260
+ // ANSI escape sequence
261
+ code = ch;
262
+ let modifier = 0;
263
+ if (ch === 'O') {
264
+ // ESC O letter
265
+ // ESC O modifier letter
266
+ ch = yield;
267
+ sequence += ch;
268
+ if (ch >= '0' && ch <= '9') {
269
+ modifier = parseInt(ch, 10) - 1;
270
+ ch = yield;
271
+ sequence += ch;
272
+ }
273
+ code += ch;
274
+ }
275
+ else if (ch === '[') {
276
+ // ESC [ letter
277
+ // ESC [ modifier letter
278
+ // ESC [ [ modifier letter
279
+ // ESC [ [ num char
280
+ ch = yield;
281
+ sequence += ch;
282
+ if (ch === '[') {
283
+ // \x1b[[A
284
+ // ^--- escape codes might have a second bracket
285
+ code += ch;
286
+ ch = yield;
287
+ sequence += ch;
288
+ }
289
+ /*
290
+ * Here and later we try to buffer just enough data to get
291
+ * a complete ascii sequence.
292
+ *
293
+ * We have basically two classes of ascii characters to process:
294
+ *
295
+ *
296
+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
297
+ *
298
+ * This particular example is featuring Ctrl+F12 in xterm.
299
+ *
300
+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
301
+ * - first part can contain one or two digits
302
+ * - there is also special case when there can be 3 digits
303
+ * but without modifier. They are the case of paste bracket mode
304
+ *
305
+ * So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
306
+ *
307
+ *
308
+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
309
+ *
310
+ * This particular example is featuring Ctrl+Home in xterm.
311
+ *
312
+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
313
+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
314
+ *
315
+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
316
+ *
317
+ */
318
+ const cmdStart = sequence.length - 1;
319
+ // collect as many digits as possible
320
+ while (ch >= '0' && ch <= '9') {
321
+ ch = yield;
322
+ sequence += ch;
323
+ }
324
+ // skip modifier
325
+ if (ch === ';') {
326
+ while (ch === ';') {
327
+ ch = yield;
328
+ sequence += ch;
329
+ // collect as many digits as possible
330
+ while (ch >= '0' && ch <= '9') {
331
+ ch = yield;
332
+ sequence += ch;
333
+ }
334
+ }
335
+ }
336
+ else if (ch === '<') {
337
+ // SGR mouse mode
338
+ ch = yield;
339
+ sequence += ch;
340
+ // Don't skip on empty string here to avoid timeouts on slow events.
341
+ while (ch === '' || ch === ';' || (ch >= '0' && ch <= '9')) {
342
+ ch = yield;
343
+ sequence += ch;
344
+ }
345
+ }
346
+ else if (ch === 'M') {
347
+ // X11 mouse mode
348
+ // three characters after 'M'
349
+ ch = yield;
350
+ sequence += ch;
351
+ ch = yield;
352
+ sequence += ch;
353
+ ch = yield;
354
+ sequence += ch;
355
+ }
356
+ /*
357
+ * We buffered enough data, now trying to extract code
358
+ * and modifier from it
359
+ */
360
+ const cmd = sequence.slice(cmdStart);
361
+ let match;
362
+ if ((match = /^(\d+)(?:;(\d+))?(?:;(\d+))?([~^$u])$/.exec(cmd))) {
363
+ if (match[1] === '27' && match[3] && match[4] === '~') {
364
+ // modifyOtherKeys format: CSI 27 ; modifier ; key ~
365
+ // Treat as CSI u: key + 'u'
366
+ code += match[3] + 'u';
367
+ modifier = parseInt(match[2] ?? '1', 10) - 1;
368
+ }
369
+ else {
370
+ code += match[1] + match[4];
371
+ // Defaults to '1' if no modifier exists, resulting in a 0 modifier value
372
+ modifier = parseInt(match[2] ?? '1', 10) - 1;
373
+ }
374
+ }
375
+ else if ((match = /^(\d+)?(?:;(\d+))?([A-Za-z])$/.exec(cmd))) {
376
+ code += match[3];
377
+ modifier = parseInt(match[2] ?? match[1] ?? '1', 10) - 1;
378
+ }
379
+ else {
380
+ code += cmd;
381
+ }
382
+ }
383
+ // Parse the key modifier
384
+ ctrl = !!(modifier & 4);
385
+ meta = !!(modifier & 10); // use 10 to catch both alt (2) and meta (8).
386
+ shift = !!(modifier & 1);
387
+ const keyInfo = KEY_INFO_MAP[code];
388
+ if (keyInfo) {
389
+ name = keyInfo.name;
390
+ if (keyInfo.shift) {
391
+ shift = true;
392
+ }
393
+ if (keyInfo.ctrl) {
394
+ ctrl = true;
395
+ }
396
+ }
397
+ else {
398
+ name = 'undefined';
399
+ if ((ctrl || meta) && (code.endsWith('u') || code.endsWith('~'))) {
400
+ // CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
401
+ const codeNumber = parseInt(code.slice(1, -1), 10);
402
+ if (codeNumber >= 'a'.charCodeAt(0) &&
403
+ codeNumber <= 'z'.charCodeAt(0)) {
404
+ name = String.fromCharCode(codeNumber);
405
+ }
406
+ }
407
+ }
408
+ }
409
+ else if (ch === '\r') {
410
+ // carriage return
411
+ name = 'return';
412
+ meta = escaped;
413
+ }
414
+ else if (ch === '\n') {
415
+ // Enter, should have been called linefeed
416
+ name = 'enter';
417
+ meta = escaped;
418
+ }
419
+ else if (ch === '\t') {
420
+ // tab
421
+ name = 'tab';
422
+ meta = escaped;
423
+ }
424
+ else if (ch === '\b' || ch === '\x7f') {
425
+ // backspace or ctrl+h
426
+ name = 'backspace';
427
+ meta = escaped;
428
+ }
429
+ else if (ch === ESC) {
430
+ // escape key
431
+ name = 'escape';
432
+ meta = escaped;
433
+ }
434
+ else if (ch === ' ') {
435
+ name = 'space';
436
+ meta = escaped;
437
+ insertable = true;
438
+ }
439
+ else if (!escaped && ch <= '\x1a') {
440
+ // ctrl+letter
441
+ name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
442
+ ctrl = true;
443
+ }
444
+ else if (/^[0-9A-Za-z]$/.exec(ch) !== null) {
445
+ // Letter, number, shift+letter
446
+ name = ch.toLowerCase();
447
+ shift = /^[A-Z]$/.exec(ch) !== null;
448
+ meta = escaped;
449
+ insertable = true;
450
+ }
451
+ else if (MAC_ALT_KEY_CHARACTER_MAP[ch] && process.platform === 'darwin') {
452
+ name = MAC_ALT_KEY_CHARACTER_MAP[ch];
453
+ meta = true;
454
+ }
455
+ else if (sequence === `${ESC}${ESC}`) {
456
+ // Double escape
457
+ name = 'escape';
458
+ meta = true;
459
+ // Emit first escape key here, then continue processing
460
+ keypressHandler({
461
+ name: 'escape',
462
+ ctrl,
463
+ meta,
464
+ shift,
465
+ paste: false,
466
+ insertable: false,
467
+ sequence: ESC,
468
+ });
469
+ }
470
+ else if (escaped) {
471
+ // Escape sequence timeout
472
+ name = ch.length ? undefined : 'escape';
473
+ meta = true;
474
+ }
475
+ else {
476
+ // Any other character is considered printable.
477
+ insertable = true;
478
+ }
479
+ if ((sequence.length !== 0 && (name !== undefined || escaped)) ||
480
+ charLengthAt(sequence, 0) === sequence.length) {
481
+ keypressHandler({
482
+ name: name || '',
483
+ ctrl,
484
+ meta,
485
+ shift,
486
+ paste: false,
487
+ insertable,
488
+ sequence,
489
+ });
490
+ }
491
+ // Unrecognized or broken escape sequence, don't emit anything
492
+ }
493
+ }
494
+ const KeypressContext = createContext(undefined);
495
+ export function useKeypressContext() {
496
+ const context = useContext(KeypressContext);
497
+ if (!context) {
498
+ throw new Error('useKeypressContext must be used within a KeypressProvider');
499
+ }
500
+ return context;
501
+ }
502
+ export function KeypressProvider({ children, config, debugKeystrokeLogging, }) {
503
+ const { stdin, setRawMode } = useStdin();
504
+ const subscribers = useRef(new Set()).current;
505
+ const subscribe = useCallback((handler) => subscribers.add(handler), [subscribers]);
506
+ const unsubscribe = useCallback((handler) => subscribers.delete(handler), [subscribers]);
507
+ const broadcast = useCallback((key) => subscribers.forEach((handler) => handler(key)), [subscribers]);
508
+ useEffect(() => {
509
+ const wasRaw = stdin.isRaw;
510
+ if (wasRaw === false) {
511
+ setRawMode(true);
512
+ }
513
+ process.stdin.setEncoding('utf8'); // Make data events emit strings
514
+ const mouseFilterer = nonKeyboardEventFilter(broadcast);
515
+ const backslashBufferer = bufferBackslashEnter(mouseFilterer);
516
+ const pasteBufferer = bufferPaste(backslashBufferer);
517
+ let dataListener = createDataListener(pasteBufferer);
518
+ stdin.on('data', dataListener);
519
+ return () => {
520
+ stdin.removeListener('data', dataListener);
521
+ if (wasRaw === false) {
522
+ setRawMode(false);
523
+ }
524
+ };
525
+ }, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]);
526
+ return (React.createElement(KeypressContext.Provider, { value: { subscribe, unsubscribe } }, children));
527
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { useStdin } from 'ink';
7
+ import React, { createContext, useCallback, useContext, useEffect, useRef, } from 'react';
8
+ import { AppEvent, appEvents } from '../../utils/events';
9
+ import { ESC } from '../utils/input';
10
+ import { isIncompleteMouseSequence, parseMouseEvent, } from '../utils/mouse';
11
+ const MAX_MOUSE_BUFFER_SIZE = 4096;
12
+ const MouseContext = createContext(undefined);
13
+ export function useMouseContext() {
14
+ const context = useContext(MouseContext);
15
+ if (!context) {
16
+ throw new Error('useMouseContext must be used within a MouseProvider');
17
+ }
18
+ return context;
19
+ }
20
+ export function useMouse(handler, { isActive = true } = {}) {
21
+ const { subscribe, unsubscribe } = useMouseContext();
22
+ useEffect(() => {
23
+ if (!isActive) {
24
+ return;
25
+ }
26
+ subscribe(handler);
27
+ return () => unsubscribe(handler);
28
+ }, [isActive, handler, subscribe, unsubscribe]);
29
+ }
30
+ export function MouseProvider({ children, mouseEventsEnabled, debugKeystrokeLogging, }) {
31
+ const { stdin } = useStdin();
32
+ const subscribers = useRef(new Set()).current;
33
+ const subscribe = useCallback((handler) => {
34
+ subscribers.add(handler);
35
+ }, [subscribers]);
36
+ const unsubscribe = useCallback((handler) => {
37
+ subscribers.delete(handler);
38
+ }, [subscribers]);
39
+ useEffect(() => {
40
+ if (!mouseEventsEnabled) {
41
+ return;
42
+ }
43
+ let mouseBuffer = '';
44
+ const broadcast = (event) => {
45
+ let handled = false;
46
+ for (const handler of subscribers) {
47
+ if (handler(event) === true) {
48
+ handled = true;
49
+ }
50
+ }
51
+ if (!handled &&
52
+ event.name === 'move' &&
53
+ event.col >= 0 &&
54
+ event.row >= 0 &&
55
+ event.button === 'left') {
56
+ // Terminal apps only receive mouse move events when the mouse is down
57
+ // so this always indicates a mouse drag that the user was expecting
58
+ // would trigger text selection but does not as we are handling mouse
59
+ // events not the terminal.
60
+ appEvents.emit(AppEvent.SelectionWarning);
61
+ }
62
+ };
63
+ const handleData = (data) => {
64
+ mouseBuffer += typeof data === 'string' ? data : data.toString('utf-8');
65
+ // Safety cap to prevent infinite buffer growth on garbage
66
+ if (mouseBuffer.length > MAX_MOUSE_BUFFER_SIZE) {
67
+ mouseBuffer = mouseBuffer.slice(-MAX_MOUSE_BUFFER_SIZE);
68
+ }
69
+ while (mouseBuffer.length > 0) {
70
+ const parsed = parseMouseEvent(mouseBuffer);
71
+ if (parsed) {
72
+ broadcast(parsed.event);
73
+ mouseBuffer = mouseBuffer.slice(parsed.length);
74
+ continue;
75
+ }
76
+ if (isIncompleteMouseSequence(mouseBuffer)) {
77
+ break; // Wait for more data
78
+ }
79
+ // Not a valid sequence at start, and not waiting for more data.
80
+ // Discard garbage until next possible sequence start.
81
+ const nextEsc = mouseBuffer.indexOf(ESC, 1);
82
+ if (nextEsc !== -1) {
83
+ mouseBuffer = mouseBuffer.slice(nextEsc);
84
+ // Loop continues to try parsing at new location
85
+ }
86
+ else {
87
+ mouseBuffer = '';
88
+ break;
89
+ }
90
+ }
91
+ };
92
+ stdin.on('data', handleData);
93
+ return () => {
94
+ stdin.removeListener('data', handleData);
95
+ };
96
+ }, [stdin, mouseEventsEnabled, subscribers, debugKeystrokeLogging]);
97
+ return (React.createElement(MouseContext.Provider, { value: { subscribe, unsubscribe } }, children));
98
+ }