@aitty/browser 0.1.2 → 0.2.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,62 @@
1
+ import { TerminalKeyResolver } from "./terminal-input-policies.js";
2
+ import { AittyTheme } from "@aitty/protocol";
3
+
4
+ //#region packages/browser/src/frontend/terminal-config.d.ts
5
+ type TerminalTheme = AittyTheme;
6
+ type TerminalThemeTarget = "container" | "document";
7
+ type TerminalFontSizeRange = {
8
+ max?: number;
9
+ min?: number;
10
+ step?: number;
11
+ };
12
+ type TerminalAppearanceConfig = {
13
+ cssVars?: Record<string, string | null | undefined>;
14
+ fontSize?: number;
15
+ fontSizeRange?: TerminalFontSizeRange;
16
+ lineHeight?: number;
17
+ theme?: TerminalTheme;
18
+ themeTarget?: TerminalThemeTarget;
19
+ };
20
+ type TerminalBehaviorConfig = {
21
+ input?: {
22
+ resolveKey?: TerminalKeyResolver;
23
+ };
24
+ scrollbackLimit?: number;
25
+ };
26
+ type TerminalConfig = {
27
+ appearance?: TerminalAppearanceConfig;
28
+ behavior?: TerminalBehaviorConfig;
29
+ };
30
+ type TerminalConfigPatch = {
31
+ appearance?: TerminalAppearanceConfig;
32
+ behavior?: TerminalBehaviorConfig;
33
+ };
34
+ type TerminalResolvedAppearanceConfig = {
35
+ cssVars: Record<string, string>;
36
+ fontSize: number;
37
+ fontSizeRange: Required<TerminalFontSizeRange>;
38
+ lineHeight?: number;
39
+ theme?: TerminalTheme;
40
+ themeTarget: TerminalThemeTarget;
41
+ };
42
+ type TerminalResolvedBehaviorConfig = {
43
+ input?: {
44
+ resolveKey?: TerminalKeyResolver;
45
+ };
46
+ scrollbackLimit?: number;
47
+ };
48
+ type TerminalResolvedConfig = {
49
+ appearance: TerminalResolvedAppearanceConfig;
50
+ behavior: TerminalResolvedBehaviorConfig;
51
+ };
52
+ type TerminalConfigListener = (config: TerminalResolvedConfig) => void;
53
+ declare function normalizeTerminalConfig(config: TerminalConfig | undefined): TerminalResolvedConfig;
54
+ declare function mergeTerminalConfig(current: TerminalResolvedConfig, patch: TerminalConfigPatch): TerminalResolvedConfig;
55
+ declare function cloneTerminalConfig(config: TerminalResolvedConfig): TerminalResolvedConfig;
56
+ declare function areTerminalAppearancesEqual(left: TerminalResolvedAppearanceConfig, right: TerminalResolvedAppearanceConfig): boolean;
57
+ declare function areTerminalBehaviorsEqual(left: TerminalResolvedBehaviorConfig, right: TerminalResolvedBehaviorConfig): boolean;
58
+ declare function hasTerminalLayoutAppearanceChange(previous: TerminalResolvedAppearanceConfig, next: TerminalResolvedAppearanceConfig): boolean;
59
+ declare function normalizeThemeName(theme: TerminalTheme | undefined): string | undefined;
60
+ declare function normalizeThemeTarget(value: TerminalThemeTarget | string | undefined): TerminalThemeTarget;
61
+ //#endregion
62
+ export { TerminalAppearanceConfig, TerminalBehaviorConfig, TerminalConfig, TerminalConfigListener, TerminalConfigPatch, TerminalFontSizeRange, TerminalResolvedAppearanceConfig, TerminalResolvedBehaviorConfig, TerminalResolvedConfig, TerminalTheme, TerminalThemeTarget, areTerminalAppearancesEqual, areTerminalBehaviorsEqual, cloneTerminalConfig, hasTerminalLayoutAppearanceChange, mergeTerminalConfig, normalizeTerminalConfig, normalizeThemeName, normalizeThemeTarget };
@@ -0,0 +1,126 @@
1
+ import { normalizeTheme } from "@aitty/protocol";
2
+ //#region packages/browser/src/frontend/terminal-config.ts
3
+ const DEFAULT_TERMINAL_FONT_SIZE = 15;
4
+ const DEFAULT_TERMINAL_FONT_SIZE_MAX = 24;
5
+ const DEFAULT_TERMINAL_FONT_SIZE_MIN = 11;
6
+ const DEFAULT_TERMINAL_FONT_SIZE_STEP = 1;
7
+ function normalizeTerminalConfig(config) {
8
+ const appearance = config?.appearance ?? {};
9
+ const fontSizeRange = normalizeTerminalFontSizeRange(appearance.fontSizeRange);
10
+ const fontSize = normalizeTerminalFontSize(appearance.fontSize, fontSizeRange);
11
+ return {
12
+ appearance: {
13
+ cssVars: normalizeTerminalCssVars(appearance.cssVars),
14
+ fontSize,
15
+ fontSizeRange,
16
+ lineHeight: normalizeTerminalLineHeight(appearance.lineHeight),
17
+ theme: normalizeThemeName(appearance.theme),
18
+ themeTarget: normalizeThemeTarget(appearance.themeTarget)
19
+ },
20
+ behavior: {
21
+ input: config?.behavior?.input,
22
+ scrollbackLimit: normalizeOptionalNonNegativeInteger(config?.behavior?.scrollbackLimit)
23
+ }
24
+ };
25
+ }
26
+ function mergeTerminalConfig(current, patch) {
27
+ const nextAppearancePatch = patch.appearance ?? {};
28
+ const fontSizeRange = normalizeTerminalFontSizeRange(nextAppearancePatch.fontSizeRange ?? current.appearance.fontSizeRange);
29
+ const fontSize = normalizeTerminalFontSize(nextAppearancePatch.fontSize, fontSizeRange, current.appearance.fontSize);
30
+ return {
31
+ appearance: {
32
+ cssVars: normalizeTerminalCssVars({
33
+ ...current.appearance.cssVars,
34
+ ...nextAppearancePatch.cssVars
35
+ }),
36
+ fontSize,
37
+ fontSizeRange,
38
+ lineHeight: "lineHeight" in nextAppearancePatch ? normalizeTerminalLineHeight(nextAppearancePatch.lineHeight) : current.appearance.lineHeight,
39
+ theme: "theme" in nextAppearancePatch ? normalizeThemeName(nextAppearancePatch.theme) : current.appearance.theme,
40
+ themeTarget: current.appearance.themeTarget
41
+ },
42
+ behavior: {
43
+ input: patch.behavior?.input ?? current.behavior.input,
44
+ scrollbackLimit: "scrollbackLimit" in (patch.behavior ?? {}) ? normalizeOptionalNonNegativeInteger(patch.behavior?.scrollbackLimit) : current.behavior.scrollbackLimit
45
+ }
46
+ };
47
+ }
48
+ function cloneTerminalConfig(config) {
49
+ return {
50
+ appearance: {
51
+ cssVars: { ...config.appearance.cssVars },
52
+ fontSize: config.appearance.fontSize,
53
+ fontSizeRange: { ...config.appearance.fontSizeRange },
54
+ lineHeight: config.appearance.lineHeight,
55
+ theme: config.appearance.theme,
56
+ themeTarget: config.appearance.themeTarget
57
+ },
58
+ behavior: {
59
+ input: config.behavior.input,
60
+ scrollbackLimit: config.behavior.scrollbackLimit
61
+ }
62
+ };
63
+ }
64
+ function areTerminalAppearancesEqual(left, right) {
65
+ return left.fontSize === right.fontSize && left.lineHeight === right.lineHeight && left.theme === right.theme && left.themeTarget === right.themeTarget && areFontSizeRangesEqual(left.fontSizeRange, right.fontSizeRange) && areCssVarsEqual(left.cssVars, right.cssVars);
66
+ }
67
+ function areTerminalBehaviorsEqual(left, right) {
68
+ return left.input === right.input && left.scrollbackLimit === right.scrollbackLimit;
69
+ }
70
+ function hasTerminalLayoutAppearanceChange(previous, next) {
71
+ return previous.fontSize !== next.fontSize || previous.lineHeight !== next.lineHeight;
72
+ }
73
+ function normalizeThemeName(theme) {
74
+ return normalizeTheme(theme);
75
+ }
76
+ function normalizeThemeTarget(value) {
77
+ return value === "document" ? "document" : "container";
78
+ }
79
+ function areFontSizeRangesEqual(left, right) {
80
+ return left.min === right.min && left.max === right.max && left.step === right.step;
81
+ }
82
+ function areCssVarsEqual(left, right) {
83
+ const leftEntries = Object.entries(left);
84
+ const rightKeys = new Set(Object.keys(right));
85
+ return leftEntries.length === rightKeys.size && leftEntries.every(([key, value]) => right[key] === value);
86
+ }
87
+ function normalizeTerminalCssVars(vars) {
88
+ const normalized = {};
89
+ for (const [name, value] of Object.entries(vars ?? {})) {
90
+ if (!name.startsWith("--") || value === null || value === void 0) continue;
91
+ normalized[name] = String(value);
92
+ }
93
+ return normalized;
94
+ }
95
+ function normalizeTerminalLineHeight(value) {
96
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
97
+ return Math.round(value * 100) / 100;
98
+ }
99
+ function normalizeOptionalNonNegativeInteger(value) {
100
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
101
+ return Math.max(0, Math.floor(value));
102
+ }
103
+ function normalizeTerminalFontSizeRange(range) {
104
+ const min = normalizeFiniteNumber(range?.min, DEFAULT_TERMINAL_FONT_SIZE_MIN);
105
+ const max = normalizeFiniteNumber(range?.max, DEFAULT_TERMINAL_FONT_SIZE_MAX);
106
+ const low = Math.max(1, Math.min(min, max));
107
+ const high = Math.max(low, Math.max(min, max));
108
+ const step = Math.max(.1, roundTerminalFontSize(normalizeFiniteNumber(range?.step, DEFAULT_TERMINAL_FONT_SIZE_STEP)));
109
+ return {
110
+ max: roundTerminalFontSize(high),
111
+ min: roundTerminalFontSize(low),
112
+ step
113
+ };
114
+ }
115
+ function normalizeTerminalFontSize(value, range, fallback = DEFAULT_TERMINAL_FONT_SIZE) {
116
+ const base = normalizeFiniteNumber(value, fallback);
117
+ return roundTerminalFontSize(Math.min(range.max, Math.max(range.min, base)));
118
+ }
119
+ function normalizeFiniteNumber(value, fallback) {
120
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
121
+ }
122
+ function roundTerminalFontSize(value) {
123
+ return Math.round(value * 10) / 10;
124
+ }
125
+ //#endregion
126
+ export { areTerminalAppearancesEqual, areTerminalBehaviorsEqual, cloneTerminalConfig, hasTerminalLayoutAppearanceChange, mergeTerminalConfig, normalizeTerminalConfig, normalizeThemeName, normalizeThemeTarget };
@@ -1,4 +1,4 @@
1
- //#region src/frontend/terminal-input-policies.d.ts
1
+ //#region packages/browser/src/frontend/terminal-input-policies.d.ts
2
2
  type TerminalInputHost = {
3
3
  bridge?: {
4
4
  bracketedPaste?: () => boolean;
@@ -10,15 +10,25 @@ type TerminalInputHost = {
10
10
  } | null;
11
11
  } | null | undefined;
12
12
  type TerminalKeyResolver = (event: KeyboardEvent) => string | null | undefined;
13
+ type TerminalPasteShortcut = "cmd-v" | "ctrl-v";
14
+ type TerminalPasteShortcutResult = string | {
15
+ allowTerminalFallback?: boolean;
16
+ data?: string | null;
17
+ } | null | undefined;
13
18
  type TerminalInputCallbacks = {
14
19
  captureDocumentCtrlN?: boolean;
15
20
  onData?: (data: string) => void;
21
+ onPasteShortcut?: (event?: ClipboardEvent) => Promise<TerminalPasteShortcutResult> | TerminalPasteShortcutResult;
22
+ pasteShortcut?: TerminalPasteShortcut;
16
23
  resolveKey?: TerminalKeyResolver;
17
24
  resolveDocumentKeyData?: (event: KeyboardEvent) => string | null;
18
25
  };
19
26
  declare function installTerminalInputPolicies(term: TerminalInputHost, callbacks?: TerminalInputCallbacks): () => void;
20
27
  declare function isBrowserSafeShortcut(event: KeyboardEvent): boolean;
21
28
  declare function isContainedTerminalNavigationKey(event: KeyboardEvent): boolean;
29
+ declare function resolveTerminalDocumentKeyData(term: TerminalInputHost, event: KeyboardEvent): string | null;
22
30
  declare function isCompositionKeyDown(event: KeyboardEvent): boolean;
31
+ declare function resolvePlainInputText(event: InputEvent, textareaValue: string): string;
32
+ declare function resolveCommittedInputText(event: InputEvent, textareaValue: string): string;
23
33
  //#endregion
24
- export { TerminalInputCallbacks, TerminalKeyResolver, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
34
+ export { TerminalInputCallbacks, TerminalKeyResolver, TerminalPasteShortcut, TerminalPasteShortcutResult, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, resolveCommittedInputText, resolvePlainInputText, resolveTerminalDocumentKeyData };
@@ -1,4 +1,5 @@
1
- //#region src/frontend/terminal-input-policies.ts
1
+ import { getTerminalSelectionText, hasTerminalSelection } from "@aitty/wterm-dom";
2
+ //#region packages/browser/src/frontend/terminal-input-policies.ts
2
3
  const RESERVED_BROWSER_SHORTCUT_KEYS = new Set(["r", "w"]);
3
4
  const CONTAINED_TERMINAL_NAVIGATION_KEYS = new Set([
4
5
  "ArrowUp",
@@ -98,11 +99,17 @@ const ALT_PRINTABLE_CODE_MAP = Object.freeze({
98
99
  Slash: "/",
99
100
  Space: " "
100
101
  });
102
+ const PASTE_SHORTCUT_PENDING_MS = 15e3;
103
+ const PASTE_SHORTCUT_FALLBACK_MS = 0;
104
+ const CTRL_V_SEQUENCE = "";
105
+ const DEFAULT_PASTE_SHORTCUT = "ctrl-v";
101
106
  function installTerminalInputPolicies(term, callbacks = {}) {
102
107
  const root = term?.element;
103
108
  const textarea = term?.input?.textarea;
104
109
  const ownerDocument = root?.ownerDocument;
105
110
  const onData = callbacks.onData ?? (() => {});
111
+ const onPasteShortcut = callbacks.onPasteShortcut;
112
+ const pasteShortcut = callbacks.pasteShortcut ?? DEFAULT_PASTE_SHORTCUT;
106
113
  const captureDocumentCtrlN = callbacks.captureDocumentCtrlN === true;
107
114
  const resolveKey = callbacks.resolveKey ?? (() => null);
108
115
  const resolveDocumentKeyData = callbacks.resolveDocumentKeyData ?? (() => null);
@@ -111,11 +118,58 @@ function installTerminalInputPolicies(term, callbacks = {}) {
111
118
  let pendingCompositionCommit = false;
112
119
  let suppressedCompositionValue = "";
113
120
  let latestCompositionValue = "";
114
- let speculativePlainInputText = "";
115
121
  let pendingSyntheticPrintableText = "";
116
122
  let syntheticPrintableFlushQueued = false;
117
- const clearSpeculativePlainInputText = () => {
118
- speculativePlainInputText = "";
123
+ let pendingPasteShortcut = false;
124
+ let pendingPasteShortcutClearTimer = null;
125
+ let pendingPasteShortcutFallbackTimer = null;
126
+ let pasteShortcutGeneration = 0;
127
+ let consumedPasteShortcutGeneration = 0;
128
+ const schedulePasteShortcutFallback = (generation) => {
129
+ if (pendingPasteShortcutFallbackTimer !== null) ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutFallbackTimer);
130
+ pendingPasteShortcutFallbackTimer = ownerDocument.defaultView?.setTimeout(() => {
131
+ pendingPasteShortcutFallbackTimer = null;
132
+ if (pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation) {
133
+ consumedPasteShortcutGeneration = generation;
134
+ clearPendingPasteShortcut();
135
+ onData(CTRL_V_SEQUENCE);
136
+ }
137
+ }, PASTE_SHORTCUT_FALLBACK_MS) ?? null;
138
+ };
139
+ const runPasteShortcut = (generation, event) => {
140
+ if (!onPasteShortcut) return;
141
+ Promise.resolve(onPasteShortcut(event)).then((result) => {
142
+ const data = resolvePasteShortcutData(result);
143
+ if (typeof data === "string" && data.length > 0 && pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation) {
144
+ consumedPasteShortcutGeneration = generation;
145
+ clearPendingPasteShortcut();
146
+ sendPasteText(data);
147
+ return;
148
+ }
149
+ if (pasteShortcut === "ctrl-v" && shouldAllowPasteShortcutFallback(result) && pendingPasteShortcut && pasteShortcutGeneration === generation && consumedPasteShortcutGeneration !== generation && event === void 0) schedulePasteShortcutFallback(generation);
150
+ }).catch(() => void 0);
151
+ };
152
+ const clearPendingPasteShortcut = () => {
153
+ pendingPasteShortcut = false;
154
+ if (pendingPasteShortcutClearTimer !== null) {
155
+ ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutClearTimer);
156
+ pendingPasteShortcutClearTimer = null;
157
+ }
158
+ if (pendingPasteShortcutFallbackTimer !== null) {
159
+ ownerDocument.defaultView?.clearTimeout(pendingPasteShortcutFallbackTimer);
160
+ pendingPasteShortcutFallbackTimer = null;
161
+ }
162
+ };
163
+ const markPendingPasteShortcut = () => {
164
+ clearPendingPasteShortcut();
165
+ pasteShortcutGeneration += 1;
166
+ pendingPasteShortcut = true;
167
+ const generation = pasteShortcutGeneration;
168
+ pendingPasteShortcutClearTimer = ownerDocument.defaultView?.setTimeout(() => {
169
+ if (pasteShortcutGeneration !== generation) return;
170
+ pendingPasteShortcut = false;
171
+ pendingPasteShortcutClearTimer = null;
172
+ }, PASTE_SHORTCUT_PENDING_MS) ?? null;
119
173
  };
120
174
  const clearPendingSyntheticPrintableText = () => {
121
175
  pendingSyntheticPrintableText = "";
@@ -127,11 +181,9 @@ function installTerminalInputPolicies(term, callbacks = {}) {
127
181
  onData(pendingText);
128
182
  };
129
183
  const clearPendingTextState = () => {
130
- clearSpeculativePlainInputText();
131
184
  clearPendingSyntheticPrintableText();
132
185
  };
133
186
  const flushPendingTextState = () => {
134
- clearSpeculativePlainInputText();
135
187
  flushPendingSyntheticPrintableText();
136
188
  };
137
189
  const deferSyntheticPrintableText = (text) => {
@@ -146,17 +198,18 @@ function installTerminalInputPolicies(term, callbacks = {}) {
146
198
  onData(pendingText);
147
199
  });
148
200
  };
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
201
  const sendText = (text) => {
157
202
  if (!text) return;
158
203
  onData(text);
159
204
  };
205
+ const sendPasteText = (text) => {
206
+ if (!text) return;
207
+ if (term?.bridge?.bracketedPaste?.() === true) {
208
+ onData(`\u001b[200~${text.split("\x1B").join("")}\u001b[201~`);
209
+ return;
210
+ }
211
+ onData(text);
212
+ };
160
213
  const handleDocumentKeyDownCapture = (event) => {
161
214
  if (event.target === textarea) return;
162
215
  const documentKeyData = captureDocumentCtrlN && !event.altKey && !event.metaKey && event.ctrlKey && event.key.toLowerCase() === "n" ? "" : resolveDocumentKeyData(event);
@@ -167,6 +220,7 @@ function installTerminalInputPolicies(term, callbacks = {}) {
167
220
  onData(documentKeyData);
168
221
  };
169
222
  const handleKeyDownCapture = (event) => {
223
+ if (!isTerminalSessionInteractive(root)) return;
170
224
  if (isBrowserSafeShortcut(event)) {
171
225
  flushPendingTextState();
172
226
  stopInputPropagation(event);
@@ -177,6 +231,14 @@ function installTerminalInputPolicies(term, callbacks = {}) {
177
231
  stopInputPropagation(event);
178
232
  return;
179
233
  }
234
+ if (isPasteShortcut(event, pasteShortcut) && onPasteShortcut) {
235
+ flushPendingTextState();
236
+ stopInputPropagation(event);
237
+ markPendingPasteShortcut();
238
+ runPasteShortcut(pasteShortcutGeneration);
239
+ return;
240
+ }
241
+ clearPendingPasteShortcut();
180
242
  const customKeyData = resolveKey(event);
181
243
  if (customKeyData != null) {
182
244
  flushPendingTextState();
@@ -223,7 +285,7 @@ function installTerminalInputPolicies(term, callbacks = {}) {
223
285
  }
224
286
  };
225
287
  const handleCompositionStartCapture = (event) => {
226
- clearPendingSyntheticPrintableText();
288
+ clearPendingTextState();
227
289
  composing = true;
228
290
  pendingCompositionCommit = false;
229
291
  suppressedCompositionValue = "";
@@ -255,10 +317,8 @@ function installTerminalInputPolicies(term, callbacks = {}) {
255
317
  };
256
318
  const handleInputCapture = (event) => {
257
319
  if (composing || isComposingInput(event)) {
258
- if (isCompositionPreeditInput(event)) {
259
- clearPendingSyntheticPrintableText();
260
- rollBackSpeculativePlainInputText(textarea.value);
261
- } else clearPendingTextState();
320
+ if (isCompositionPreeditInput(event)) clearPendingTextState();
321
+ else clearPendingTextState();
262
322
  latestCompositionValue = textarea.value;
263
323
  stopInputPropagation(event);
264
324
  return;
@@ -313,28 +373,37 @@ function installTerminalInputPolicies(term, callbacks = {}) {
313
373
  sendText(committedText);
314
374
  return;
315
375
  }
316
- const inputText = resolvePlainInputText(event, value);
317
- if (isPotentialImeSeedText(inputText)) speculativePlainInputText += inputText;
318
- else clearSpeculativePlainInputText();
319
- sendText(inputText);
376
+ sendText(resolvePlainInputText(event, value));
320
377
  };
321
378
  const handleCopy = (event) => {
322
- const selectionText = getSelectionText(root);
379
+ const selectionText = getTerminalSelectionText(root);
323
380
  if (!selectionText || !event.clipboardData?.setData) return;
324
381
  event.clipboardData.setData("text/plain", selectionText);
325
382
  event.preventDefault();
326
383
  };
327
384
  const handlePaste = (event) => {
328
385
  flushPendingTextState();
386
+ if (!isTerminalSessionInteractive(root)) return;
387
+ if (onPasteShortcut && clipboardEventHasImage(event)) {
388
+ if (!pendingPasteShortcut) markPendingPasteShortcut();
389
+ event.preventDefault();
390
+ stopInputPropagation(event);
391
+ runPasteShortcut(pasteShortcutGeneration, event);
392
+ return;
393
+ }
329
394
  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~`);
395
+ if (!text) {
396
+ if (pendingPasteShortcut && onPasteShortcut) {
397
+ event.preventDefault();
398
+ stopInputPropagation(event);
399
+ runPasteShortcut(pasteShortcutGeneration, event);
400
+ }
335
401
  return;
336
402
  }
337
- onData(text);
403
+ clearPendingPasteShortcut();
404
+ event.preventDefault();
405
+ stopInputPropagation(event);
406
+ sendPasteText(text);
338
407
  };
339
408
  ownerDocument.addEventListener("keydown", handleDocumentKeyDownCapture, true);
340
409
  textarea.addEventListener("keydown", handleKeyDownCapture, true);
@@ -344,6 +413,8 @@ function installTerminalInputPolicies(term, callbacks = {}) {
344
413
  textarea.addEventListener("paste", handlePaste, true);
345
414
  ownerDocument.addEventListener("copy", handleCopy, true);
346
415
  return () => {
416
+ clearPendingTextState();
417
+ clearPendingPasteShortcut();
347
418
  ownerDocument.removeEventListener("keydown", handleDocumentKeyDownCapture, true);
348
419
  textarea.removeEventListener("keydown", handleKeyDownCapture, true);
349
420
  textarea.removeEventListener("compositionstart", handleCompositionStartCapture, true);
@@ -363,6 +434,13 @@ function isContainedTerminalNavigationKey(event) {
363
434
  if (event.altKey || event.ctrlKey || event.metaKey) return false;
364
435
  return !(event.shiftKey && (event.key === "PageUp" || event.key === "PageDown"));
365
436
  }
437
+ function resolveTerminalDocumentKeyData(term, event) {
438
+ if (isBrowserSafeShortcut(event) || isCompositionKeyDown(event)) return null;
439
+ const normalizedAltSequence = resolveAltPrintableSequence(event);
440
+ if (normalizedAltSequence) return normalizedAltSequence;
441
+ if (isPlainPrintableKey(event)) return event.key;
442
+ return resolveTerminalKeySequence(term, event);
443
+ }
366
444
  function resolveAltPrintableSequence(event) {
367
445
  if (!event.altKey || event.ctrlKey || event.metaKey) return null;
368
446
  const baseKey = ALT_PRINTABLE_CODE_MAP[event.code];
@@ -426,37 +504,41 @@ function resolveCommittedInputText(event, textareaValue) {
426
504
  if (typeof event.data === "string" && event.data.length > 0) return event.data;
427
505
  return textareaValue;
428
506
  }
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
507
  function normalizeCompositionCommit(data) {
437
508
  if (typeof data === "string" && data.length > 0) return data;
438
509
  return "";
439
510
  }
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
511
  function isCopyShortcutWithTerminalSelection(root, event) {
447
512
  if (event.altKey || event.key.toLowerCase() !== "c") return false;
448
513
  if (!(event.metaKey || event.ctrlKey)) return false;
449
- return getSelectionText(root).length > 0;
514
+ return hasTerminalSelection(root);
515
+ }
516
+ function isPasteShortcut(event, shortcut) {
517
+ if (event.altKey || event.key.toLowerCase() !== "v") return false;
518
+ return shortcut === "cmd-v" ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
519
+ }
520
+ function clipboardEventHasImage(event) {
521
+ const clipboardData = event.clipboardData;
522
+ if (!clipboardData) return false;
523
+ for (const item of Array.from(clipboardData.items)) if (item.kind === "file" && item.type.startsWith("image/")) return true;
524
+ for (const file of Array.from(clipboardData.files)) if (file.type.startsWith("image/")) return true;
525
+ return false;
526
+ }
527
+ function resolvePasteShortcutData(result) {
528
+ if (typeof result === "string") return result;
529
+ if (!result || typeof result !== "object") return null;
530
+ return typeof result.data === "string" ? result.data : null;
531
+ }
532
+ function shouldAllowPasteShortcutFallback(result) {
533
+ if (!result || typeof result === "string") return true;
534
+ return result.allowTerminalFallback !== false;
450
535
  }
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;
536
+ function isTerminalSessionInteractive(root) {
537
+ return root.dataset?.sessionInteractive !== "false";
456
538
  }
457
539
  function stopInputPropagation(event) {
458
540
  event.stopPropagation();
459
541
  if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
460
542
  }
461
543
  //#endregion
462
- export { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey };
544
+ export { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, resolveCommittedInputText, resolvePlainInputText, resolveTerminalDocumentKeyData };
@@ -0,0 +1,25 @@
1
+ //#region packages/browser/src/frontend/terminal-scroll-anchor.ts
2
+ function resolveScrollAnchorRestoreTop(options) {
3
+ const maxScrollTop = normalizeScrollMetric(options.maxScrollTop);
4
+ if (options.stickToBottom) return maxScrollTop;
5
+ return clamp(normalizeOptionalScrollMetric(options.inputAnchoredScrollTop) ?? normalizeOptionalScrollMetric(options.anchoredScrollTop) ?? resolveRatioFallbackScrollTop({
6
+ maxScrollTop,
7
+ ratio: options.ratio,
8
+ scrollTop: options.scrollTop
9
+ }), 0, maxScrollTop);
10
+ }
11
+ function resolveRatioFallbackScrollTop({ maxScrollTop, ratio, scrollTop }) {
12
+ if (maxScrollTop <= 0) return Math.min(normalizeScrollMetric(scrollTop), maxScrollTop);
13
+ return Math.round(maxScrollTop * clamp(Number.isFinite(ratio) ? ratio : 0, 0, 1));
14
+ }
15
+ function normalizeOptionalScrollMetric(value) {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
17
+ }
18
+ function normalizeScrollMetric(value) {
19
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
20
+ }
21
+ function clamp(value, min, max) {
22
+ return Math.min(Math.max(value, min), max);
23
+ }
24
+ //#endregion
25
+ export { resolveScrollAnchorRestoreTop };
@@ -0,0 +1,23 @@
1
+ function resolveScrollFollowPolicy(options) {
2
+ const tolerancePx = normalizeNonNegativeMetric(options.tolerancePx ?? 1);
3
+ const maxScrollTop = normalizeNonNegativeMetric(options.maxScrollTop);
4
+ const scrollTop = normalizeNonNegativeMetric(options.scrollTop);
5
+ const previousScrollTop = normalizeNonNegativeMetric(options.previousScrollTop);
6
+ if (!options.keepBottomFollow || isScrollAtBottom(scrollTop, maxScrollTop, tolerancePx)) return { action: "idle" };
7
+ if (scrollTop < previousScrollTop - tolerancePx) return { action: "stop-follow" };
8
+ if (options.programmaticScroll) return { action: "idle" };
9
+ return {
10
+ action: "snap-to-bottom",
11
+ passes: 3,
12
+ preferTranscriptFollow: true
13
+ };
14
+ }
15
+ function isScrollAtBottom(scrollTop, maxScrollTop, tolerancePx = 1) {
16
+ const normalizedMaxScrollTop = normalizeNonNegativeMetric(maxScrollTop);
17
+ return normalizedMaxScrollTop <= 0 || normalizeNonNegativeMetric(scrollTop) >= normalizedMaxScrollTop - normalizeNonNegativeMetric(tolerancePx);
18
+ }
19
+ function normalizeNonNegativeMetric(value) {
20
+ return Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0;
21
+ }
22
+ //#endregion
23
+ export { isScrollAtBottom, resolveScrollFollowPolicy };
@@ -0,0 +1,18 @@
1
+ //#region packages/browser/src/frontend/terminal-scrollback-window.ts
2
+ const DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT = 1200;
3
+ function resolveRenderedScrollbackWindow(options) {
4
+ const totalRows = Math.max(0, Math.floor(options.totalRows));
5
+ const limit = normalizeRenderedScrollbackLimit(options.limit);
6
+ const renderedRows = Math.min(totalRows, limit);
7
+ return {
8
+ firstOffset: Math.max(0, renderedRows - 1),
9
+ renderedRows,
10
+ skippedRows: Math.max(0, totalRows - renderedRows)
11
+ };
12
+ }
13
+ function normalizeRenderedScrollbackLimit(value) {
14
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT;
15
+ return Math.max(1, Math.floor(value));
16
+ }
17
+ //#endregion
18
+ export { DEFAULT_RENDERED_SCROLLBACK_ROW_LIMIT, resolveRenderedScrollbackWindow };
@@ -1,4 +1,4 @@
1
- //#region src/frontend/terminal-theme-protocol.d.ts
1
+ //#region packages/browser/src/frontend/terminal-theme-protocol.d.ts
2
2
  type TerminalThemeName = "dark" | "light";
3
3
  type TerminalThemeProtocolUpdate = {
4
4
  colors: Partial<Record<TerminalThemeColorSlot, string>>;
@@ -1,4 +1,4 @@
1
- //#region src/frontend/terminal-theme-protocol.ts
1
+ //#region packages/browser/src/frontend/terminal-theme-protocol.ts
2
2
  const BEL = "\x07";
3
3
  const ESC = "\x1B";
4
4
  const MAX_OSC_LENGTH = 512;