@aitty/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ //#region src/frontend/terminal-input-policies.d.ts
2
+ type TerminalInputHost = {
3
+ bridge?: {
4
+ bracketedPaste?: () => boolean;
5
+ cursorKeysApp?: () => boolean;
6
+ } | null;
7
+ element?: HTMLElement | null;
8
+ input?: {
9
+ textarea?: HTMLTextAreaElement | null;
10
+ } | null;
11
+ } | null | undefined;
12
+ type TerminalKeyResolver = (event: KeyboardEvent) => string | null | undefined;
13
+ type TerminalInputCallbacks = {
14
+ captureDocumentCtrlN?: boolean;
15
+ onData?: (data: string) => void;
16
+ resolveKey?: TerminalKeyResolver;
17
+ resolveDocumentKeyData?: (event: KeyboardEvent) => string | null;
18
+ };
19
+ declare function installTerminalInputPolicies(term: TerminalInputHost, callbacks?: TerminalInputCallbacks): () => void;
20
+ declare function isBrowserSafeShortcut(event: KeyboardEvent): boolean;
21
+ declare function isContainedTerminalNavigationKey(event: KeyboardEvent): boolean;
22
+ declare function isCompositionKeyDown(event: KeyboardEvent): boolean;
23
+ //#endregion
24
+ export { TerminalInputCallbacks, TerminalKeyResolver, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
@@ -0,0 +1,462 @@
1
+ //#region src/frontend/terminal-input-policies.ts
2
+ const RESERVED_BROWSER_SHORTCUT_KEYS = new Set(["r", "w"]);
3
+ const CONTAINED_TERMINAL_NAVIGATION_KEYS = new Set([
4
+ "ArrowUp",
5
+ "ArrowDown",
6
+ "ArrowLeft",
7
+ "ArrowRight",
8
+ "Home",
9
+ "End",
10
+ "PageUp",
11
+ "PageDown"
12
+ ]);
13
+ const NORMAL_NAVIGATION_SEQUENCES = Object.freeze({
14
+ ArrowDown: "\x1B[B",
15
+ ArrowLeft: "\x1B[D",
16
+ ArrowRight: "\x1B[C",
17
+ ArrowUp: "\x1B[A",
18
+ End: "\x1B[F",
19
+ Home: "\x1B[H"
20
+ });
21
+ const APPLICATION_NAVIGATION_SEQUENCES = Object.freeze({
22
+ ArrowDown: "\x1BOB",
23
+ ArrowLeft: "\x1BOD",
24
+ ArrowRight: "\x1BOC",
25
+ ArrowUp: "\x1BOA",
26
+ End: "\x1BOF",
27
+ Home: "\x1BOH"
28
+ });
29
+ const FIXED_KEY_SEQUENCES = Object.freeze({
30
+ Backspace: "",
31
+ Delete: "\x1B[3~",
32
+ Enter: "\r",
33
+ Escape: "\x1B",
34
+ F1: "\x1BOP",
35
+ F10: "\x1B[21~",
36
+ F11: "\x1B[23~",
37
+ F12: "\x1B[24~",
38
+ F2: "\x1BOQ",
39
+ F3: "\x1BOR",
40
+ F4: "\x1BOS",
41
+ F5: "\x1B[15~",
42
+ F6: "\x1B[17~",
43
+ F7: "\x1B[18~",
44
+ F8: "\x1B[19~",
45
+ F9: "\x1B[20~",
46
+ Insert: "\x1B[2~",
47
+ PageDown: "\x1B[6~",
48
+ PageUp: "\x1B[5~",
49
+ Tab: " "
50
+ });
51
+ const ALT_PRINTABLE_CODE_MAP = Object.freeze({
52
+ Backquote: "`",
53
+ BracketLeft: "[",
54
+ BracketRight: "]",
55
+ Backslash: "\\",
56
+ Comma: ",",
57
+ Digit0: "0",
58
+ Digit1: "1",
59
+ Digit2: "2",
60
+ Digit3: "3",
61
+ Digit4: "4",
62
+ Digit5: "5",
63
+ Digit6: "6",
64
+ Digit7: "7",
65
+ Digit8: "8",
66
+ Digit9: "9",
67
+ Equal: "=",
68
+ KeyA: "a",
69
+ KeyB: "b",
70
+ KeyC: "c",
71
+ KeyD: "d",
72
+ KeyE: "e",
73
+ KeyF: "f",
74
+ KeyG: "g",
75
+ KeyH: "h",
76
+ KeyI: "i",
77
+ KeyJ: "j",
78
+ KeyK: "k",
79
+ KeyL: "l",
80
+ KeyM: "m",
81
+ KeyN: "n",
82
+ KeyO: "o",
83
+ KeyP: "p",
84
+ KeyQ: "q",
85
+ KeyR: "r",
86
+ KeyS: "s",
87
+ KeyT: "t",
88
+ KeyU: "u",
89
+ KeyV: "v",
90
+ KeyW: "w",
91
+ KeyX: "x",
92
+ KeyY: "y",
93
+ KeyZ: "z",
94
+ Minus: "-",
95
+ Period: ".",
96
+ Quote: "'",
97
+ Semicolon: ";",
98
+ Slash: "/",
99
+ Space: " "
100
+ });
101
+ function installTerminalInputPolicies(term, callbacks = {}) {
102
+ const root = term?.element;
103
+ const textarea = term?.input?.textarea;
104
+ const ownerDocument = root?.ownerDocument;
105
+ const onData = callbacks.onData ?? (() => {});
106
+ const captureDocumentCtrlN = callbacks.captureDocumentCtrlN === true;
107
+ const resolveKey = callbacks.resolveKey ?? (() => null);
108
+ const resolveDocumentKeyData = callbacks.resolveDocumentKeyData ?? (() => null);
109
+ if (!(root instanceof HTMLElement) || !(textarea instanceof HTMLTextAreaElement) || !(ownerDocument instanceof Document)) return () => {};
110
+ let composing = false;
111
+ let pendingCompositionCommit = false;
112
+ let suppressedCompositionValue = "";
113
+ let latestCompositionValue = "";
114
+ let speculativePlainInputText = "";
115
+ let pendingSyntheticPrintableText = "";
116
+ let syntheticPrintableFlushQueued = false;
117
+ const clearSpeculativePlainInputText = () => {
118
+ speculativePlainInputText = "";
119
+ };
120
+ const clearPendingSyntheticPrintableText = () => {
121
+ pendingSyntheticPrintableText = "";
122
+ };
123
+ const flushPendingSyntheticPrintableText = () => {
124
+ if (!pendingSyntheticPrintableText) return;
125
+ const pendingText = pendingSyntheticPrintableText;
126
+ pendingSyntheticPrintableText = "";
127
+ onData(pendingText);
128
+ };
129
+ const clearPendingTextState = () => {
130
+ clearSpeculativePlainInputText();
131
+ clearPendingSyntheticPrintableText();
132
+ };
133
+ const flushPendingTextState = () => {
134
+ clearSpeculativePlainInputText();
135
+ flushPendingSyntheticPrintableText();
136
+ };
137
+ const deferSyntheticPrintableText = (text) => {
138
+ pendingSyntheticPrintableText += text;
139
+ if (syntheticPrintableFlushQueued) return;
140
+ syntheticPrintableFlushQueued = true;
141
+ queueMicrotask(() => {
142
+ syntheticPrintableFlushQueued = false;
143
+ if (composing || !pendingSyntheticPrintableText) return;
144
+ const pendingText = pendingSyntheticPrintableText;
145
+ pendingSyntheticPrintableText = "";
146
+ onData(pendingText);
147
+ });
148
+ };
149
+ const rollBackSpeculativePlainInputText = (preeditText) => {
150
+ const rollbackText = resolveSpeculativeRollbackText(speculativePlainInputText, preeditText);
151
+ if (!rollbackText) return;
152
+ const rollback = "".repeat([...rollbackText].length);
153
+ speculativePlainInputText = speculativePlainInputText.slice(0, -rollbackText.length);
154
+ onData(rollback);
155
+ };
156
+ const sendText = (text) => {
157
+ if (!text) return;
158
+ onData(text);
159
+ };
160
+ const handleDocumentKeyDownCapture = (event) => {
161
+ if (event.target === textarea) return;
162
+ const documentKeyData = captureDocumentCtrlN && !event.altKey && !event.metaKey && event.ctrlKey && event.key.toLowerCase() === "n" ? "" : resolveDocumentKeyData(event);
163
+ if (documentKeyData === null) return;
164
+ flushPendingTextState();
165
+ event.preventDefault();
166
+ stopInputPropagation(event);
167
+ onData(documentKeyData);
168
+ };
169
+ const handleKeyDownCapture = (event) => {
170
+ if (isBrowserSafeShortcut(event)) {
171
+ flushPendingTextState();
172
+ stopInputPropagation(event);
173
+ return;
174
+ }
175
+ if (isCopyShortcutWithTerminalSelection(root, event)) {
176
+ flushPendingTextState();
177
+ stopInputPropagation(event);
178
+ return;
179
+ }
180
+ const customKeyData = resolveKey(event);
181
+ if (customKeyData != null) {
182
+ flushPendingTextState();
183
+ event.preventDefault();
184
+ stopInputPropagation(event);
185
+ onData(customKeyData);
186
+ return;
187
+ }
188
+ const normalizedAltSequence = resolveAltPrintableSequence(event);
189
+ if (normalizedAltSequence) {
190
+ flushPendingTextState();
191
+ event.preventDefault();
192
+ stopInputPropagation(event);
193
+ onData(normalizedAltSequence);
194
+ return;
195
+ }
196
+ if (composing || isCompositionKeyDown(event)) {
197
+ stopInputPropagation(event);
198
+ return;
199
+ }
200
+ if (isPlainPrintableKey(event)) {
201
+ pendingCompositionCommit = false;
202
+ suppressedCompositionValue = "";
203
+ latestCompositionValue = "";
204
+ stopInputPropagation(event);
205
+ if (!event.isTrusted) {
206
+ event.preventDefault();
207
+ deferSyntheticPrintableText(event.key);
208
+ }
209
+ return;
210
+ }
211
+ const terminalSequence = resolveTerminalKeySequence(term, event);
212
+ if (terminalSequence !== null) {
213
+ flushPendingTextState();
214
+ event.preventDefault();
215
+ stopInputPropagation(event);
216
+ onData(terminalSequence);
217
+ return;
218
+ }
219
+ if (isContainedTerminalNavigationKey(event)) {
220
+ flushPendingTextState();
221
+ event.preventDefault();
222
+ stopInputPropagation(event);
223
+ }
224
+ };
225
+ const handleCompositionStartCapture = (event) => {
226
+ clearPendingSyntheticPrintableText();
227
+ composing = true;
228
+ pendingCompositionCommit = false;
229
+ suppressedCompositionValue = "";
230
+ latestCompositionValue = "";
231
+ stopInputPropagation(event);
232
+ };
233
+ const handleCompositionEndCapture = (event) => {
234
+ clearPendingTextState();
235
+ composing = false;
236
+ stopInputPropagation(event);
237
+ pendingCompositionCommit = false;
238
+ const value = normalizeCompositionCommit(event.data);
239
+ if (!value) {
240
+ const fallbackValue = textarea.value;
241
+ if (latestCompositionValue && fallbackValue && fallbackValue !== latestCompositionValue) {
242
+ latestCompositionValue = "";
243
+ suppressedCompositionValue = fallbackValue;
244
+ sendText(fallbackValue);
245
+ textarea.value = "";
246
+ return;
247
+ }
248
+ pendingCompositionCommit = true;
249
+ return;
250
+ }
251
+ latestCompositionValue = "";
252
+ suppressedCompositionValue = value;
253
+ sendText(value);
254
+ textarea.value = "";
255
+ };
256
+ const handleInputCapture = (event) => {
257
+ if (composing || isComposingInput(event)) {
258
+ if (isCompositionPreeditInput(event)) {
259
+ clearPendingSyntheticPrintableText();
260
+ rollBackSpeculativePlainInputText(textarea.value);
261
+ } else clearPendingTextState();
262
+ latestCompositionValue = textarea.value;
263
+ stopInputPropagation(event);
264
+ return;
265
+ }
266
+ const value = textarea.value;
267
+ const compositionInput = isCompositionInput(event);
268
+ const compositionPreeditInput = isCompositionPreeditInput(event);
269
+ if (pendingCompositionCommit) {
270
+ clearPendingTextState();
271
+ stopInputPropagation(event);
272
+ if (!value) {
273
+ textarea.value = "";
274
+ if (!compositionInput) {
275
+ pendingCompositionCommit = false;
276
+ latestCompositionValue = "";
277
+ }
278
+ return;
279
+ }
280
+ if (compositionPreeditInput && value === latestCompositionValue) {
281
+ textarea.value = "";
282
+ return;
283
+ }
284
+ pendingCompositionCommit = false;
285
+ latestCompositionValue = "";
286
+ suppressedCompositionValue = resolveCommittedInputText(event, value);
287
+ sendText(suppressedCompositionValue);
288
+ textarea.value = "";
289
+ return;
290
+ }
291
+ if (compositionPreeditInput) {
292
+ clearPendingSyntheticPrintableText();
293
+ latestCompositionValue = value;
294
+ stopInputPropagation(event);
295
+ return;
296
+ }
297
+ if (suppressedCompositionValue) {
298
+ if (!value || value === suppressedCompositionValue) {
299
+ suppressedCompositionValue = "";
300
+ stopInputPropagation(event);
301
+ textarea.value = "";
302
+ return;
303
+ }
304
+ suppressedCompositionValue = "";
305
+ }
306
+ if (!value) return;
307
+ latestCompositionValue = "";
308
+ stopInputPropagation(event);
309
+ textarea.value = "";
310
+ if (compositionInput) {
311
+ const committedText = resolveCommittedInputText(event, value);
312
+ suppressedCompositionValue = committedText;
313
+ sendText(committedText);
314
+ return;
315
+ }
316
+ const inputText = resolvePlainInputText(event, value);
317
+ if (isPotentialImeSeedText(inputText)) speculativePlainInputText += inputText;
318
+ else clearSpeculativePlainInputText();
319
+ sendText(inputText);
320
+ };
321
+ const handleCopy = (event) => {
322
+ const selectionText = getSelectionText(root);
323
+ if (!selectionText || !event.clipboardData?.setData) return;
324
+ event.clipboardData.setData("text/plain", selectionText);
325
+ event.preventDefault();
326
+ };
327
+ const handlePaste = (event) => {
328
+ flushPendingTextState();
329
+ const text = event.clipboardData?.getData("text") ?? event.clipboardData?.getData("text/plain") ?? "";
330
+ if (!text) return;
331
+ event.preventDefault();
332
+ stopInputPropagation(event);
333
+ if (term?.bridge?.bracketedPaste?.() === true) {
334
+ onData(`\u001b[200~${text.split("\x1B").join("")}\u001b[201~`);
335
+ return;
336
+ }
337
+ onData(text);
338
+ };
339
+ ownerDocument.addEventListener("keydown", handleDocumentKeyDownCapture, true);
340
+ textarea.addEventListener("keydown", handleKeyDownCapture, true);
341
+ textarea.addEventListener("compositionstart", handleCompositionStartCapture, true);
342
+ textarea.addEventListener("compositionend", handleCompositionEndCapture, true);
343
+ textarea.addEventListener("input", handleInputCapture, true);
344
+ textarea.addEventListener("paste", handlePaste, true);
345
+ ownerDocument.addEventListener("copy", handleCopy, true);
346
+ return () => {
347
+ ownerDocument.removeEventListener("keydown", handleDocumentKeyDownCapture, true);
348
+ textarea.removeEventListener("keydown", handleKeyDownCapture, true);
349
+ textarea.removeEventListener("compositionstart", handleCompositionStartCapture, true);
350
+ textarea.removeEventListener("compositionend", handleCompositionEndCapture, true);
351
+ textarea.removeEventListener("input", handleInputCapture, true);
352
+ textarea.removeEventListener("paste", handlePaste, true);
353
+ ownerDocument.removeEventListener("copy", handleCopy, true);
354
+ };
355
+ }
356
+ function isBrowserSafeShortcut(event) {
357
+ if (event.altKey) return false;
358
+ if (!(event.metaKey && !event.ctrlKey || event.ctrlKey && !event.metaKey)) return false;
359
+ return RESERVED_BROWSER_SHORTCUT_KEYS.has(event.key.toLowerCase());
360
+ }
361
+ function isContainedTerminalNavigationKey(event) {
362
+ if (!CONTAINED_TERMINAL_NAVIGATION_KEYS.has(event.key)) return false;
363
+ if (event.altKey || event.ctrlKey || event.metaKey) return false;
364
+ return !(event.shiftKey && (event.key === "PageUp" || event.key === "PageDown"));
365
+ }
366
+ function resolveAltPrintableSequence(event) {
367
+ if (!event.altKey || event.ctrlKey || event.metaKey) return null;
368
+ const baseKey = ALT_PRINTABLE_CODE_MAP[event.code];
369
+ if (!baseKey) return null;
370
+ return `\u001b${baseKey}`;
371
+ }
372
+ function isPlainPrintableKey(event) {
373
+ if (event.altKey || event.ctrlKey || event.metaKey) return false;
374
+ return event.key.length === 1;
375
+ }
376
+ function resolveTerminalKeySequence(term, event) {
377
+ if (event.metaKey && !event.ctrlKey) {
378
+ if (event.key === "Backspace") return "";
379
+ return null;
380
+ }
381
+ if (event.ctrlKey) {
382
+ const controlSequence = resolveControlKeySequence(event);
383
+ return event.altKey && controlSequence ? `\u001b${controlSequence}` : controlSequence;
384
+ }
385
+ if (event.key === "Enter" && event.shiftKey) return "\x1B[13;2u";
386
+ if (event.key === "Tab" && event.shiftKey) return "\x1B[Z";
387
+ const fixedSequence = FIXED_KEY_SEQUENCES[event.key];
388
+ if (fixedSequence) return event.altKey ? `\u001b${fixedSequence}` : fixedSequence;
389
+ const navigationSequence = (term?.bridge?.cursorKeysApp?.() === true ? APPLICATION_NAVIGATION_SEQUENCES : NORMAL_NAVIGATION_SEQUENCES)[event.key];
390
+ if (navigationSequence) return event.altKey ? `\u001b${navigationSequence}` : navigationSequence;
391
+ return null;
392
+ }
393
+ function resolveControlKeySequence(event) {
394
+ if (event.key.length !== 1) return null;
395
+ const key = event.key.toLowerCase();
396
+ if (key >= "a" && key <= "z") return String.fromCharCode(key.charCodeAt(0) - 96);
397
+ return {
398
+ " ": "\0",
399
+ "[": "\x1B",
400
+ "\\": "",
401
+ "]": "",
402
+ "^": "",
403
+ "_": ""
404
+ }[key] ?? null;
405
+ }
406
+ function isCompositionKeyDown(event) {
407
+ return event.isComposing || event.key === "Process" || event.key === "Dead" || event.keyCode === 229 || event.which === 229;
408
+ }
409
+ function isCompositionInput(event) {
410
+ return typeof event.inputType === "string" && event.inputType.toLowerCase().includes("composition");
411
+ }
412
+ function isCompositionPreeditInput(event) {
413
+ return event.inputType === "insertCompositionText";
414
+ }
415
+ function isComposingInput(event) {
416
+ return event.isComposing || isCompositionPreeditInput(event);
417
+ }
418
+ function resolvePlainInputText(event, textareaValue) {
419
+ if (typeof event.data === "string" && event.data.length > 0) return event.data;
420
+ if (event.inputType === "insertLineBreak") return "\r";
421
+ if (event.inputType === "deleteContentBackward") return "";
422
+ if (event.inputType === "deleteContentForward") return "\x1B[3~";
423
+ return textareaValue;
424
+ }
425
+ function resolveCommittedInputText(event, textareaValue) {
426
+ if (typeof event.data === "string" && event.data.length > 0) return event.data;
427
+ return textareaValue;
428
+ }
429
+ function isPotentialImeSeedText(text) {
430
+ return /^[0-9A-Za-z']$/.test(text);
431
+ }
432
+ function resolveSpeculativeRollbackText(sentText, preeditText) {
433
+ if (!sentText || !preeditText || !preeditText.startsWith(sentText)) return "";
434
+ return sentText;
435
+ }
436
+ function normalizeCompositionCommit(data) {
437
+ if (typeof data === "string" && data.length > 0) return data;
438
+ return "";
439
+ }
440
+ function getSelectionText(root) {
441
+ const selection = root.ownerDocument?.getSelection?.();
442
+ if (!selection || selection.isCollapsed) return "";
443
+ if (!nodeIsInsideRoot(root, selection.anchorNode) || !nodeIsInsideRoot(root, selection.focusNode)) return "";
444
+ return selection.toString();
445
+ }
446
+ function isCopyShortcutWithTerminalSelection(root, event) {
447
+ if (event.altKey || event.key.toLowerCase() !== "c") return false;
448
+ if (!(event.metaKey || event.ctrlKey)) return false;
449
+ return getSelectionText(root).length > 0;
450
+ }
451
+ function nodeIsInsideRoot(root, node) {
452
+ if (!node) return false;
453
+ if (node === root) return true;
454
+ if (node instanceof Element) return root.contains(node);
455
+ return node.parentElement ? root.contains(node.parentElement) : false;
456
+ }
457
+ function stopInputPropagation(event) {
458
+ event.stopPropagation();
459
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
460
+ }
461
+ //#endregion
462
+ export { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
@@ -0,0 +1,22 @@
1
+ //#region src/frontend/terminal-theme-protocol.d.ts
2
+ type TerminalThemeName = "dark" | "light";
3
+ type TerminalThemeProtocolUpdate = {
4
+ colors: Partial<Record<TerminalThemeColorSlot, string>>;
5
+ palette: TerminalThemePaletteUpdate[];
6
+ theme?: TerminalThemeName;
7
+ };
8
+ type TerminalThemeColorSlot = "background" | "cursor" | "foreground";
9
+ type TerminalThemePaletteUpdate = {
10
+ color: string;
11
+ index: number;
12
+ };
13
+ declare function createTerminalThemeProtocolParser(): {
14
+ append(chunk: Uint8Array): TerminalThemeProtocolUpdate | null;
15
+ reset(): void;
16
+ };
17
+ declare function resolveTerminalThemeQueryResponse(chunk: string | Uint8Array, options: {
18
+ root: HTMLElement;
19
+ theme?: string;
20
+ }): string | null;
21
+ //#endregion
22
+ export { TerminalThemeColorSlot, TerminalThemeName, TerminalThemePaletteUpdate, TerminalThemeProtocolUpdate, createTerminalThemeProtocolParser, resolveTerminalThemeQueryResponse };