@aitty/browser 0.1.1 → 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.
- package/README.md +84 -0
- package/dist/browser.d.ts +6 -4
- package/dist/browser.js +3 -1
- package/dist/frontend/aitty-sw.js +43 -0
- package/dist/frontend/ansi-sequences.d.ts +7 -2
- package/dist/frontend/ansi-sequences.js +65 -2
- package/dist/frontend/ansi-style-tracker.d.ts +2 -6
- package/dist/frontend/ansi-style-tracker.js +11 -47
- package/dist/frontend/browser-terminal-renderer.d.ts +23 -15
- package/dist/frontend/browser-terminal-renderer.js +266 -102
- package/dist/frontend/cell-width.d.ts +1 -1
- package/dist/frontend/cell-width.js +1 -1
- package/dist/frontend/shell-controls.d.ts +24 -0
- package/dist/frontend/shell-controls.js +221 -0
- package/dist/frontend/terminal-app.d.ts +84 -20
- package/dist/frontend/terminal-app.js +2672 -278
- package/dist/frontend/terminal-config.d.ts +62 -0
- package/dist/frontend/terminal-config.js +126 -0
- package/dist/frontend/terminal-input-policies.d.ts +12 -2
- package/dist/frontend/terminal-input-policies.js +131 -49
- package/dist/frontend/terminal-scroll-anchor.js +25 -0
- package/dist/frontend/terminal-scroll-follow.js +23 -0
- package/dist/frontend/terminal-scrollback-window.js +18 -0
- package/dist/frontend/terminal-theme-protocol.d.ts +1 -1
- package/dist/frontend/terminal-theme-protocol.js +1 -1
- package/dist/frontend/terminal.css +161 -19
- package/dist/frontend/virtual-transcript-window.js +42 -0
- package/package.json +3 -3
|
@@ -1,22 +1,86 @@
|
|
|
1
|
-
import { nextAltScreenState, parseCsiParams } from "./ansi-sequences.js";
|
|
1
|
+
import { createFocusReportingParser, nextAltScreenState, parseCsiParams } from "./ansi-sequences.js";
|
|
2
2
|
import { createAnsiStyleTracker } from "./ansi-style-tracker.js";
|
|
3
3
|
import { BrowserTerminalRenderer } from "./browser-terminal-renderer.js";
|
|
4
|
-
import {
|
|
4
|
+
import { areTerminalAppearancesEqual, areTerminalBehaviorsEqual, cloneTerminalConfig, hasTerminalLayoutAppearanceChange, mergeTerminalConfig, normalizeTerminalConfig, normalizeThemeName, normalizeThemeTarget } from "./terminal-config.js";
|
|
5
|
+
import { installShellControls } from "./shell-controls.js";
|
|
6
|
+
import { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, resolveTerminalDocumentKeyData } from "./terminal-input-policies.js";
|
|
7
|
+
import { resolveScrollAnchorRestoreTop } from "./terminal-scroll-anchor.js";
|
|
8
|
+
import { isScrollAtBottom, resolveScrollFollowPolicy } from "./terminal-scroll-follow.js";
|
|
5
9
|
import { createTerminalThemeProtocolParser, resolveTerminalThemeQueryResponse } from "./terminal-theme-protocol.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
10
|
+
import { resolveVirtualTranscriptWindow } from "./virtual-transcript-window.js";
|
|
11
|
+
import { WTerm, hasTerminalSelection } from "@aitty/wterm-dom";
|
|
12
|
+
import { createPongControlFrame, createPushSubscribeControlFrame, createPushUnsubscribeControlFrame, createResizeControlFrame, createTakeoverRequestControlFrame, createTakeoverResponseControlFrame, isAittyRoleControlFrame, isAittyTakeoverRequestControlFrame, isAittyTakeoverResultControlFrame, parseAittyControlFrame } from "@aitty/protocol";
|
|
13
|
+
//#region packages/browser/src/frontend/terminal-app.ts
|
|
9
14
|
const SUPERSEDED_CLOSE_CODE = 4001;
|
|
10
15
|
const DEFAULT_MAX_BYTES_PER_FRAME = 32 * 1024;
|
|
11
|
-
const DEFAULT_TRANSCRIPT_SCROLLBACK_LIMIT = 1e3;
|
|
12
16
|
const DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
17
|
+
const DEFAULT_TERMINAL_OUTPUT_FRAME_INTERVAL_MS = 1e3 / 30;
|
|
18
|
+
const HIGH_PRESSURE_TERMINAL_OUTPUT_FRAME_INTERVAL_MS = 1e3 / 24;
|
|
19
|
+
const HIGH_PRESSURE_TERMINAL_OUTPUT_PENDING_BYTES = 96 * 1024;
|
|
20
|
+
const OUTPUT_PRESSURE_WINDOW_MS = 750;
|
|
21
|
+
const OUTPUT_PRESSURE_CHUNK_THRESHOLD = 12;
|
|
22
|
+
const OUTPUT_PRESSURE_BYTE_THRESHOLD = 128 * 1024;
|
|
23
|
+
const OUTPUT_PRESSURE_HOLD_MS = 1e3;
|
|
24
|
+
const DEFAULT_TERMINAL_RENDER_FONT_SIZE = 15;
|
|
13
25
|
const SCREEN_REDRAW_ARCHIVE_SUPPRESS_MS = 500;
|
|
26
|
+
const DESKTOP_IME_TEXTAREA_INSET_PX = 8;
|
|
27
|
+
const DESKTOP_IME_CANDIDATE_SAFE_WIDTH_PX = 560;
|
|
28
|
+
const DESKTOP_IME_CANDIDATE_SAFE_HEIGHT_PX = 260;
|
|
29
|
+
const IOS_BROWSER_CHROME_KEYBOARD_GUARD_PX = 56;
|
|
30
|
+
const MOBILE_SCREEN_KEYBOARD_BASELINE_MIN_INSET_PX = 240;
|
|
31
|
+
const MOBILE_FOCUS_PROXY_MIN_WIDTH_PX = 280;
|
|
32
|
+
const MOBILE_FOCUS_PROXY_HEIGHT_PX = 44;
|
|
33
|
+
const MOBILE_FOCUS_PROXY_VIEWPORT_GAP_PX = 10;
|
|
34
|
+
const MOBILE_FOCUS_PROXY_OPACITY = "0.02";
|
|
35
|
+
const MOBILE_KEYBOARD_ALIGN_DELAYS_MS = [
|
|
36
|
+
60,
|
|
37
|
+
180,
|
|
38
|
+
360,
|
|
39
|
+
700,
|
|
40
|
+
1100
|
|
41
|
+
];
|
|
42
|
+
const MOBILE_COMPOSER_DIRECTION_LONG_PRESS_MS = 480;
|
|
43
|
+
const MOBILE_COMPOSER_DOCK_SIZE_PX = 58;
|
|
44
|
+
const MOBILE_COMPOSER_BUTTON_SIZE_PX = 54;
|
|
45
|
+
const MOBILE_COMPOSER_BUTTON_GAP_PX = 10;
|
|
46
|
+
const MOBILE_COMPOSER_EXPANDED_GAP_PX = 64;
|
|
47
|
+
const MOBILE_COMPOSER_MARGIN_PX = 10;
|
|
48
|
+
const MOBILE_COMPOSER_DIRECTION_OUTSET_PX = 62;
|
|
49
|
+
const MOBILE_COMPOSER_DIRECTION_BOTTOM_OUTSET_PX = 66;
|
|
50
|
+
const MOBILE_COMPOSER_TAP_SLOP_PX = 8;
|
|
51
|
+
const MOBILE_COMPOSER_CLICK_SUPPRESS_MS = 420;
|
|
52
|
+
const MOBILE_KEYBOARD_SCROLL_INTENT_MS = 1200;
|
|
53
|
+
const MOBILE_KEYBOARD_MANUAL_SCROLL_SUPPRESS_MS = 1800;
|
|
54
|
+
const TAKEOVER_NOTICE_TIMEOUT_MS = 6e4;
|
|
55
|
+
const OUTPUT_DRAIN_SETTLE_DELAY_MS = 80;
|
|
56
|
+
const LAYOUT_BOTTOM_FOLLOW_DELAYS_MS = [
|
|
57
|
+
80,
|
|
58
|
+
180,
|
|
59
|
+
360,
|
|
60
|
+
700,
|
|
61
|
+
1100,
|
|
62
|
+
1600
|
|
63
|
+
];
|
|
64
|
+
const USER_INPUT_LAYOUT_STICK_MS = 12e4;
|
|
65
|
+
const KEYBOARD_VISIBLE_HEIGHT_RATIO = .85;
|
|
66
|
+
const VIEWPORT_BASELINE_WIDTH_RESET_PX = 24;
|
|
67
|
+
const LIVE_INPUT_KEYBOARD_MAX_GAP_PX = 112;
|
|
68
|
+
const TERMINAL_FOCUS_IN = "\x1B[I";
|
|
14
69
|
const TERMINAL_THEME_PROTOCOL_VARIABLES = [
|
|
15
70
|
"--theme-term-bg",
|
|
16
71
|
"--theme-term-cursor",
|
|
17
72
|
"--theme-term-fg",
|
|
73
|
+
"--theme-term-reverse-bg",
|
|
74
|
+
"--theme-term-reverse-fg",
|
|
18
75
|
...Array.from({ length: 16 }, (_, index) => `--theme-term-color-${index}`)
|
|
19
76
|
];
|
|
77
|
+
let nextTerminalInputId = 0;
|
|
78
|
+
const MOBILE_COMPOSER_PRIMARY_CONTROLS = Object.freeze([
|
|
79
|
+
"esc",
|
|
80
|
+
"tab",
|
|
81
|
+
"enter"
|
|
82
|
+
]);
|
|
83
|
+
const browserViewportBaselines = /* @__PURE__ */ new WeakMap();
|
|
20
84
|
/**
|
|
21
85
|
* Batches raw PTY bytes into animation frames without splitting UTF-8 codepoints or
|
|
22
86
|
* incomplete ANSI sequences. This is the only throttling layer in the browser path.
|
|
@@ -24,21 +88,60 @@ const TERMINAL_THEME_PROTOCOL_VARIABLES = [
|
|
|
24
88
|
function createBufferedTerminalWriter(target, scheduler = {}, options = {}) {
|
|
25
89
|
const requestFrame = scheduler.requestFrame ?? ((callback) => requestBrowserFrame(callback));
|
|
26
90
|
const cancelFrame = scheduler.cancelFrame ?? ((handle) => cancelBrowserFrame(handle));
|
|
91
|
+
const now = scheduler.now ?? getMonotonicTime;
|
|
92
|
+
const scheduleTimeout = scheduler.setTimeout ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
93
|
+
const clearScheduledTimeout = scheduler.clearTimeout ?? ((handle) => clearTimeout(handle));
|
|
27
94
|
const maxBytesPerFrame = Math.max(1, options.maxBytesPerFrame ?? DEFAULT_MAX_BYTES_PER_FRAME);
|
|
95
|
+
const minFrameIntervalMs = Math.max(0, options.minFrameIntervalMs ?? 0);
|
|
28
96
|
let chunks = [];
|
|
29
97
|
let frameHandle = null;
|
|
98
|
+
let frameTimer = null;
|
|
99
|
+
let headIndex = 0;
|
|
30
100
|
let headOffset = 0;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
101
|
+
let lastFlushTimestamp = 0;
|
|
102
|
+
let pendingByteLength = 0;
|
|
103
|
+
const resetQueue = () => {
|
|
104
|
+
chunks = [];
|
|
105
|
+
headIndex = 0;
|
|
106
|
+
headOffset = 0;
|
|
107
|
+
pendingByteLength = 0;
|
|
108
|
+
};
|
|
109
|
+
const compactQueue = () => {
|
|
110
|
+
if (headIndex === 0) return;
|
|
111
|
+
if (headIndex < 64 && headIndex * 2 < chunks.length) return;
|
|
112
|
+
chunks = chunks.slice(headIndex);
|
|
113
|
+
headIndex = 0;
|
|
114
|
+
};
|
|
115
|
+
const peekPendingBytes = (maxByteCount) => {
|
|
116
|
+
const byteCount = Math.min(Math.max(0, maxByteCount), pendingByteLength);
|
|
117
|
+
if (byteCount === 0) return new Uint8Array(0);
|
|
118
|
+
const head = chunks[headIndex];
|
|
119
|
+
if (!head) {
|
|
120
|
+
resetQueue();
|
|
121
|
+
return new Uint8Array(0);
|
|
122
|
+
}
|
|
123
|
+
if (byteCount <= head.length - headOffset) return head.subarray(headOffset, headOffset + byteCount);
|
|
124
|
+
const payload = new Uint8Array(byteCount);
|
|
125
|
+
let copiedBytes = 0;
|
|
126
|
+
let chunkIndex = headIndex;
|
|
127
|
+
while (copiedBytes < byteCount && chunkIndex < chunks.length) {
|
|
128
|
+
const chunk = chunks[chunkIndex];
|
|
129
|
+
const chunkOffset = chunkIndex === headIndex ? headOffset : 0;
|
|
130
|
+
const chunkAvailableBytes = chunk.length - chunkOffset;
|
|
131
|
+
const copyBytes = Math.min(chunkAvailableBytes, byteCount - copiedBytes);
|
|
132
|
+
payload.set(chunk.subarray(chunkOffset, chunkOffset + copyBytes), copiedBytes);
|
|
133
|
+
copiedBytes += copyBytes;
|
|
134
|
+
chunkIndex += 1;
|
|
135
|
+
}
|
|
136
|
+
return payload;
|
|
35
137
|
};
|
|
36
138
|
const consumePendingBytes = (byteCount) => {
|
|
37
|
-
let remainingBytes = byteCount;
|
|
38
|
-
|
|
39
|
-
|
|
139
|
+
let remainingBytes = Math.min(Math.max(0, byteCount), pendingByteLength);
|
|
140
|
+
pendingByteLength -= remainingBytes;
|
|
141
|
+
while (headIndex < chunks.length && remainingBytes > 0) {
|
|
142
|
+
const availableBytes = chunks[headIndex].length - headOffset;
|
|
40
143
|
if (availableBytes <= remainingBytes) {
|
|
41
|
-
|
|
144
|
+
headIndex += 1;
|
|
42
145
|
headOffset = 0;
|
|
43
146
|
remainingBytes -= availableBytes;
|
|
44
147
|
continue;
|
|
@@ -46,14 +149,31 @@ function createBufferedTerminalWriter(target, scheduler = {}, options = {}) {
|
|
|
46
149
|
headOffset += remainingBytes;
|
|
47
150
|
remainingBytes = 0;
|
|
48
151
|
}
|
|
152
|
+
if (pendingByteLength === 0) {
|
|
153
|
+
resetQueue();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
compactQueue();
|
|
49
157
|
};
|
|
50
158
|
const isUtf8ContinuationByte = (value) => (value & 192) === 128;
|
|
51
159
|
const isUtf8LeadByte = (value) => (value & 128) !== 0 && !isUtf8ContinuationByte(value);
|
|
160
|
+
const resolveUtf8SequenceLength = (value) => {
|
|
161
|
+
if ((value & 224) === 192) return 2;
|
|
162
|
+
if ((value & 240) === 224) return 3;
|
|
163
|
+
if ((value & 248) === 240) return 4;
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
52
166
|
const resolveUtf8SafeBoundary = (bytes, preferredEnd) => {
|
|
53
167
|
if (preferredEnd <= 0 || preferredEnd >= bytes.length) return preferredEnd;
|
|
54
168
|
let index = preferredEnd;
|
|
55
169
|
while (index > 0 && isUtf8ContinuationByte(bytes[index])) index -= 1;
|
|
56
|
-
if (index < preferredEnd && isUtf8LeadByte(bytes[index]))
|
|
170
|
+
if (index < preferredEnd && isUtf8LeadByte(bytes[index])) {
|
|
171
|
+
if (index === 0) {
|
|
172
|
+
const sequenceLength = resolveUtf8SequenceLength(bytes[index]);
|
|
173
|
+
if (sequenceLength && sequenceLength <= bytes.length) return sequenceLength;
|
|
174
|
+
}
|
|
175
|
+
return index;
|
|
176
|
+
}
|
|
57
177
|
return preferredEnd;
|
|
58
178
|
};
|
|
59
179
|
const resolveAnsiSafeBoundary = (bytes, preferredEnd) => {
|
|
@@ -112,68 +232,209 @@ function createBufferedTerminalWriter(target, scheduler = {}, options = {}) {
|
|
|
112
232
|
boundary = resolveAnsiSafeBoundary(bytes, boundary);
|
|
113
233
|
return boundary > 0 ? boundary : preferredEnd;
|
|
114
234
|
};
|
|
115
|
-
const
|
|
235
|
+
const clearScheduledFrame = () => {
|
|
236
|
+
if (frameHandle !== null) {
|
|
237
|
+
cancelFrame(frameHandle);
|
|
238
|
+
frameHandle = null;
|
|
239
|
+
}
|
|
240
|
+
if (frameTimer !== null) {
|
|
241
|
+
clearScheduledTimeout(frameTimer);
|
|
242
|
+
frameTimer = null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const resolveMinFrameIntervalMs = () => {
|
|
246
|
+
const resolved = options.resolveMinFrameIntervalMs?.({ pendingByteLength });
|
|
247
|
+
return Math.max(0, resolved ?? minFrameIntervalMs);
|
|
248
|
+
};
|
|
249
|
+
const scheduleFlush = () => {
|
|
250
|
+
if (frameHandle !== null || frameTimer !== null) return;
|
|
251
|
+
const frameIntervalMs = resolveMinFrameIntervalMs();
|
|
252
|
+
const delayMs = frameIntervalMs <= 0 ? 0 : Math.max(0, frameIntervalMs - (now() - lastFlushTimestamp));
|
|
253
|
+
const requestNextFrame = () => {
|
|
254
|
+
frameHandle = requestFrame((timestamp) => {
|
|
255
|
+
flush(timestamp);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
if (delayMs > 1) {
|
|
259
|
+
frameTimer = scheduleTimeout(() => {
|
|
260
|
+
frameTimer = null;
|
|
261
|
+
requestNextFrame();
|
|
262
|
+
}, delayMs);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
requestNextFrame();
|
|
266
|
+
};
|
|
267
|
+
const flush = (timestamp = now()) => {
|
|
116
268
|
frameHandle = null;
|
|
117
|
-
if (
|
|
118
|
-
|
|
269
|
+
if (pendingByteLength === 0) {
|
|
270
|
+
resetQueue();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const preferredEnd = Math.min(maxBytesPerFrame, pendingByteLength);
|
|
274
|
+
const pendingBytes = peekPendingBytes(preferredEnd < pendingByteLength ? preferredEnd + 1 : preferredEnd);
|
|
119
275
|
if (pendingBytes.length === 0) {
|
|
120
|
-
|
|
121
|
-
headOffset = 0;
|
|
276
|
+
resetQueue();
|
|
122
277
|
return;
|
|
123
278
|
}
|
|
124
|
-
const preferredEnd = Math.min(maxBytesPerFrame, pendingBytes.length);
|
|
125
279
|
const safeEnd = resolveSafeSplitBoundary(pendingBytes, preferredEnd);
|
|
126
|
-
const trailingAnsiCarryLength = preferredEnd ===
|
|
280
|
+
const trailingAnsiCarryLength = preferredEnd === pendingByteLength ? resolveTrailingAnsiCarryLength(pendingBytes) : 0;
|
|
127
281
|
const payloadByteCount = trailingAnsiCarryLength > 0 ? Math.max(0, safeEnd - trailingAnsiCarryLength) : safeEnd > 0 ? safeEnd : preferredEnd;
|
|
128
|
-
if (payloadByteCount <= 0)
|
|
129
|
-
frameHandle = requestFrame(() => {
|
|
130
|
-
flush();
|
|
131
|
-
});
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
282
|
+
if (payloadByteCount <= 0) return;
|
|
134
283
|
const payload = pendingBytes.subarray(0, payloadByteCount);
|
|
135
284
|
target.write(payload);
|
|
285
|
+
lastFlushTimestamp = timestamp;
|
|
136
286
|
consumePendingBytes(payloadByteCount);
|
|
137
|
-
|
|
138
|
-
|
|
287
|
+
options.onFlush?.({
|
|
288
|
+
pendingByteLength,
|
|
289
|
+
wroteByteLength: payloadByteCount
|
|
139
290
|
});
|
|
291
|
+
if (pendingByteLength > 0) {
|
|
292
|
+
scheduleFlush();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
options.onDrain?.();
|
|
140
296
|
};
|
|
141
297
|
return {
|
|
142
298
|
destroy() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
frameHandle = null;
|
|
146
|
-
}
|
|
147
|
-
chunks = [];
|
|
148
|
-
headOffset = 0;
|
|
299
|
+
clearScheduledFrame();
|
|
300
|
+
resetQueue();
|
|
149
301
|
},
|
|
150
302
|
discardPending() {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
frameHandle = null;
|
|
154
|
-
}
|
|
155
|
-
chunks = [];
|
|
156
|
-
headOffset = 0;
|
|
303
|
+
clearScheduledFrame();
|
|
304
|
+
resetQueue();
|
|
157
305
|
},
|
|
158
306
|
enqueue(chunk) {
|
|
159
307
|
if (chunk.length === 0) return;
|
|
160
308
|
chunks.push(chunk);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
309
|
+
pendingByteLength += chunk.length;
|
|
310
|
+
scheduleFlush();
|
|
164
311
|
}
|
|
165
312
|
};
|
|
166
313
|
}
|
|
314
|
+
function resolveBrowserViewport(windowObject) {
|
|
315
|
+
const visualViewport = windowObject.visualViewport;
|
|
316
|
+
const root = windowObject.document?.documentElement;
|
|
317
|
+
const body = windowObject.document?.body;
|
|
318
|
+
const layoutWidth = firstFinitePositiveNumber(windowObject.innerWidth, root?.clientWidth, body?.clientWidth) ?? 1;
|
|
319
|
+
const currentLayoutHeight = largestFinitePositiveNumber(windowObject.innerHeight, root?.clientHeight) ?? firstFinitePositiveNumber(body?.clientHeight) ?? 1;
|
|
320
|
+
const visualWidth = firstFinitePositiveNumber(visualViewport?.width);
|
|
321
|
+
const visualHeight = firstFinitePositiveNumber(visualViewport?.height);
|
|
322
|
+
const visualOffsetTop = normalizeViewportOffset(visualViewport?.offsetTop);
|
|
323
|
+
const height = visualHeight ?? currentLayoutHeight;
|
|
324
|
+
const layoutHeight = resolveStableViewportBaselineHeight(windowObject, layoutWidth, currentLayoutHeight, height);
|
|
325
|
+
const visualKeyboardVisible = layoutHeight > 0 && height < layoutHeight * KEYBOARD_VISIBLE_HEIGHT_RATIO;
|
|
326
|
+
const keyboardInsetBottom = visualKeyboardVisible ? Math.max(0, layoutHeight - height) : 0;
|
|
327
|
+
const keyboardVisible = visualKeyboardVisible && keyboardInsetBottom > 0;
|
|
328
|
+
const browserChromeInsetBottom = resolveBrowserChromeKeyboardGuard(windowObject, keyboardVisible);
|
|
329
|
+
return {
|
|
330
|
+
browserChromeInsetBottom,
|
|
331
|
+
height,
|
|
332
|
+
keyboardInsetBottom,
|
|
333
|
+
offsetTop: visualOffsetTop,
|
|
334
|
+
safeHeight: keyboardVisible ? layoutHeight : Math.max(1, height - browserChromeInsetBottom),
|
|
335
|
+
width: visualWidth ?? layoutWidth
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function resolveStableViewportBaselineHeight(windowObject, width, layoutHeight, visibleHeight) {
|
|
339
|
+
const screenHeight = resolveMobileScreenViewportBaselineHeight(windowObject, width, visibleHeight);
|
|
340
|
+
const height = Math.max(1, layoutHeight, visibleHeight, screenHeight ?? 0);
|
|
341
|
+
const previous = browserViewportBaselines.get(windowObject);
|
|
342
|
+
if (!previous || Math.abs(previous.width - width) > VIEWPORT_BASELINE_WIDTH_RESET_PX) {
|
|
343
|
+
browserViewportBaselines.set(windowObject, {
|
|
344
|
+
height,
|
|
345
|
+
width
|
|
346
|
+
});
|
|
347
|
+
return height;
|
|
348
|
+
}
|
|
349
|
+
if (height > previous.height) {
|
|
350
|
+
const nextBaseline = {
|
|
351
|
+
height,
|
|
352
|
+
width
|
|
353
|
+
};
|
|
354
|
+
browserViewportBaselines.set(windowObject, nextBaseline);
|
|
355
|
+
return nextBaseline.height;
|
|
356
|
+
}
|
|
357
|
+
return previous.height;
|
|
358
|
+
}
|
|
359
|
+
function resolveMobileScreenViewportBaselineHeight(windowObject, width, visibleHeight) {
|
|
360
|
+
if (!isCoarseMobileViewport(windowObject) || !isAittyInputFocused(windowObject)) return null;
|
|
361
|
+
const screen = windowObject.screen;
|
|
362
|
+
const candidate = largestFinitePositiveNumber(screen?.availHeight, screen?.height);
|
|
363
|
+
if (!candidate || candidate < width) return null;
|
|
364
|
+
if (candidate - visibleHeight < MOBILE_SCREEN_KEYBOARD_BASELINE_MIN_INSET_PX) return null;
|
|
365
|
+
return candidate;
|
|
366
|
+
}
|
|
367
|
+
function isAittyInputFocused(windowObject) {
|
|
368
|
+
return (windowObject.document?.activeElement)?.hasAttribute?.("data-aitty-input") === true;
|
|
369
|
+
}
|
|
370
|
+
function resolveMobileFocusProxyPosition(windowObject) {
|
|
371
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
372
|
+
const anchor = resolveMobileLiveInputAnchorRect(windowObject);
|
|
373
|
+
const width = Math.max(44, viewport.width - 16);
|
|
374
|
+
const height = MOBILE_FOCUS_PROXY_HEIGHT_PX;
|
|
375
|
+
const viewportTop = viewport.offsetTop + MOBILE_FOCUS_PROXY_VIEWPORT_GAP_PX;
|
|
376
|
+
const viewportBottom = resolveMobileFocusProxyBottom(viewport, height);
|
|
377
|
+
if (anchor) return {
|
|
378
|
+
anchor: "mobile-focus-proxy",
|
|
379
|
+
height,
|
|
380
|
+
left: 8,
|
|
381
|
+
top: clamp(anchor.top + Math.max(0, anchor.height - height), viewportTop, viewportBottom - height),
|
|
382
|
+
width
|
|
383
|
+
};
|
|
384
|
+
const topAnchor = viewport.keyboardInsetBottom > 0 ? viewportBottom - height : viewport.offsetTop + Math.max(12, Math.min(96, viewport.height * .32));
|
|
385
|
+
return {
|
|
386
|
+
anchor: "mobile-focus-proxy",
|
|
387
|
+
height,
|
|
388
|
+
left: Math.max(8, Math.min(24, viewport.width - 2)),
|
|
389
|
+
top: clamp(topAnchor, viewportTop, viewportBottom - height),
|
|
390
|
+
width
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function shouldEnableMobileFocusProxyPointerEvents(windowObject, state = {}) {
|
|
394
|
+
if (!isCoarseMobileViewport(windowObject)) return false;
|
|
395
|
+
if (state.keyboardOpen) return true;
|
|
396
|
+
return resolveMobileLiveInputAnchorRect(windowObject) !== null;
|
|
397
|
+
}
|
|
398
|
+
function isMobileLiveInputTapTarget(terminalRoot, windowObject, clientX, clientY) {
|
|
399
|
+
const anchorRect = resolveLiveInputSurfaceAnchor(terminalRoot)?.getBoundingClientRect?.();
|
|
400
|
+
if (!hasUsableClientRect(anchorRect)) return false;
|
|
401
|
+
const rect = anchorRect;
|
|
402
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
403
|
+
const proxyHeight = MOBILE_FOCUS_PROXY_HEIGHT_PX;
|
|
404
|
+
const viewportTop = viewport.offsetTop + MOBILE_FOCUS_PROXY_VIEWPORT_GAP_PX;
|
|
405
|
+
const viewportBottom = resolveMobileFocusProxyBottom(viewport, proxyHeight);
|
|
406
|
+
const hitTop = clamp(rect.top + Math.max(0, rect.height - proxyHeight), viewportTop, viewportBottom - proxyHeight);
|
|
407
|
+
const hitBottom = hitTop + proxyHeight;
|
|
408
|
+
return clientX >= 0 && clientX <= viewport.width && clientY >= hitTop && clientY <= hitBottom;
|
|
409
|
+
}
|
|
410
|
+
function resolveMobileFocusProxyBottom(viewport, proxyHeight) {
|
|
411
|
+
const visibleBottom = viewport.offsetTop + viewport.height - viewport.browserChromeInsetBottom - MOBILE_FOCUS_PROXY_VIEWPORT_GAP_PX;
|
|
412
|
+
return Math.max(viewport.offsetTop + MOBILE_FOCUS_PROXY_VIEWPORT_GAP_PX + proxyHeight, visibleBottom);
|
|
413
|
+
}
|
|
414
|
+
function resolveMobileKeyboardPromptAlignDelays() {
|
|
415
|
+
return [...MOBILE_KEYBOARD_ALIGN_DELAYS_MS];
|
|
416
|
+
}
|
|
417
|
+
function resolveTerminalOutputFrameIntervalMs({ pendingByteLength, sustainedPressure = false }) {
|
|
418
|
+
if (sustainedPressure || pendingByteLength >= HIGH_PRESSURE_TERMINAL_OUTPUT_PENDING_BYTES) return HIGH_PRESSURE_TERMINAL_OUTPUT_FRAME_INTERVAL_MS;
|
|
419
|
+
return DEFAULT_TERMINAL_OUTPUT_FRAME_INTERVAL_MS;
|
|
420
|
+
}
|
|
421
|
+
function resolveMobileComposerExpandedListHeight(controlCount = MOBILE_COMPOSER_PRIMARY_CONTROLS.length) {
|
|
422
|
+
return controlCount * MOBILE_COMPOSER_BUTTON_SIZE_PX + Math.max(0, controlCount - 1) * MOBILE_COMPOSER_BUTTON_GAP_PX;
|
|
423
|
+
}
|
|
167
424
|
async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
168
425
|
const elements = dependencies.elements ?? {};
|
|
169
426
|
const shell = elements.shell ?? getRequiredElement(doc, "[data-shell]");
|
|
170
427
|
const status = hasElementOverride(elements, "status") ? elements.status ?? null : getOptionalElement(doc, "[data-terminal-status]");
|
|
171
428
|
const loading = hasElementOverride(elements, "loading") ? elements.loading ?? null : getOptionalElement(doc, "[data-terminal-loading]");
|
|
172
429
|
const terminalRoot = elements.terminalRoot ?? getRequiredElement(doc, "[data-terminal-root]");
|
|
430
|
+
const takeoverControl = shell.querySelector("[data-aitty-take-control]");
|
|
173
431
|
terminalRoot.classList.add("aitty-terminal-root");
|
|
174
432
|
const windowObject = doc.defaultView ?? window;
|
|
175
433
|
const sessionUrl = resolveAittySessionUrl(windowObject, dependencies.src);
|
|
176
|
-
const
|
|
434
|
+
const initialConfig = normalizeTerminalConfig(dependencies.config);
|
|
435
|
+
const themeController = createTerminalThemeController(doc, shell, terminalRoot, initialConfig.appearance.theme, initialConfig.appearance.themeTarget);
|
|
436
|
+
let terminalConfig = initialConfig;
|
|
437
|
+
applyTerminalAppearance(shell, terminalRoot, terminalConfig.appearance);
|
|
177
438
|
const resolvedRuntimeKind = (dependencies.resolveRuntimeKind ?? ((resolverDoc, resolverShell) => resolveShellRuntimeKind(resolverDoc, resolverShell, sessionUrl)))(doc, shell);
|
|
178
439
|
const runtimeKind = typeof resolvedRuntimeKind === "string" ? resolvedRuntimeKind : await resolvedRuntimeKind;
|
|
179
440
|
shell.dataset.runtime = runtimeKind;
|
|
@@ -183,33 +444,60 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
183
444
|
const requestFrame = frameScheduler.requestFrame ?? ((callback) => windowObject.requestAnimationFrame(callback));
|
|
184
445
|
const cancelFrame = frameScheduler.cancelFrame ?? ((handle) => windowObject.cancelAnimationFrame(handle));
|
|
185
446
|
const reconnectDelayMs = Math.max(0, dependencies.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS);
|
|
186
|
-
const getScrollSurface = () => resolveScrollSurface(doc, terminalRoot, dependencies.scroll);
|
|
447
|
+
const getScrollSurface = () => resolveScrollSurface(doc, shell, terminalRoot, dependencies.scroll);
|
|
187
448
|
const encoder = new TextEncoder();
|
|
188
449
|
const ansiStyleTracker = createAnsiStyleTracker();
|
|
450
|
+
const focusReportingParser = createFocusReportingParser();
|
|
189
451
|
const themeProtocolParser = createTerminalThemeProtocolParser();
|
|
190
452
|
let transport = null;
|
|
191
453
|
let term = null;
|
|
192
454
|
let transcriptArchive = null;
|
|
193
455
|
let writer = null;
|
|
194
|
-
let archiveWriter = null;
|
|
195
456
|
let sessionState = "running";
|
|
196
457
|
let destroyed = false;
|
|
197
458
|
let helloReceived = false;
|
|
198
459
|
let outputReady = false;
|
|
199
460
|
let reconnecting = false;
|
|
200
461
|
let termReady = false;
|
|
462
|
+
let terminalFocusReportingEnabled = false;
|
|
463
|
+
let terminalInputFocused = false;
|
|
464
|
+
let clientId = null;
|
|
465
|
+
let clientRole = null;
|
|
466
|
+
let takeoverPending = false;
|
|
467
|
+
let authoritativeTerminalSize = null;
|
|
468
|
+
let takeoverNotice = null;
|
|
201
469
|
let snapScrollbackOnNextWrite = false;
|
|
202
470
|
let followLiveOutput = false;
|
|
471
|
+
let pendingInitialBottomFollow = false;
|
|
203
472
|
const pendingFrames = [];
|
|
204
473
|
const pendingInputFrames = [];
|
|
205
474
|
let reconnectHandle = null;
|
|
206
475
|
let scrollbackSnapHandle = null;
|
|
207
476
|
let scrollbackSnapPassesRemaining = 0;
|
|
477
|
+
let scrollProgrammaticGeneration = 0;
|
|
478
|
+
const programmaticScrollSurfaces = /* @__PURE__ */ new WeakMap();
|
|
479
|
+
let pendingLayoutBottomFollow = false;
|
|
208
480
|
let disposeViewportSizer = null;
|
|
481
|
+
let disposeViewportDebugOverlay = null;
|
|
482
|
+
let mobileComposer = null;
|
|
209
483
|
let disposeScrollMetricsObserver = null;
|
|
484
|
+
let disposeTerminalFocusInteractions = null;
|
|
210
485
|
let lastResizeSignature = null;
|
|
211
486
|
let lastScrollMetrics = null;
|
|
487
|
+
let keyboardVisible = false;
|
|
488
|
+
let shellViewportVariablesFrame = null;
|
|
489
|
+
let mobileKeyboardAlignFrame = null;
|
|
490
|
+
let outputDrainSettleTimer = null;
|
|
491
|
+
const mobileKeyboardAlignTimers = /* @__PURE__ */ new Set();
|
|
492
|
+
const layoutBottomFollowTimers = /* @__PURE__ */ new Set();
|
|
493
|
+
let mobileKeyboardAlignAllowedUntil = 0;
|
|
494
|
+
let mobileKeyboardAlignSuppressedUntil = 0;
|
|
495
|
+
let userInputLayoutStickUntil = 0;
|
|
212
496
|
let suppressTermResizeCallback = false;
|
|
497
|
+
let outputPressureWindowStart = null;
|
|
498
|
+
let outputPressureWindowBytes = 0;
|
|
499
|
+
let outputPressureWindowChunks = 0;
|
|
500
|
+
let highOutputPressureUntil = 0;
|
|
213
501
|
let statusSnapshot = {
|
|
214
502
|
connection: normalizeConnectionState(shell.dataset.connection),
|
|
215
503
|
loading: {
|
|
@@ -221,13 +509,193 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
221
509
|
sessionState: normalizeSessionPresentationState(shell.dataset.sessionState)
|
|
222
510
|
};
|
|
223
511
|
const getStatusSnapshot = () => cloneTerminalStatusSnapshot(statusSnapshot);
|
|
512
|
+
const syncTerminalInputAnchor = () => {
|
|
513
|
+
term?.input?.setImeAnchor?.(resolveLiveInputSurfaceAnchor(terminalRoot));
|
|
514
|
+
};
|
|
515
|
+
const syncShellViewportVariables = () => {
|
|
516
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
517
|
+
const wasKeyboardVisible = keyboardVisible;
|
|
518
|
+
const visibleViewportHeight = Math.max(1, viewport.height - viewport.browserChromeInsetBottom);
|
|
519
|
+
keyboardVisible = viewport.keyboardInsetBottom > 0;
|
|
520
|
+
shell.style.setProperty("--shell-visual-viewport-height", `${viewport.safeHeight}px`);
|
|
521
|
+
shell.style.setProperty("--shell-visible-viewport-height", `${visibleViewportHeight}px`);
|
|
522
|
+
shell.style.setProperty("--shell-terminal-viewport-height", `${keyboardVisible ? visibleViewportHeight : viewport.safeHeight}px`);
|
|
523
|
+
shell.style.setProperty("--shell-visual-viewport-offset-top", `${viewport.offsetTop}px`);
|
|
524
|
+
shell.style.setProperty("--shell-keyboard-inset-bottom", `${viewport.keyboardInsetBottom}px`);
|
|
525
|
+
shell.style.setProperty("--shell-browser-chrome-inset-bottom", `${viewport.browserChromeInsetBottom}px`);
|
|
526
|
+
shell.style.setProperty("--shell-mobile-obstruction-inset-bottom", keyboardVisible ? "0px" : `${viewport.browserChromeInsetBottom}px`);
|
|
527
|
+
shell.dataset.keyboardOpen = String(keyboardVisible);
|
|
528
|
+
terminalRoot.style.setProperty("--shell-terminal-keyboard-padding-block-end", "0px");
|
|
529
|
+
syncTerminalInputAnchor();
|
|
530
|
+
if (!wasKeyboardVisible && keyboardVisible && terminalInputFocused) scheduleMobileKeyboardPromptAlign({ force: true });
|
|
531
|
+
};
|
|
532
|
+
const scheduleShellViewportVariableSync = () => {
|
|
533
|
+
if (shellViewportVariablesFrame !== null) return;
|
|
534
|
+
shellViewportVariablesFrame = requestFrame(() => {
|
|
535
|
+
shellViewportVariablesFrame = null;
|
|
536
|
+
syncShellViewportVariables();
|
|
537
|
+
});
|
|
538
|
+
};
|
|
224
539
|
const publishScrollMetrics = () => {
|
|
225
|
-
const onMetricsChange = dependencies.scroll?.onMetricsChange;
|
|
226
|
-
if (!onMetricsChange) return;
|
|
227
540
|
const nextMetrics = getScrollMetrics(getScrollSurface());
|
|
228
541
|
if (lastScrollMetrics && areScrollMetricsEqual(lastScrollMetrics, nextMetrics)) return;
|
|
229
542
|
lastScrollMetrics = nextMetrics;
|
|
230
|
-
onMetricsChange({ ...nextMetrics });
|
|
543
|
+
dependencies.scroll?.onMetricsChange?.({ ...nextMetrics });
|
|
544
|
+
};
|
|
545
|
+
syncShellViewportVariables();
|
|
546
|
+
disposeViewportDebugOverlay = shouldInstallViewportDebugOverlay(windowObject, shell) ? installViewportDebugOverlay(shell, terminalRoot, getScrollSurface, windowObject) : null;
|
|
547
|
+
windowObject.addEventListener("resize", scheduleShellViewportVariableSync);
|
|
548
|
+
windowObject.visualViewport?.addEventListener("resize", scheduleShellViewportVariableSync);
|
|
549
|
+
windowObject.visualViewport?.addEventListener("scroll", scheduleShellViewportVariableSync);
|
|
550
|
+
const shouldKeepBottomFollow = () => pendingInitialBottomFollow || pendingLayoutBottomFollow || followLiveOutput;
|
|
551
|
+
const markProgrammaticScroll = (scrollSurface) => {
|
|
552
|
+
const generation = scrollProgrammaticGeneration += 1;
|
|
553
|
+
programmaticScrollSurfaces.set(scrollSurface, generation);
|
|
554
|
+
requestFrame(() => {
|
|
555
|
+
requestFrame(() => {
|
|
556
|
+
if (programmaticScrollSurfaces.get(scrollSurface) === generation) programmaticScrollSurfaces.delete(scrollSurface);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
const isProgrammaticScroll = (scrollSurface) => programmaticScrollSurfaces.has(scrollSurface);
|
|
561
|
+
const setProgrammaticScrollTop = (scrollSurface, scrollTop) => {
|
|
562
|
+
const nextScrollTop = clamp(scrollTop, 0, getScrollbackMax(scrollSurface));
|
|
563
|
+
markProgrammaticScroll(scrollSurface);
|
|
564
|
+
setScrollSurfaceScrollTop(scrollSurface, nextScrollTop);
|
|
565
|
+
};
|
|
566
|
+
const stopBottomFollow = () => {
|
|
567
|
+
pendingInitialBottomFollow = false;
|
|
568
|
+
pendingLayoutBottomFollow = false;
|
|
569
|
+
snapScrollbackOnNextWrite = false;
|
|
570
|
+
followLiveOutput = false;
|
|
571
|
+
clearOutputDrainSettleTimer();
|
|
572
|
+
clearLayoutBottomFollowTimers();
|
|
573
|
+
cancelScrollbackSnap();
|
|
574
|
+
};
|
|
575
|
+
const syncAfterProgrammaticScroll = () => {
|
|
576
|
+
transcriptArchive?.refreshNow();
|
|
577
|
+
requestScrollUiUpdate(dependencies.scroll);
|
|
578
|
+
publishScrollMetrics();
|
|
579
|
+
};
|
|
580
|
+
const clearMobileKeyboardPromptAlign = () => {
|
|
581
|
+
if (mobileKeyboardAlignFrame !== null) {
|
|
582
|
+
cancelFrame(mobileKeyboardAlignFrame);
|
|
583
|
+
mobileKeyboardAlignFrame = null;
|
|
584
|
+
}
|
|
585
|
+
for (const timer of mobileKeyboardAlignTimers) clearTimeout(timer);
|
|
586
|
+
mobileKeyboardAlignTimers.clear();
|
|
587
|
+
};
|
|
588
|
+
const clearLayoutBottomFollowTimers = () => {
|
|
589
|
+
for (const timer of layoutBottomFollowTimers) clearTimeout(timer);
|
|
590
|
+
layoutBottomFollowTimers.clear();
|
|
591
|
+
};
|
|
592
|
+
const clearOutputDrainSettleTimer = () => {
|
|
593
|
+
if (outputDrainSettleTimer === null) return;
|
|
594
|
+
clearTimeout(outputDrainSettleTimer);
|
|
595
|
+
outputDrainSettleTimer = null;
|
|
596
|
+
};
|
|
597
|
+
const now = () => windowObject.performance?.now?.() ?? Date.now();
|
|
598
|
+
const noteOutputPressure = (byteLength) => {
|
|
599
|
+
const currentTime = now();
|
|
600
|
+
if (outputPressureWindowStart === null || currentTime - outputPressureWindowStart > OUTPUT_PRESSURE_WINDOW_MS) {
|
|
601
|
+
outputPressureWindowStart = currentTime;
|
|
602
|
+
outputPressureWindowBytes = 0;
|
|
603
|
+
outputPressureWindowChunks = 0;
|
|
604
|
+
}
|
|
605
|
+
outputPressureWindowBytes += Math.max(0, byteLength);
|
|
606
|
+
outputPressureWindowChunks += 1;
|
|
607
|
+
if (outputPressureWindowBytes >= OUTPUT_PRESSURE_BYTE_THRESHOLD || outputPressureWindowChunks >= OUTPUT_PRESSURE_CHUNK_THRESHOLD) highOutputPressureUntil = Math.max(highOutputPressureUntil, currentTime + OUTPUT_PRESSURE_HOLD_MS);
|
|
608
|
+
};
|
|
609
|
+
const hasHighOutputPressure = () => highOutputPressureUntil > now();
|
|
610
|
+
const grantMobileKeyboardPromptAlign = (durationMs = MOBILE_KEYBOARD_SCROLL_INTENT_MS) => {
|
|
611
|
+
mobileKeyboardAlignAllowedUntil = Math.max(mobileKeyboardAlignAllowedUntil, now() + durationMs);
|
|
612
|
+
};
|
|
613
|
+
const grantUserInputLayoutStick = (durationMs = USER_INPUT_LAYOUT_STICK_MS) => {
|
|
614
|
+
userInputLayoutStickUntil = Math.max(userInputLayoutStickUntil, now() + durationMs);
|
|
615
|
+
};
|
|
616
|
+
const isMobileKeyboardPromptAlignAllowed = (force = false) => force || mobileKeyboardAlignAllowedUntil > now();
|
|
617
|
+
const suppressMobileKeyboardPromptAlign = () => {
|
|
618
|
+
mobileKeyboardAlignSuppressedUntil = Math.max(mobileKeyboardAlignSuppressedUntil, now() + MOBILE_KEYBOARD_MANUAL_SCROLL_SUPPRESS_MS);
|
|
619
|
+
clearMobileKeyboardPromptAlign();
|
|
620
|
+
};
|
|
621
|
+
const noteMobileKeyboardManualScrollIntent = () => {
|
|
622
|
+
suppressMobileKeyboardPromptAlign();
|
|
623
|
+
userInputLayoutStickUntil = 0;
|
|
624
|
+
stopBottomFollow();
|
|
625
|
+
};
|
|
626
|
+
const isMobileKeyboardPromptAlignSuppressed = (force = false) => !force && mobileKeyboardAlignSuppressedUntil > now();
|
|
627
|
+
const hasRecentMobileKeyboardManualScrollIntent = () => mobileKeyboardAlignSuppressedUntil > now();
|
|
628
|
+
const hasRecentUserInputLayoutStick = () => userInputLayoutStickUntil > now();
|
|
629
|
+
const handleMobileKeyboardTouchScrollIntent = () => {
|
|
630
|
+
if (!terminalInputFocused || resolveBrowserViewport(windowObject).keyboardInsetBottom <= 0) return;
|
|
631
|
+
noteMobileKeyboardManualScrollIntent();
|
|
632
|
+
};
|
|
633
|
+
windowObject.addEventListener("touchmove", handleMobileKeyboardTouchScrollIntent, { passive: true });
|
|
634
|
+
const alignMobileKeyboardPrompt = (options = {}) => {
|
|
635
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
636
|
+
if (!terminalInputFocused || viewport.keyboardInsetBottom <= 0 || isMobileKeyboardPromptAlignSuppressed(options.force) || !isMobileKeyboardPromptAlignAllowed(options.force)) return;
|
|
637
|
+
syncShellViewportVariables();
|
|
638
|
+
syncTerminalInputAnchor();
|
|
639
|
+
const scrollSurface = getScrollSurface();
|
|
640
|
+
if (options.onlyIfNeeded && isLiveInputSurfaceSafelyAboveViewportBottom(shell, terminalRoot, windowObject)) return;
|
|
641
|
+
markProgrammaticScroll(scrollSurface);
|
|
642
|
+
alignLiveInputSurfaceToBottom(scrollSurface, shell, terminalRoot, windowObject);
|
|
643
|
+
markProgrammaticScroll(getScrollSurface());
|
|
644
|
+
alignLiveInputSurfaceIntoViewport(shell, terminalRoot, windowObject, scrollSurface);
|
|
645
|
+
syncAfterProgrammaticScroll();
|
|
646
|
+
};
|
|
647
|
+
const requestMobileKeyboardPromptAlign = (options = {}) => {
|
|
648
|
+
if (mobileKeyboardAlignFrame !== null || !terminalInputFocused || resolveBrowserViewport(windowObject).keyboardInsetBottom <= 0 || isMobileKeyboardPromptAlignSuppressed(options.force) || !isMobileKeyboardPromptAlignAllowed(options.force)) return;
|
|
649
|
+
mobileKeyboardAlignFrame = requestFrame(() => {
|
|
650
|
+
mobileKeyboardAlignFrame = null;
|
|
651
|
+
alignMobileKeyboardPrompt(options);
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
const scheduleMobileKeyboardPromptAlign = (options = {}) => {
|
|
655
|
+
grantMobileKeyboardPromptAlign();
|
|
656
|
+
clearMobileKeyboardPromptAlign();
|
|
657
|
+
requestMobileKeyboardPromptAlign(options);
|
|
658
|
+
for (const delay of MOBILE_KEYBOARD_ALIGN_DELAYS_MS) {
|
|
659
|
+
const timer = setTimeout(() => {
|
|
660
|
+
mobileKeyboardAlignTimers.delete(timer);
|
|
661
|
+
alignMobileKeyboardPrompt(options);
|
|
662
|
+
}, delay);
|
|
663
|
+
mobileKeyboardAlignTimers.add(timer);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
const refreshScrollMetricsObserver = () => {
|
|
667
|
+
disposeScrollMetricsObserver?.();
|
|
668
|
+
disposeScrollMetricsObserver = installScrollMetricsObserver(getScrollSurface(), () => {
|
|
669
|
+
const scrollSurface = getScrollSurface();
|
|
670
|
+
const maxScrollTop = getScrollbackMax(scrollSurface);
|
|
671
|
+
const followDecision = resolveScrollFollowPolicy({
|
|
672
|
+
keepBottomFollow: shouldKeepBottomFollow(),
|
|
673
|
+
maxScrollTop,
|
|
674
|
+
previousScrollTop: lastScrollMetrics?.scrollTop ?? scrollSurface.scrollTop,
|
|
675
|
+
programmaticScroll: isProgrammaticScroll(scrollSurface),
|
|
676
|
+
scrollTop: scrollSurface.scrollTop
|
|
677
|
+
});
|
|
678
|
+
if (followDecision.action !== "idle") {
|
|
679
|
+
if (followDecision.action === "stop-follow") {
|
|
680
|
+
stopBottomFollow();
|
|
681
|
+
syncPreservedArchiveVisibility({
|
|
682
|
+
allowReveal: true,
|
|
683
|
+
maxScrollTop,
|
|
684
|
+
scrollTop: scrollSurface.scrollTop
|
|
685
|
+
});
|
|
686
|
+
requestScrollUiUpdate(dependencies.scroll);
|
|
687
|
+
publishScrollMetrics();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
setProgrammaticScrollTop(scrollSurface, maxScrollTop);
|
|
691
|
+
syncAfterProgrammaticScroll();
|
|
692
|
+
scheduleScrollbackSnapToBottom(followDecision.passes, { preferTranscriptFollow: followDecision.preferTranscriptFollow });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
transcriptArchive?.refresh();
|
|
696
|
+
publishScrollMetrics();
|
|
697
|
+
});
|
|
698
|
+
publishScrollMetrics();
|
|
231
699
|
};
|
|
232
700
|
const presentStatus = (patch) => {
|
|
233
701
|
const nextStatus = {
|
|
@@ -251,13 +719,35 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
251
719
|
if (changed) dependencies.onStatusChange?.(getStatusSnapshot());
|
|
252
720
|
};
|
|
253
721
|
dependencies.onStatusChange?.(getStatusSnapshot());
|
|
722
|
+
const resolveLiveStatusMessage = () => {
|
|
723
|
+
if (clientRole === "viewer") return takeoverPending ? "Viewer mode. Waiting for control..." : "Viewer mode. Read-only.";
|
|
724
|
+
return "Live shell connected";
|
|
725
|
+
};
|
|
726
|
+
const syncTakeoverControl = () => {
|
|
727
|
+
shell.dataset.clientRole = clientRole ?? "unknown";
|
|
728
|
+
shell.dataset.clientId = clientId ?? "";
|
|
729
|
+
shell.dataset.takeoverPending = String(takeoverPending);
|
|
730
|
+
terminalRoot.dataset.clientRole = clientRole ?? "unknown";
|
|
731
|
+
if (!takeoverControl) return;
|
|
732
|
+
takeoverControl.hidden = clientRole !== "viewer";
|
|
733
|
+
takeoverControl.disabled = clientRole !== "viewer" || takeoverPending || sessionState !== "running";
|
|
734
|
+
takeoverControl.textContent = takeoverPending ? "Request sent" : "Take control";
|
|
735
|
+
takeoverControl.setAttribute("aria-label", takeoverPending ? "Control request pending" : "Request terminal control");
|
|
736
|
+
};
|
|
254
737
|
const syncTerminalPresentation = () => {
|
|
255
738
|
if (!transcriptArchive) return;
|
|
256
|
-
syncTerminalSurface(terminalRoot, transcriptArchive, term, ansiStyleTracker, { interactive: sessionState === "running" });
|
|
257
|
-
|
|
739
|
+
syncTerminalSurface(terminalRoot, transcriptArchive, term, ansiStyleTracker, { interactive: sessionState === "running" && clientRole === "controller" });
|
|
740
|
+
syncTakeoverControl();
|
|
741
|
+
takeoverNotice?.syncPermissionPrompt(sessionState === "running" && clientRole === "controller");
|
|
742
|
+
takeoverNotice?.syncNotificationControl();
|
|
743
|
+
mobileComposer?.setEnabled(sessionState === "running" && clientRole === "controller");
|
|
258
744
|
requestScrollUiUpdate(dependencies.scroll);
|
|
259
745
|
publishScrollMetrics();
|
|
260
746
|
};
|
|
747
|
+
const syncTerminalToAuthoritativeSize = (size) => {
|
|
748
|
+
authoritativeTerminalSize = size;
|
|
749
|
+
resizeTerminalTo(size.cols, size.rows);
|
|
750
|
+
};
|
|
261
751
|
const markLiveSession = () => {
|
|
262
752
|
reconnecting = false;
|
|
263
753
|
presentStatus({
|
|
@@ -266,11 +756,44 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
266
756
|
message: "Booting terminal...",
|
|
267
757
|
visible: false
|
|
268
758
|
},
|
|
269
|
-
message:
|
|
759
|
+
message: resolveLiveStatusMessage(),
|
|
270
760
|
output: "ready",
|
|
271
761
|
sessionState: "live"
|
|
272
762
|
});
|
|
273
763
|
};
|
|
764
|
+
const requestTakeover = () => {
|
|
765
|
+
if (clientRole !== "viewer" || takeoverPending || sessionState !== "running") {
|
|
766
|
+
syncTakeoverControl();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
takeoverPending = true;
|
|
770
|
+
presentStatus({ message: "Requesting terminal control..." });
|
|
771
|
+
syncTakeoverControl();
|
|
772
|
+
transport?.sendControl?.(createTakeoverRequestControlFrame());
|
|
773
|
+
};
|
|
774
|
+
const handleTakeoverClick = (event) => {
|
|
775
|
+
event.preventDefault();
|
|
776
|
+
requestTakeover();
|
|
777
|
+
};
|
|
778
|
+
takeoverControl?.addEventListener("click", handleTakeoverClick);
|
|
779
|
+
takeoverNotice = installTakeoverNotice(shell, windowObject, {
|
|
780
|
+
getClientId() {
|
|
781
|
+
return clientId;
|
|
782
|
+
},
|
|
783
|
+
getClientRole() {
|
|
784
|
+
return clientRole;
|
|
785
|
+
},
|
|
786
|
+
onRespond(requestId, approved) {
|
|
787
|
+
if (clientRole !== "controller") return;
|
|
788
|
+
transport?.sendControl?.(createTakeoverResponseControlFrame({
|
|
789
|
+
approved,
|
|
790
|
+
requestId
|
|
791
|
+
}));
|
|
792
|
+
},
|
|
793
|
+
sendControl(data) {
|
|
794
|
+
transport?.sendControl?.(data);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
274
797
|
const markWaitingForOutput = () => {
|
|
275
798
|
presentStatus({
|
|
276
799
|
connection: "open",
|
|
@@ -278,7 +801,7 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
278
801
|
message: "Booting terminal...",
|
|
279
802
|
visible: !outputReady
|
|
280
803
|
},
|
|
281
|
-
message: outputReady ?
|
|
804
|
+
message: outputReady ? resolveLiveStatusMessage() : "Connected. Waiting for shell output...",
|
|
282
805
|
sessionState: outputReady ? "live" : "starting"
|
|
283
806
|
});
|
|
284
807
|
};
|
|
@@ -305,26 +828,93 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
305
828
|
const getTargetScrollTop = (scrollSurface, { preferTranscriptFollow = false } = {}) => {
|
|
306
829
|
return getScrollbackMax(scrollSurface);
|
|
307
830
|
};
|
|
831
|
+
const alignTerminalPrompt = () => {
|
|
832
|
+
const scrollSurface = getScrollSurface();
|
|
833
|
+
setProgrammaticScrollTop(scrollSurface, getTargetScrollTop(scrollSurface));
|
|
834
|
+
markProgrammaticScroll(scrollSurface);
|
|
835
|
+
alignLiveInputSurfaceToBottom(scrollSurface, shell, terminalRoot, windowObject);
|
|
836
|
+
markProgrammaticScroll(getScrollSurface());
|
|
837
|
+
alignLiveInputSurfaceIntoViewport(shell, terminalRoot, windowObject, scrollSurface);
|
|
838
|
+
syncAfterProgrammaticScroll();
|
|
839
|
+
syncPreservedArchiveVisibility();
|
|
840
|
+
};
|
|
841
|
+
const focusTerminalInput = ({ alignToPrompt = false } = {}) => {
|
|
842
|
+
syncTerminalInputAnchor();
|
|
843
|
+
if (alignToPrompt && resolveBrowserViewport(windowObject).keyboardInsetBottom <= 0) alignTerminalPrompt();
|
|
844
|
+
else if (alignToPrompt) scheduleMobileKeyboardPromptAlign({ force: true });
|
|
845
|
+
term?.focus?.();
|
|
846
|
+
};
|
|
847
|
+
const followBottomNow = (options = {}) => {
|
|
848
|
+
const scrollSurface = getScrollSurface();
|
|
849
|
+
setProgrammaticScrollTop(scrollSurface, getTargetScrollTop(scrollSurface, options));
|
|
850
|
+
if (options.preferTranscriptFollow) {
|
|
851
|
+
markProgrammaticScroll(scrollSurface);
|
|
852
|
+
alignLiveInputSurfaceToBottom(scrollSurface, shell, terminalRoot, windowObject);
|
|
853
|
+
markProgrammaticScroll(getScrollSurface());
|
|
854
|
+
alignLiveInputSurfaceIntoViewport(shell, terminalRoot, windowObject, scrollSurface);
|
|
855
|
+
}
|
|
856
|
+
if (options.sync !== false) syncAfterProgrammaticScroll();
|
|
857
|
+
};
|
|
308
858
|
const scheduleScrollbackSnapToBottom = (passes = 1, options = {}) => {
|
|
309
859
|
scrollbackSnapPassesRemaining = Math.max(scrollbackSnapPassesRemaining, passes);
|
|
310
860
|
if (scrollbackSnapHandle !== null) return;
|
|
311
861
|
const runSnap = () => {
|
|
312
862
|
scrollbackSnapHandle = requestFrame(() => {
|
|
313
863
|
scrollbackSnapHandle = null;
|
|
314
|
-
|
|
315
|
-
scrollSurface.scrollTop = getTargetScrollTop(scrollSurface, options);
|
|
316
|
-
requestScrollUiUpdate(dependencies.scroll);
|
|
317
|
-
publishScrollMetrics();
|
|
864
|
+
followBottomNow(options);
|
|
318
865
|
scrollbackSnapPassesRemaining = Math.max(0, scrollbackSnapPassesRemaining - 1);
|
|
319
866
|
if (scrollbackSnapPassesRemaining > 0) runSnap();
|
|
320
867
|
});
|
|
321
868
|
};
|
|
322
869
|
runSnap();
|
|
323
870
|
};
|
|
324
|
-
const
|
|
325
|
-
if (!
|
|
871
|
+
const followBottomAfterLayoutChange = () => {
|
|
872
|
+
if (!pendingLayoutBottomFollow) return;
|
|
873
|
+
followBottomNow({ preferTranscriptFollow: true });
|
|
874
|
+
scheduleScrollbackSnapToBottom(12, { preferTranscriptFollow: true });
|
|
875
|
+
clearLayoutBottomFollowTimers();
|
|
876
|
+
for (const delay of LAYOUT_BOTTOM_FOLLOW_DELAYS_MS) {
|
|
877
|
+
const timer = setTimeout(() => {
|
|
878
|
+
layoutBottomFollowTimers.delete(timer);
|
|
879
|
+
if (!pendingLayoutBottomFollow) return;
|
|
880
|
+
followBottomNow({ preferTranscriptFollow: true });
|
|
881
|
+
if (layoutBottomFollowTimers.size === 0) requestFrame(() => {
|
|
882
|
+
pendingLayoutBottomFollow = false;
|
|
883
|
+
});
|
|
884
|
+
}, delay);
|
|
885
|
+
layoutBottomFollowTimers.add(timer);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
const followBottomAfterOutputFlush = () => {
|
|
889
|
+
if (!shouldKeepBottomFollow()) return;
|
|
890
|
+
if (terminalInputFocused && resolveBrowserViewport(windowObject).keyboardInsetBottom > 0 && hasRecentMobileKeyboardManualScrollIntent()) {
|
|
891
|
+
stopBottomFollow();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
followBottomNow({ preferTranscriptFollow: true });
|
|
895
|
+
};
|
|
896
|
+
const settleBottomAfterOutputDrain = () => {
|
|
897
|
+
if (!shouldKeepBottomFollow()) return;
|
|
898
|
+
if (terminalInputFocused && resolveBrowserViewport(windowObject).keyboardInsetBottom > 0 && hasRecentMobileKeyboardManualScrollIntent()) {
|
|
899
|
+
stopBottomFollow();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
followBottomNow({ preferTranscriptFollow: true });
|
|
326
903
|
scheduleScrollbackSnapToBottom(2, { preferTranscriptFollow: true });
|
|
327
904
|
};
|
|
905
|
+
const scheduleBottomSettleAfterOutputDrain = () => {
|
|
906
|
+
if (!shouldKeepBottomFollow()) return;
|
|
907
|
+
clearOutputDrainSettleTimer();
|
|
908
|
+
outputDrainSettleTimer = setTimeout(() => {
|
|
909
|
+
outputDrainSettleTimer = null;
|
|
910
|
+
settleBottomAfterOutputDrain();
|
|
911
|
+
}, OUTPUT_DRAIN_SETTLE_DELAY_MS);
|
|
912
|
+
};
|
|
913
|
+
const isScrollSurfaceAtBottom = () => {
|
|
914
|
+
const scrollSurface = getScrollSurface();
|
|
915
|
+
return isAtScrollbackBottom(scrollSurface.scrollTop, getScrollbackMax(scrollSurface));
|
|
916
|
+
};
|
|
917
|
+
const isScrollSurfaceKnownAtBottom = () => lastScrollMetrics?.atBottom ?? isScrollSurfaceAtBottom();
|
|
328
918
|
const syncPreservedArchiveVisibility = ({ allowReveal = false, scrollTop = getScrollSurface().scrollTop, maxScrollTop = getScrollbackMax(getScrollSurface()) } = {}) => {
|
|
329
919
|
if (!transcriptArchive) return;
|
|
330
920
|
if (isAtScrollbackBottom(scrollTop, maxScrollTop)) {
|
|
@@ -336,9 +926,8 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
336
926
|
const disposeHotkeys = installScrollbackInteractions(terminalRoot, {
|
|
337
927
|
getScrollSurface,
|
|
338
928
|
setScrollTop(scrollSurface, scrollTop) {
|
|
339
|
-
scrollSurface
|
|
340
|
-
|
|
341
|
-
publishScrollMetrics();
|
|
929
|
+
setProgrammaticScrollTop(scrollSurface, scrollTop);
|
|
930
|
+
syncAfterProgrammaticScroll();
|
|
342
931
|
},
|
|
343
932
|
onInput({ event }) {
|
|
344
933
|
const preserveVisiblePrompt = shouldPreserveVisiblePromptOnInput(event) && isLiveInputSurfaceVisible(shell, terminalRoot, windowObject);
|
|
@@ -348,29 +937,38 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
348
937
|
cancelScrollbackSnap();
|
|
349
938
|
if (!preserveVisiblePrompt) {
|
|
350
939
|
const scrollSurface = getScrollSurface();
|
|
351
|
-
scrollSurface
|
|
352
|
-
|
|
353
|
-
publishScrollMetrics();
|
|
940
|
+
setProgrammaticScrollTop(scrollSurface, getTargetScrollTop(scrollSurface));
|
|
941
|
+
syncAfterProgrammaticScroll();
|
|
354
942
|
}
|
|
355
943
|
syncPreservedArchiveVisibility();
|
|
356
944
|
},
|
|
357
|
-
onManualScroll({ scrollTop, maxScrollTop }) {
|
|
945
|
+
onManualScroll({ scrollTop, maxScrollTop, source }) {
|
|
946
|
+
noteMobileKeyboardManualScrollIntent();
|
|
358
947
|
syncPreservedArchiveVisibility({
|
|
359
948
|
allowReveal: true,
|
|
360
949
|
scrollTop,
|
|
361
950
|
maxScrollTop
|
|
362
951
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
cancelScrollbackSnap();
|
|
952
|
+
stopBottomFollow();
|
|
953
|
+
if (source) shell.dataset.lastManualScrollSource = source;
|
|
366
954
|
requestScrollUiUpdate(dependencies.scroll);
|
|
367
955
|
publishScrollMetrics();
|
|
368
956
|
}
|
|
369
957
|
});
|
|
958
|
+
disposeTerminalFocusInteractions = installTerminalFocusInteractions(shell, terminalRoot, {
|
|
959
|
+
focusTerminal({ alignToPrompt = false } = {}) {
|
|
960
|
+
focusTerminalInput({ alignToPrompt });
|
|
961
|
+
},
|
|
962
|
+
resolveKeyData(event) {
|
|
963
|
+
return resolveTerminalDocumentKeyData(term, event);
|
|
964
|
+
},
|
|
965
|
+
sendData(data) {
|
|
966
|
+
sendUserInput(data);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
370
969
|
const resetTerminalBuffer = () => {
|
|
371
970
|
if (!term) return;
|
|
372
971
|
writer?.discardPending();
|
|
373
|
-
archiveWriter?.discardPending();
|
|
374
972
|
transcriptArchive?.preserveSnapshot([]);
|
|
375
973
|
ansiStyleTracker.reset(term.cols, term.rows);
|
|
376
974
|
if (term.bridge && typeof term.bridge.init === "function") {
|
|
@@ -388,42 +986,139 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
388
986
|
if (!helloReceived || sessionState !== "running" || pendingInputFrames.length === 0) return;
|
|
389
987
|
for (const chunk of pendingInputFrames.splice(0)) transport?.send(chunk);
|
|
390
988
|
};
|
|
989
|
+
const sendTerminalFocusReport = (focused, { updateFocusState = true } = {}) => {
|
|
990
|
+
if (updateFocusState) terminalInputFocused = focused;
|
|
991
|
+
if (!terminalFocusReportingEnabled) return;
|
|
992
|
+
sendInputToPty(focused ? TERMINAL_FOCUS_IN : "\x1B[O");
|
|
993
|
+
};
|
|
994
|
+
const syncTerminalFocusReportingMode = (data) => {
|
|
995
|
+
const nextState = focusReportingParser.append(data);
|
|
996
|
+
if (nextState === null) return;
|
|
997
|
+
const changed = terminalFocusReportingEnabled !== nextState;
|
|
998
|
+
terminalFocusReportingEnabled = nextState;
|
|
999
|
+
if (changed && nextState && terminalInputFocused) sendInputToPty(TERMINAL_FOCUS_IN);
|
|
1000
|
+
};
|
|
1001
|
+
const notifyTerminalThemeChanged = () => {
|
|
1002
|
+
renderTerminalNow(term, { force: true });
|
|
1003
|
+
if (terminalInputFocused) sendTerminalFocusReport(true, { updateFocusState: false });
|
|
1004
|
+
};
|
|
1005
|
+
const applyTheme = (theme) => {
|
|
1006
|
+
const changed = themeController.setTheme(theme);
|
|
1007
|
+
if (changed) notifyTerminalThemeChanged();
|
|
1008
|
+
return changed;
|
|
1009
|
+
};
|
|
1010
|
+
const getConfigSnapshot = () => cloneTerminalConfig(terminalConfig);
|
|
1011
|
+
const updateTerminalConfig = (patch) => {
|
|
1012
|
+
const nextConfig = mergeTerminalConfig(terminalConfig, patch);
|
|
1013
|
+
const previousAppearance = terminalConfig.appearance;
|
|
1014
|
+
const nextAppearance = nextConfig.appearance;
|
|
1015
|
+
const appearanceChanged = !areTerminalAppearancesEqual(previousAppearance, nextAppearance);
|
|
1016
|
+
if (!appearanceChanged && areTerminalBehaviorsEqual(terminalConfig.behavior, nextConfig.behavior)) return getConfigSnapshot();
|
|
1017
|
+
const shouldRelayout = hasTerminalLayoutAppearanceChange(previousAppearance, nextAppearance);
|
|
1018
|
+
const scrollAnchor = shouldRelayout ? captureScrollSurfaceAnchor(getScrollSurface(), shell) : null;
|
|
1019
|
+
terminalConfig = nextConfig;
|
|
1020
|
+
if (appearanceChanged) {
|
|
1021
|
+
applyTerminalAppearance(shell, terminalRoot, nextAppearance, previousAppearance);
|
|
1022
|
+
if (previousAppearance.theme !== nextAppearance.theme) applyTheme(nextAppearance.theme);
|
|
1023
|
+
else renderTerminalNow(term, { force: true });
|
|
1024
|
+
}
|
|
1025
|
+
if (shouldRelayout) {
|
|
1026
|
+
resizeTerminalToViewport();
|
|
1027
|
+
renderTerminalNow(term, { force: true });
|
|
1028
|
+
syncTerminalPresentation();
|
|
1029
|
+
if (scrollAnchor) scheduleScrollAnchorRestore(scrollAnchor);
|
|
1030
|
+
}
|
|
1031
|
+
const snapshot = getConfigSnapshot();
|
|
1032
|
+
dependencies.onConfigChange?.(snapshot);
|
|
1033
|
+
return snapshot;
|
|
1034
|
+
};
|
|
391
1035
|
const processVisiblePayload = (data) => {
|
|
1036
|
+
clearOutputDrainSettleTimer();
|
|
1037
|
+
noteOutputPressure(data.length);
|
|
392
1038
|
const firstOutput = markOutputReady();
|
|
1039
|
+
const shouldFollowOutput = isScrollSurfaceKnownAtBottom() && !hasRecentMobileKeyboardManualScrollIntent();
|
|
1040
|
+
syncTerminalFocusReportingMode(data);
|
|
393
1041
|
const themeUpdate = themeProtocolParser.append(data);
|
|
394
1042
|
let themeChanged = false;
|
|
395
|
-
if (themeUpdate)
|
|
1043
|
+
if (themeUpdate) {
|
|
1044
|
+
themeChanged = themeController.syncProtocolUpdate(themeUpdate);
|
|
1045
|
+
if (themeUpdate.theme && normalizeThemeName(themeUpdate.theme) !== terminalConfig.appearance.theme) {
|
|
1046
|
+
terminalConfig = mergeTerminalConfig(terminalConfig, { appearance: { theme: themeUpdate.theme } });
|
|
1047
|
+
dependencies.onConfigChange?.(getConfigSnapshot());
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
396
1050
|
const themeQueryResponse = resolveTerminalThemeQueryResponse(data, {
|
|
397
1051
|
root: terminalRoot,
|
|
398
1052
|
theme: themeController.getTheme()
|
|
399
1053
|
});
|
|
400
1054
|
if (themeQueryResponse) sendInputToPty(themeQueryResponse);
|
|
401
|
-
if (!termReady || !writer
|
|
1055
|
+
if (!termReady || !writer) {
|
|
402
1056
|
pendingFrames.push(data);
|
|
403
|
-
|
|
1057
|
+
pendingInitialBottomFollow = pendingInitialBottomFollow || firstOutput;
|
|
1058
|
+
if (themeChanged) notifyTerminalThemeChanged();
|
|
404
1059
|
return;
|
|
405
1060
|
}
|
|
406
1061
|
writer.enqueue(data);
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
1062
|
+
if (themeChanged) notifyTerminalThemeChanged();
|
|
1063
|
+
if (followLiveOutput || shouldFollowOutput) {
|
|
1064
|
+
const keyboardOpen = terminalInputFocused && resolveBrowserViewport(windowObject).keyboardInsetBottom > 0;
|
|
1065
|
+
if (keyboardOpen && hasRecentMobileKeyboardManualScrollIntent()) {
|
|
1066
|
+
stopBottomFollow();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (keyboardOpen && !followLiveOutput) {
|
|
1070
|
+
snapScrollbackOnNextWrite = false;
|
|
1071
|
+
pendingInitialBottomFollow = pendingInitialBottomFollow || firstOutput;
|
|
1072
|
+
grantMobileKeyboardPromptAlign(280);
|
|
1073
|
+
requestMobileKeyboardPromptAlign({
|
|
1074
|
+
force: true,
|
|
1075
|
+
onlyIfNeeded: true
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
411
1079
|
snapScrollbackOnNextWrite = false;
|
|
412
|
-
|
|
1080
|
+
pendingInitialBottomFollow = pendingInitialBottomFollow || firstOutput || shouldFollowOutput;
|
|
413
1081
|
} else if (snapScrollbackOnNextWrite || firstOutput) {
|
|
414
1082
|
snapScrollbackOnNextWrite = false;
|
|
415
|
-
|
|
1083
|
+
pendingInitialBottomFollow = pendingInitialBottomFollow || firstOutput;
|
|
416
1084
|
}
|
|
417
1085
|
};
|
|
418
1086
|
const sendInputToPty = (data) => {
|
|
419
|
-
if (sessionState !== "running") return;
|
|
1087
|
+
if (sessionState !== "running" || clientRole !== "controller") return;
|
|
420
1088
|
if (!helloReceived) {
|
|
421
1089
|
pendingInputFrames.push(data);
|
|
422
1090
|
return;
|
|
423
1091
|
}
|
|
424
1092
|
transport?.send(data);
|
|
425
1093
|
};
|
|
426
|
-
const sendUserInput = (data) => {
|
|
1094
|
+
const sendUserInput = (data, options = {}) => {
|
|
1095
|
+
if (clientRole !== "controller") {
|
|
1096
|
+
syncTerminalPresentation();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (options.focus === false) {
|
|
1100
|
+
sendInputToPty(data);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const shouldFollowLiveOutput = shouldFollowLiveOutputOnUserInputData(data);
|
|
1104
|
+
const keyboardOpen = resolveBrowserViewport(windowObject).keyboardInsetBottom > 0;
|
|
1105
|
+
grantUserInputLayoutStick();
|
|
1106
|
+
focusTerminalInput({ alignToPrompt: shouldFollowLiveOutput || !keyboardOpen });
|
|
1107
|
+
if (shouldFollowLiveOutput) {
|
|
1108
|
+
followLiveOutput = true;
|
|
1109
|
+
pendingInitialBottomFollow = true;
|
|
1110
|
+
snapScrollbackOnNextWrite = true;
|
|
1111
|
+
cancelScrollbackSnap();
|
|
1112
|
+
scheduleMobileKeyboardPromptAlign({ force: true });
|
|
1113
|
+
followBottomNow({ preferTranscriptFollow: true });
|
|
1114
|
+
scheduleScrollbackSnapToBottom(2, { preferTranscriptFollow: true });
|
|
1115
|
+
} else {
|
|
1116
|
+
grantMobileKeyboardPromptAlign(280);
|
|
1117
|
+
requestMobileKeyboardPromptAlign({
|
|
1118
|
+
force: true,
|
|
1119
|
+
onlyIfNeeded: true
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
427
1122
|
sendInputToPty(data);
|
|
428
1123
|
};
|
|
429
1124
|
const emitResize = (cols, rows) => {
|
|
@@ -433,10 +1128,11 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
433
1128
|
syncDimensions(terminalRoot, normalizedCols, normalizedRows);
|
|
434
1129
|
transcriptArchive?.refresh();
|
|
435
1130
|
syncTerminalPresentation();
|
|
1131
|
+
followBottomAfterLayoutChange();
|
|
436
1132
|
const signature = `${normalizedCols}x${normalizedRows}`;
|
|
437
1133
|
if (lastResizeSignature === signature) return;
|
|
438
1134
|
lastResizeSignature = signature;
|
|
439
|
-
if (sessionState !== "running") return;
|
|
1135
|
+
if (sessionState !== "running" || clientRole !== "controller") return;
|
|
440
1136
|
transport?.sendControl?.(createResizeControlFrame(normalizedCols, normalizedRows));
|
|
441
1137
|
};
|
|
442
1138
|
const syncTermDimensionsFromBridge = (requestedCols = term?.cols ?? 80, requestedRows = term?.rows ?? 24) => {
|
|
@@ -450,7 +1146,7 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
450
1146
|
if (term.renderer && typeof term.renderer.setup === "function" && (term.renderer.cols !== dimensions.cols || term.renderer.rows !== dimensions.rows)) term.renderer.setup(dimensions.cols, dimensions.rows);
|
|
451
1147
|
return dimensions;
|
|
452
1148
|
};
|
|
453
|
-
const resizeTerminalTo = (cols, rows) => {
|
|
1149
|
+
const resizeTerminalTo = (cols, rows, options = {}) => {
|
|
454
1150
|
if (!term) return;
|
|
455
1151
|
const requestedCols = normalizeResizeDimension(cols, term.cols);
|
|
456
1152
|
const requestedRows = normalizeResizeDimension(rows, term.rows);
|
|
@@ -464,16 +1160,55 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
464
1160
|
}
|
|
465
1161
|
const actual = syncTermDimensionsFromBridge(requestedCols, requestedRows);
|
|
466
1162
|
applyTerminalElementHeight(terminalRoot, actual.rows);
|
|
467
|
-
|
|
1163
|
+
if (options.sendControl === true) {
|
|
1164
|
+
emitResize(actual.cols, actual.rows);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
ansiStyleTracker.resize(actual.cols, actual.rows);
|
|
1168
|
+
syncDimensions(terminalRoot, actual.cols, actual.rows);
|
|
1169
|
+
transcriptArchive?.refresh();
|
|
1170
|
+
syncTerminalPresentation();
|
|
1171
|
+
followBottomAfterLayoutChange();
|
|
1172
|
+
};
|
|
1173
|
+
const resizeTerminalToViewport = () => {
|
|
1174
|
+
if (!term) return;
|
|
1175
|
+
syncShellViewportVariables();
|
|
1176
|
+
if (clientRole !== "controller" && authoritativeTerminalSize) {
|
|
1177
|
+
syncTerminalToAuthoritativeSize(authoritativeTerminalSize);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const target = resolveViewportSize(term, terminalRoot, windowObject);
|
|
1181
|
+
resizeTerminalTo(target.cols, target.rows, { sendControl: clientRole === "controller" });
|
|
1182
|
+
};
|
|
1183
|
+
const handleViewportResize = (cols, rows) => {
|
|
1184
|
+
if (clientRole !== "controller") return;
|
|
1185
|
+
resizeTerminalTo(cols, rows, { sendControl: true });
|
|
1186
|
+
};
|
|
1187
|
+
const scheduleScrollAnchorRestore = (anchor, passes = 10) => {
|
|
1188
|
+
let remainingPasses = Math.max(1, passes);
|
|
1189
|
+
const restore = () => {
|
|
1190
|
+
requestFrame(() => {
|
|
1191
|
+
markProgrammaticScroll(getScrollSurface());
|
|
1192
|
+
restoreScrollSurfaceAnchor(getScrollSurface(), shell, anchor);
|
|
1193
|
+
syncAfterProgrammaticScroll();
|
|
1194
|
+
remainingPasses -= 1;
|
|
1195
|
+
if (remainingPasses > 0) restore();
|
|
1196
|
+
});
|
|
1197
|
+
};
|
|
1198
|
+
restore();
|
|
468
1199
|
};
|
|
469
1200
|
const handleTermResize = (cols, rows) => {
|
|
470
1201
|
if (suppressTermResizeCallback) return;
|
|
471
1202
|
const actual = syncTermDimensionsFromBridge(cols, rows);
|
|
472
|
-
emitResize(actual.cols, actual.rows);
|
|
1203
|
+
if (clientRole === "controller") emitResize(actual.cols, actual.rows);
|
|
473
1204
|
};
|
|
474
1205
|
const showReconnectState = (statusText) => {
|
|
475
1206
|
reconnecting = true;
|
|
476
1207
|
helloReceived = false;
|
|
1208
|
+
clientRole = null;
|
|
1209
|
+
clientId = null;
|
|
1210
|
+
takeoverPending = false;
|
|
1211
|
+
syncTakeoverControl();
|
|
477
1212
|
presentStatus({
|
|
478
1213
|
connection: "reconnecting",
|
|
479
1214
|
loading: {
|
|
@@ -522,22 +1257,31 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
522
1257
|
getCols() {
|
|
523
1258
|
return term?.cols ?? 80;
|
|
524
1259
|
},
|
|
1260
|
+
getScrollSurface,
|
|
525
1261
|
getVisibleRows() {
|
|
526
|
-
return term?.rows ?? 24;
|
|
1262
|
+
return (term?.renderer?.getRenderedScrollbackCount?.() ?? 0) + (term?.rows ?? 24);
|
|
527
1263
|
},
|
|
528
|
-
scrollbackLimit:
|
|
1264
|
+
scrollbackLimit: terminalConfig.behavior.scrollbackLimit
|
|
529
1265
|
});
|
|
530
|
-
archiveWriter = createBufferedTerminalWriter({ write(data) {
|
|
531
|
-
transcriptArchive?.append(data);
|
|
532
|
-
} }, frameScheduler);
|
|
533
1266
|
writer = createBufferedTerminalWriter({ write(data) {
|
|
534
1267
|
ansiStyleTracker.append(data);
|
|
535
|
-
|
|
1268
|
+
transcriptArchive?.append(data);
|
|
1269
|
+
term?.write?.(data, { scheduleRender: false });
|
|
536
1270
|
ansiStyleTracker.syncFromBridge(term?.bridge);
|
|
537
1271
|
const renderHints = ansiStyleTracker.consumeRenderHints();
|
|
538
1272
|
renderTerminalNow(term, { force: renderHints.clearScreen || renderHints.styleChanged });
|
|
539
1273
|
syncTerminalPresentation();
|
|
540
|
-
} }, frameScheduler
|
|
1274
|
+
} }, frameScheduler, {
|
|
1275
|
+
minFrameIntervalMs: DEFAULT_TERMINAL_OUTPUT_FRAME_INTERVAL_MS,
|
|
1276
|
+
resolveMinFrameIntervalMs({ pendingByteLength }) {
|
|
1277
|
+
return resolveTerminalOutputFrameIntervalMs({
|
|
1278
|
+
pendingByteLength,
|
|
1279
|
+
sustainedPressure: hasHighOutputPressure()
|
|
1280
|
+
});
|
|
1281
|
+
},
|
|
1282
|
+
onDrain: scheduleBottomSettleAfterOutputDrain,
|
|
1283
|
+
onFlush: followBottomAfterOutputFlush
|
|
1284
|
+
});
|
|
541
1285
|
transport = (dependencies.createTransport ?? ((handlers) => createBrowserTransport(handlers)))({
|
|
542
1286
|
onBinary(data) {
|
|
543
1287
|
if (!helloReceived) helloReceived = true;
|
|
@@ -576,7 +1320,12 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
576
1320
|
if (!message) return;
|
|
577
1321
|
if (message.type === "hello") {
|
|
578
1322
|
helloReceived = true;
|
|
1323
|
+
pendingInputFrames.length = 0;
|
|
579
1324
|
const replay = decodeHelloReplay(message.replay);
|
|
1325
|
+
syncTerminalToAuthoritativeSize({
|
|
1326
|
+
cols: message.cols,
|
|
1327
|
+
rows: message.rows
|
|
1328
|
+
});
|
|
580
1329
|
if (reconnecting && replay.length > 0) resetTerminalBuffer();
|
|
581
1330
|
if (replay.length > 0) processVisiblePayload(replay);
|
|
582
1331
|
cancelReconnect();
|
|
@@ -592,16 +1341,58 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
592
1341
|
transport?.sendControl?.(createPongControlFrame());
|
|
593
1342
|
return;
|
|
594
1343
|
}
|
|
1344
|
+
if (message.type === "resize") {
|
|
1345
|
+
const cols = normalizeResizeDimension(message.cols, term?.cols ?? 80);
|
|
1346
|
+
const rows = normalizeResizeDimension(message.rows, term?.rows ?? 24);
|
|
1347
|
+
if (clientRole !== "controller") syncTerminalToAuthoritativeSize({
|
|
1348
|
+
cols,
|
|
1349
|
+
rows
|
|
1350
|
+
});
|
|
1351
|
+
else authoritativeTerminalSize = {
|
|
1352
|
+
cols,
|
|
1353
|
+
rows
|
|
1354
|
+
};
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (isAittyRoleControlFrame(message)) {
|
|
1358
|
+
clientId = message.clientId;
|
|
1359
|
+
persistAittyClientId(windowObject, sessionUrl, clientId);
|
|
1360
|
+
clientRole = message.role;
|
|
1361
|
+
takeoverNotice?.syncClient(clientId, clientRole);
|
|
1362
|
+
takeoverPending = false;
|
|
1363
|
+
pendingInputFrames.length = 0;
|
|
1364
|
+
syncTakeoverControl();
|
|
1365
|
+
if (clientRole === "controller") {
|
|
1366
|
+
authoritativeTerminalSize = null;
|
|
1367
|
+
lastResizeSignature = null;
|
|
1368
|
+
resizeTerminalToViewport();
|
|
1369
|
+
} else if (authoritativeTerminalSize) syncTerminalToAuthoritativeSize(authoritativeTerminalSize);
|
|
1370
|
+
else resizeTerminalToViewport();
|
|
1371
|
+
markWaitingForOutput();
|
|
1372
|
+
syncTerminalPresentation();
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (isAittyTakeoverRequestControlFrame(message)) {
|
|
1376
|
+
if (clientRole !== "controller" || !message.requestId) return;
|
|
1377
|
+
takeoverNotice?.show({
|
|
1378
|
+
requestId: message.requestId,
|
|
1379
|
+
requesterId: message.requesterId
|
|
1380
|
+
});
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (isAittyTakeoverResultControlFrame(message)) {
|
|
1384
|
+
takeoverPending = false;
|
|
1385
|
+
if (!message.approved) presentStatus({ message: resolveTakeoverResultMessage(message.reason) });
|
|
1386
|
+
syncTerminalPresentation();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
595
1389
|
if (message.type === "theme") {
|
|
596
|
-
let themeChanged = false;
|
|
597
1390
|
if (message.theme === null) {
|
|
598
|
-
|
|
599
|
-
if (themeChanged) renderTerminalNow(term, { force: true });
|
|
1391
|
+
updateTerminalConfig({ appearance: { theme: void 0 } });
|
|
600
1392
|
return;
|
|
601
1393
|
}
|
|
602
1394
|
const theme = normalizeThemeName(message.theme);
|
|
603
|
-
if (theme)
|
|
604
|
-
if (themeChanged) renderTerminalNow(term, { force: true });
|
|
1395
|
+
if (theme) updateTerminalConfig({ appearance: { theme } });
|
|
605
1396
|
return;
|
|
606
1397
|
}
|
|
607
1398
|
if (message.type !== "exit") return;
|
|
@@ -650,29 +1441,85 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
650
1441
|
presentStatus({ connection: "open" });
|
|
651
1442
|
}
|
|
652
1443
|
});
|
|
1444
|
+
const disposeShellControls = dependencies.shellControls ? installShellControls({
|
|
1445
|
+
captureScrollAnchor() {
|
|
1446
|
+
return captureScrollSurfaceAnchor(getScrollSurface(), shell);
|
|
1447
|
+
},
|
|
1448
|
+
doc,
|
|
1449
|
+
focusTerminal() {
|
|
1450
|
+
focusTerminalInput();
|
|
1451
|
+
},
|
|
1452
|
+
getConfig: getConfigSnapshot,
|
|
1453
|
+
onInterrupt() {
|
|
1454
|
+
sendUserInput("");
|
|
1455
|
+
},
|
|
1456
|
+
onScrollSurfaceChange() {
|
|
1457
|
+
syncShellViewportVariables();
|
|
1458
|
+
syncTerminalInputAnchor();
|
|
1459
|
+
lastScrollMetrics = null;
|
|
1460
|
+
syncAfterProgrammaticScroll();
|
|
1461
|
+
refreshScrollMetricsObserver();
|
|
1462
|
+
},
|
|
1463
|
+
onStickToBottomChange(stickToBottom) {
|
|
1464
|
+
pendingLayoutBottomFollow = stickToBottom || hasRecentUserInputLayoutStick();
|
|
1465
|
+
if (pendingLayoutBottomFollow) followBottomAfterLayoutChange();
|
|
1466
|
+
},
|
|
1467
|
+
resetConfig: initialConfig,
|
|
1468
|
+
restoreScrollAnchor(anchor) {
|
|
1469
|
+
restoreScrollSurfaceAnchor(getScrollSurface(), shell, anchor);
|
|
1470
|
+
syncTerminalInputAnchor();
|
|
1471
|
+
},
|
|
1472
|
+
shell,
|
|
1473
|
+
updateConfig: updateTerminalConfig,
|
|
1474
|
+
windowObject
|
|
1475
|
+
}) : () => {};
|
|
653
1476
|
await term.init();
|
|
654
|
-
replaceTerminalInputHandler(term
|
|
1477
|
+
replaceTerminalInputHandler(term, {
|
|
1478
|
+
getScrollSurface,
|
|
1479
|
+
onBlur() {
|
|
1480
|
+
sendTerminalFocusReport(false);
|
|
1481
|
+
},
|
|
1482
|
+
onFocus() {
|
|
1483
|
+
sendTerminalFocusReport(true);
|
|
1484
|
+
syncTerminalInputAnchor();
|
|
1485
|
+
scheduleMobileKeyboardPromptAlign({ force: true });
|
|
1486
|
+
},
|
|
1487
|
+
windowObject
|
|
1488
|
+
});
|
|
1489
|
+
mobileComposer = shouldInstallMobileComposer(shell, windowObject) ? installMobileComposer(shell, terminalRoot, {
|
|
1490
|
+
focusTerminal() {
|
|
1491
|
+
focusTerminalInput({ alignToPrompt: true });
|
|
1492
|
+
},
|
|
1493
|
+
sendData(data) {
|
|
1494
|
+
sendUserInput(data, { focus: false });
|
|
1495
|
+
},
|
|
1496
|
+
windowObject
|
|
1497
|
+
}) : null;
|
|
655
1498
|
const disposeInputPolicies = installTerminalInputPolicies(term, {
|
|
656
1499
|
captureDocumentCtrlN: false,
|
|
657
1500
|
onData(data) {
|
|
658
1501
|
sendUserInput(data);
|
|
659
1502
|
},
|
|
660
|
-
|
|
1503
|
+
onPasteShortcut(event) {
|
|
1504
|
+
return pasteClipboardImageFromShortcut(windowObject, sessionUrl, event);
|
|
1505
|
+
},
|
|
1506
|
+
pasteShortcut: resolveBrowserPasteShortcut(windowObject),
|
|
1507
|
+
resolveKey: terminalConfig.behavior.input?.resolveKey
|
|
661
1508
|
});
|
|
662
|
-
installBrowserRenderer(term,
|
|
1509
|
+
installBrowserRenderer(term, { onScrollbackRowsDropped(rows) {
|
|
1510
|
+
transcriptArchive?.appendPreservedLines(rows);
|
|
1511
|
+
} });
|
|
663
1512
|
termReady = true;
|
|
664
1513
|
const initialSize = syncTermDimensionsFromBridge(term.cols, term.rows);
|
|
665
1514
|
ansiStyleTracker.reset(initialSize.cols, initialSize.rows);
|
|
666
|
-
disposeViewportSizer = installTerminalViewportSizer(term, terminalRoot, windowObject,
|
|
1515
|
+
disposeViewportSizer = installTerminalViewportSizer(term, terminalRoot, windowObject, handleViewportResize, () => clientRole === "controller" && resolveBrowserViewport(windowObject).keyboardInsetBottom <= 0);
|
|
667
1516
|
syncTermDimensionsFromBridge(term.cols, term.rows);
|
|
668
|
-
emitResize(term.cols, term.rows);
|
|
669
1517
|
syncTerminalPresentation();
|
|
670
|
-
term.focus();
|
|
671
|
-
|
|
672
|
-
publishScrollMetrics();
|
|
1518
|
+
if (!shouldAllowImeViewportAvoidance(windowObject)) term.focus();
|
|
1519
|
+
refreshScrollMetricsObserver();
|
|
673
1520
|
windowObject.addEventListener("offline", handleBrowserOffline);
|
|
674
1521
|
windowObject.addEventListener("online", handleBrowserOnline);
|
|
675
|
-
transport.connect(toWebSocketUrl(sessionUrl));
|
|
1522
|
+
transport.connect(toWebSocketUrl(sessionUrl, resolveStoredAittyClientId(windowObject, sessionUrl)));
|
|
676
1523
|
for (const frame of pendingFrames.splice(0)) writer.enqueue(frame);
|
|
677
1524
|
return {
|
|
678
1525
|
destroy() {
|
|
@@ -683,24 +1530,47 @@ async function mountTerminalApp(doc = document, dependencies = {}) {
|
|
|
683
1530
|
cancelReconnect();
|
|
684
1531
|
disposeViewportSizer?.();
|
|
685
1532
|
disposeViewportSizer = null;
|
|
1533
|
+
disposeViewportDebugOverlay?.();
|
|
1534
|
+
disposeViewportDebugOverlay = null;
|
|
1535
|
+
mobileComposer?.destroy();
|
|
1536
|
+
mobileComposer = null;
|
|
1537
|
+
clearMobileKeyboardPromptAlign();
|
|
1538
|
+
clearOutputDrainSettleTimer();
|
|
1539
|
+
clearLayoutBottomFollowTimers();
|
|
1540
|
+
if (mobileKeyboardAlignFrame !== null) {
|
|
1541
|
+
cancelFrame(mobileKeyboardAlignFrame);
|
|
1542
|
+
mobileKeyboardAlignFrame = null;
|
|
1543
|
+
}
|
|
1544
|
+
if (shellViewportVariablesFrame !== null) {
|
|
1545
|
+
cancelFrame(shellViewportVariablesFrame);
|
|
1546
|
+
shellViewportVariablesFrame = null;
|
|
1547
|
+
}
|
|
686
1548
|
disposeScrollMetricsObserver?.();
|
|
687
1549
|
disposeScrollMetricsObserver = null;
|
|
688
|
-
|
|
1550
|
+
disposeTerminalFocusInteractions?.();
|
|
1551
|
+
disposeTerminalFocusInteractions = null;
|
|
689
1552
|
transcriptArchive?.destroy();
|
|
690
1553
|
writer?.destroy();
|
|
1554
|
+
disposeShellControls();
|
|
691
1555
|
disposeHotkeys();
|
|
692
1556
|
disposeInputPolicies();
|
|
693
1557
|
themeController.destroy();
|
|
1558
|
+
takeoverControl?.removeEventListener("click", handleTakeoverClick);
|
|
1559
|
+
takeoverNotice?.destroy();
|
|
1560
|
+
windowObject.removeEventListener("resize", scheduleShellViewportVariableSync);
|
|
1561
|
+
windowObject.removeEventListener("touchmove", handleMobileKeyboardTouchScrollIntent);
|
|
1562
|
+
windowObject.visualViewport?.removeEventListener("resize", scheduleShellViewportVariableSync);
|
|
1563
|
+
windowObject.visualViewport?.removeEventListener("scroll", scheduleShellViewportVariableSync);
|
|
694
1564
|
windowObject.removeEventListener("offline", handleBrowserOffline);
|
|
695
1565
|
windowObject.removeEventListener("online", handleBrowserOnline);
|
|
696
1566
|
transport?.close();
|
|
697
1567
|
term?.destroy();
|
|
698
1568
|
},
|
|
1569
|
+
getConfig: getConfigSnapshot,
|
|
699
1570
|
getStatus: getStatusSnapshot,
|
|
700
|
-
getTheme: themeController.getTheme,
|
|
701
|
-
setTheme: themeController.setTheme,
|
|
702
1571
|
term,
|
|
703
|
-
transport
|
|
1572
|
+
transport,
|
|
1573
|
+
updateConfig: updateTerminalConfig
|
|
704
1574
|
};
|
|
705
1575
|
}
|
|
706
1576
|
function mountAitty(containerOrOptions = {}, maybeOptions = {}) {
|
|
@@ -727,7 +1597,13 @@ function defineAittyTerminalElement(registry = globalThis.customElements) {
|
|
|
727
1597
|
function createAittyTerminalElementConstructor() {
|
|
728
1598
|
return class AittyTerminalCustomElement extends HTMLElement {
|
|
729
1599
|
static get observedAttributes() {
|
|
730
|
-
return [
|
|
1600
|
+
return [
|
|
1601
|
+
"font-size",
|
|
1602
|
+
"line-height",
|
|
1603
|
+
"src",
|
|
1604
|
+
"theme",
|
|
1605
|
+
"theme-target"
|
|
1606
|
+
];
|
|
731
1607
|
}
|
|
732
1608
|
#mounted = null;
|
|
733
1609
|
#mountVersion = 0;
|
|
@@ -739,8 +1615,8 @@ function createAittyTerminalElementConstructor() {
|
|
|
739
1615
|
}
|
|
740
1616
|
attributeChangedCallback(name, oldValue, nextValue) {
|
|
741
1617
|
if (oldValue === nextValue || !this.isConnected) return;
|
|
742
|
-
if (name
|
|
743
|
-
this.#mounted.
|
|
1618
|
+
if (name !== "src" && this.#mounted) {
|
|
1619
|
+
this.#mounted.updateConfig(this.#readConfig());
|
|
744
1620
|
return;
|
|
745
1621
|
}
|
|
746
1622
|
this.#mount();
|
|
@@ -763,8 +1639,15 @@ function createAittyTerminalElementConstructor() {
|
|
|
763
1639
|
detail: status
|
|
764
1640
|
}));
|
|
765
1641
|
},
|
|
766
|
-
|
|
767
|
-
|
|
1642
|
+
onConfigChange: (config) => {
|
|
1643
|
+
this.dispatchEvent(new CustomEvent("aitty:configchange", {
|
|
1644
|
+
bubbles: true,
|
|
1645
|
+
composed: true,
|
|
1646
|
+
detail: config
|
|
1647
|
+
}));
|
|
1648
|
+
},
|
|
1649
|
+
config: this.#readConfig(),
|
|
1650
|
+
src: this.getAttribute("src") ?? void 0
|
|
768
1651
|
});
|
|
769
1652
|
if (!this.isConnected || this.#mountVersion !== mountVersion) {
|
|
770
1653
|
mounted.destroy();
|
|
@@ -784,6 +1667,14 @@ function createAittyTerminalElementConstructor() {
|
|
|
784
1667
|
}));
|
|
785
1668
|
}
|
|
786
1669
|
}
|
|
1670
|
+
#readConfig() {
|
|
1671
|
+
return { appearance: {
|
|
1672
|
+
fontSize: parseOptionalNumber(this.getAttribute("font-size") ?? void 0),
|
|
1673
|
+
lineHeight: parseOptionalNumber(this.getAttribute("line-height") ?? void 0),
|
|
1674
|
+
theme: normalizeThemeName(this.getAttribute("theme") ?? void 0),
|
|
1675
|
+
themeTarget: normalizeThemeTarget(this.getAttribute("theme-target") ?? void 0)
|
|
1676
|
+
} };
|
|
1677
|
+
}
|
|
787
1678
|
};
|
|
788
1679
|
}
|
|
789
1680
|
async function mountAittyInContainer(container, options) {
|
|
@@ -879,139 +1770,1038 @@ function createAittyEmbedView(doc, mount) {
|
|
|
879
1770
|
terminalRoot,
|
|
880
1771
|
viewport
|
|
881
1772
|
};
|
|
882
|
-
}
|
|
883
|
-
function replaceTerminalInputHandler(term) {
|
|
884
|
-
const previousInput = term.input;
|
|
885
|
-
if (typeof previousInput?.destroy !== "function") return;
|
|
886
|
-
previousInput.destroy();
|
|
887
|
-
const textarea = term.element.ownerDocument.createElement("textarea");
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1773
|
+
}
|
|
1774
|
+
function replaceTerminalInputHandler(term, options = {}) {
|
|
1775
|
+
const previousInput = term.input;
|
|
1776
|
+
if (typeof previousInput?.destroy !== "function") return;
|
|
1777
|
+
previousInput.destroy();
|
|
1778
|
+
const textarea = term.element.ownerDocument.createElement("textarea");
|
|
1779
|
+
const inputOwnerId = `aitty-input-${nextTerminalInputId += 1}`;
|
|
1780
|
+
configureTerminalTextarea(textarea);
|
|
1781
|
+
term.element.dataset.aittyInputOwner = inputOwnerId;
|
|
1782
|
+
textarea.dataset.aittyInputOwner = inputOwnerId;
|
|
1783
|
+
appendTerminalTextarea(term.element, textarea);
|
|
1784
|
+
const windowObject = options.windowObject ?? textarea.ownerDocument.defaultView ?? window;
|
|
1785
|
+
syncTerminalTextareaPosition(term, textarea, { allowViewportAvoidance: shouldAllowImeViewportAvoidance(windowObject) });
|
|
1786
|
+
let imeScrollLock = null;
|
|
1787
|
+
let imeRestoreFrame = null;
|
|
1788
|
+
let imeReleaseTimer = null;
|
|
1789
|
+
const syncInputPosition = () => {
|
|
1790
|
+
syncTerminalTextareaPosition(term, textarea, { allowViewportAvoidance: shouldAllowImeViewportAvoidance(windowObject) });
|
|
1791
|
+
};
|
|
1792
|
+
const clearImeReleaseTimer = () => {
|
|
1793
|
+
if (imeReleaseTimer === null) return;
|
|
1794
|
+
clearTimeout(imeReleaseTimer);
|
|
1795
|
+
imeReleaseTimer = null;
|
|
1796
|
+
};
|
|
1797
|
+
const restoreDesktopImeScroll = () => {
|
|
1798
|
+
const lock = imeScrollLock;
|
|
1799
|
+
if (!lock || shouldAllowImeViewportAvoidance(windowObject)) return;
|
|
1800
|
+
restoreImeScrollLock(lock);
|
|
1801
|
+
};
|
|
1802
|
+
const scheduleDesktopImeScrollRestore = () => {
|
|
1803
|
+
if (!imeScrollLock || shouldAllowImeViewportAvoidance(windowObject)) return;
|
|
1804
|
+
if (imeRestoreFrame !== null) windowObject.cancelAnimationFrame(imeRestoreFrame);
|
|
1805
|
+
imeRestoreFrame = windowObject.requestAnimationFrame(() => {
|
|
1806
|
+
imeRestoreFrame = null;
|
|
1807
|
+
restoreDesktopImeScroll();
|
|
1808
|
+
windowObject.setTimeout(restoreDesktopImeScroll, 0);
|
|
1809
|
+
});
|
|
1810
|
+
};
|
|
1811
|
+
const captureDesktopImeScroll = () => {
|
|
1812
|
+
clearImeReleaseTimer();
|
|
1813
|
+
if (shouldAllowImeViewportAvoidance(windowObject)) {
|
|
1814
|
+
imeScrollLock = null;
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
imeScrollLock = captureImeScrollLock(windowObject, options.getScrollSurface?.());
|
|
1818
|
+
};
|
|
1819
|
+
const releaseDesktopImeScroll = () => {
|
|
1820
|
+
clearImeReleaseTimer();
|
|
1821
|
+
scheduleDesktopImeScrollRestore();
|
|
1822
|
+
imeReleaseTimer = setTimeout(() => {
|
|
1823
|
+
restoreDesktopImeScroll();
|
|
1824
|
+
imeScrollLock = null;
|
|
1825
|
+
imeReleaseTimer = null;
|
|
1826
|
+
}, 120);
|
|
1827
|
+
};
|
|
1828
|
+
const syncInputPositionPreservingDesktopImeScroll = () => {
|
|
1829
|
+
syncInputPosition();
|
|
1830
|
+
scheduleDesktopImeScrollRestore();
|
|
1831
|
+
};
|
|
1832
|
+
const handleCompositionStart = () => {
|
|
1833
|
+
captureDesktopImeScroll();
|
|
1834
|
+
syncInputPositionPreservingDesktopImeScroll();
|
|
1835
|
+
};
|
|
1836
|
+
const handleCompositionUpdate = () => {
|
|
1837
|
+
syncInputPositionPreservingDesktopImeScroll();
|
|
1838
|
+
};
|
|
1839
|
+
const handleCompositionEnd = () => {
|
|
1840
|
+
syncInputPositionPreservingDesktopImeScroll();
|
|
1841
|
+
releaseDesktopImeScroll();
|
|
1842
|
+
};
|
|
1843
|
+
const onFocus = () => {
|
|
1844
|
+
syncInputPosition();
|
|
1845
|
+
term.element.classList.add("focused");
|
|
1846
|
+
options.onFocus?.();
|
|
1847
|
+
};
|
|
1848
|
+
const onBlur = () => {
|
|
1849
|
+
clearImeReleaseTimer();
|
|
1850
|
+
imeScrollLock = null;
|
|
1851
|
+
term.element.classList.remove("focused");
|
|
1852
|
+
options.onBlur?.();
|
|
1853
|
+
};
|
|
1854
|
+
textarea.addEventListener("focus", onFocus);
|
|
1855
|
+
textarea.addEventListener("blur", onBlur);
|
|
1856
|
+
textarea.addEventListener("beforeinput", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1857
|
+
textarea.addEventListener("compositionstart", handleCompositionStart, true);
|
|
1858
|
+
textarea.addEventListener("compositionupdate", handleCompositionUpdate, true);
|
|
1859
|
+
textarea.addEventListener("compositionend", handleCompositionEnd, true);
|
|
1860
|
+
textarea.addEventListener("input", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1861
|
+
textarea.addEventListener("keydown", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1862
|
+
term.input = {
|
|
1863
|
+
textarea,
|
|
1864
|
+
focus() {
|
|
1865
|
+
syncInputPosition();
|
|
1866
|
+
textarea.focus({ preventScroll: true });
|
|
1867
|
+
},
|
|
1868
|
+
setImeAnchor(anchor) {
|
|
1869
|
+
syncTerminalTextareaPosition(term, textarea, {
|
|
1870
|
+
allowViewportAvoidance: shouldAllowImeViewportAvoidance(windowObject),
|
|
1871
|
+
anchor
|
|
1872
|
+
});
|
|
1873
|
+
},
|
|
1874
|
+
destroy() {
|
|
1875
|
+
clearImeReleaseTimer();
|
|
1876
|
+
if (imeRestoreFrame !== null) {
|
|
1877
|
+
windowObject.cancelAnimationFrame(imeRestoreFrame);
|
|
1878
|
+
imeRestoreFrame = null;
|
|
1879
|
+
}
|
|
1880
|
+
textarea.removeEventListener("focus", onFocus);
|
|
1881
|
+
textarea.removeEventListener("blur", onBlur);
|
|
1882
|
+
textarea.removeEventListener("beforeinput", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1883
|
+
textarea.removeEventListener("compositionstart", handleCompositionStart, true);
|
|
1884
|
+
textarea.removeEventListener("compositionupdate", handleCompositionUpdate, true);
|
|
1885
|
+
textarea.removeEventListener("compositionend", handleCompositionEnd, true);
|
|
1886
|
+
textarea.removeEventListener("input", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1887
|
+
textarea.removeEventListener("keydown", syncInputPositionPreservingDesktopImeScroll, true);
|
|
1888
|
+
term.element.classList.remove("focused");
|
|
1889
|
+
delete term.element.dataset.aittyInputOwner;
|
|
1890
|
+
textarea.remove();
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
function appendTerminalTextarea(terminalRoot, textarea) {
|
|
1895
|
+
const body = terminalRoot.ownerDocument.body;
|
|
1896
|
+
if (body) {
|
|
1897
|
+
body.appendChild(textarea);
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
terminalRoot.appendChild(textarea);
|
|
1901
|
+
}
|
|
1902
|
+
function shouldInstallMobileComposer(shell, windowObject) {
|
|
1903
|
+
return shell.hasAttribute("data-aitty-mobile-composer") && isCoarseMobileViewport(windowObject);
|
|
1904
|
+
}
|
|
1905
|
+
function installMobileComposer(shell, terminalRoot, options) {
|
|
1906
|
+
const doc = shell.ownerDocument;
|
|
1907
|
+
const dock = doc.createElement("div");
|
|
1908
|
+
const toggleButton = doc.createElement("button");
|
|
1909
|
+
const controlList = doc.createElement("div");
|
|
1910
|
+
const directionPad = doc.createElement("div");
|
|
1911
|
+
let dockExpanded = false;
|
|
1912
|
+
let dockDragState = null;
|
|
1913
|
+
let dockPositionFrame = null;
|
|
1914
|
+
let pendingDockPosition = null;
|
|
1915
|
+
let directionModeActive = false;
|
|
1916
|
+
let toggleLongPressState = null;
|
|
1917
|
+
let suppressDockClickUntil = 0;
|
|
1918
|
+
let terminalTapFocusState = null;
|
|
1919
|
+
let composerEnabled = true;
|
|
1920
|
+
dock.className = "aitty-mobile-control-dock";
|
|
1921
|
+
dock.dataset.aittyMobileControlDock = "";
|
|
1922
|
+
dock.style.setProperty("--aitty-mobile-dock-x", "calc(100vw - 78px - env(safe-area-inset-right))");
|
|
1923
|
+
dock.style.setProperty("--aitty-mobile-dock-y", "calc(var(--shell-visual-viewport-offset-top, 0px) + var(--shell-visible-viewport-height, 100dvh) - 96px - env(safe-area-inset-bottom))");
|
|
1924
|
+
dock.setAttribute("aria-label", "Mobile terminal controls");
|
|
1925
|
+
toggleButton.className = "aitty-mobile-control-toggle";
|
|
1926
|
+
toggleButton.type = "button";
|
|
1927
|
+
toggleButton.setAttribute("aria-label", "Show terminal controls");
|
|
1928
|
+
toggleButton.setAttribute("aria-expanded", "false");
|
|
1929
|
+
toggleButton.innerHTML = "<span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span><span aria-hidden=\"true\"></span>";
|
|
1930
|
+
controlList.className = "aitty-mobile-control-list";
|
|
1931
|
+
for (const control of MOBILE_COMPOSER_PRIMARY_CONTROLS) {
|
|
1932
|
+
const button = createMobileComposerButton(doc, control, resolveMobileComposerControlLabel(control));
|
|
1933
|
+
if (control === "enter") button.classList.add("aitty-mobile-composer-enter");
|
|
1934
|
+
controlList.append(button);
|
|
1935
|
+
}
|
|
1936
|
+
directionPad.className = "aitty-mobile-direction-pad";
|
|
1937
|
+
directionPad.setAttribute("aria-label", "Arrow keys");
|
|
1938
|
+
directionPad.append(createMobileDirectionButton(doc, "arrow-up", "Up", "↑"), createMobileDirectionButton(doc, "arrow-right", "Right", "→"), createMobileDirectionButton(doc, "arrow-down", "Down", "↓"), createMobileDirectionButton(doc, "arrow-left", "Left", "←"));
|
|
1939
|
+
dock.append(controlList, directionPad, toggleButton);
|
|
1940
|
+
shell.append(dock);
|
|
1941
|
+
const applyDockPosition = (left, top) => {
|
|
1942
|
+
dock.style.setProperty("--aitty-mobile-dock-x", `${Math.round(left)}px`);
|
|
1943
|
+
dock.style.setProperty("--aitty-mobile-dock-y", `${Math.round(top)}px`);
|
|
1944
|
+
};
|
|
1945
|
+
const queueDockPosition = (left, top) => {
|
|
1946
|
+
pendingDockPosition = {
|
|
1947
|
+
left,
|
|
1948
|
+
top
|
|
1949
|
+
};
|
|
1950
|
+
if (dockPositionFrame !== null) return;
|
|
1951
|
+
dockPositionFrame = options.windowObject.requestAnimationFrame(() => {
|
|
1952
|
+
dockPositionFrame = null;
|
|
1953
|
+
if (!pendingDockPosition) return;
|
|
1954
|
+
const nextPosition = pendingDockPosition;
|
|
1955
|
+
pendingDockPosition = null;
|
|
1956
|
+
applyDockPosition(nextPosition.left, nextPosition.top);
|
|
1957
|
+
});
|
|
1958
|
+
};
|
|
1959
|
+
const flushDockPosition = () => {
|
|
1960
|
+
if (!pendingDockPosition) return;
|
|
1961
|
+
const nextPosition = pendingDockPosition;
|
|
1962
|
+
pendingDockPosition = null;
|
|
1963
|
+
if (dockPositionFrame !== null) {
|
|
1964
|
+
options.windowObject.cancelAnimationFrame(dockPositionFrame);
|
|
1965
|
+
dockPositionFrame = null;
|
|
1966
|
+
}
|
|
1967
|
+
applyDockPosition(nextPosition.left, nextPosition.top);
|
|
1968
|
+
};
|
|
1969
|
+
const syncDockExpandedState = () => {
|
|
1970
|
+
dock.hidden = !composerEnabled;
|
|
1971
|
+
dock.dataset.expanded = dockExpanded ? "true" : "false";
|
|
1972
|
+
dock.dataset.directionMode = directionModeActive ? "true" : "false";
|
|
1973
|
+
dock.dataset.enabled = composerEnabled ? "true" : "false";
|
|
1974
|
+
toggleButton.disabled = !composerEnabled;
|
|
1975
|
+
for (const button of dock.querySelectorAll("button")) button.disabled = !composerEnabled;
|
|
1976
|
+
toggleButton.setAttribute("aria-expanded", String(dockExpanded || directionModeActive));
|
|
1977
|
+
toggleButton.setAttribute("aria-label", directionModeActive ? "Hide arrow controls" : dockExpanded ? "Hide terminal controls" : "Show terminal controls");
|
|
1978
|
+
};
|
|
1979
|
+
const setDockExpanded = (expanded) => {
|
|
1980
|
+
if (!composerEnabled) return;
|
|
1981
|
+
dockExpanded = expanded;
|
|
1982
|
+
if (expanded) directionModeActive = false;
|
|
1983
|
+
syncDockExpandedState();
|
|
1984
|
+
keepActiveDockInsideViewport();
|
|
1985
|
+
scheduleDockViewportClamp();
|
|
1986
|
+
};
|
|
1987
|
+
const resolveVisibleDockBounds = () => {
|
|
1988
|
+
const viewport = resolveBrowserViewport(options.windowObject);
|
|
1989
|
+
const visibleHeight = Math.max(1, viewport.height - viewport.browserChromeInsetBottom);
|
|
1990
|
+
return {
|
|
1991
|
+
bottom: viewport.offsetTop + visibleHeight - MOBILE_COMPOSER_MARGIN_PX,
|
|
1992
|
+
left: MOBILE_COMPOSER_MARGIN_PX,
|
|
1993
|
+
right: viewport.width - MOBILE_COMPOSER_MARGIN_PX,
|
|
1994
|
+
top: viewport.offsetTop + MOBILE_COMPOSER_MARGIN_PX
|
|
1995
|
+
};
|
|
1996
|
+
};
|
|
1997
|
+
const resolveCurrentDockPosition = () => {
|
|
1998
|
+
flushDockPosition();
|
|
1999
|
+
const rect = dock.getBoundingClientRect();
|
|
2000
|
+
return {
|
|
2001
|
+
left: rect.left,
|
|
2002
|
+
top: rect.top
|
|
2003
|
+
};
|
|
2004
|
+
};
|
|
2005
|
+
const clampDockPosition = ({ dockHeight = MOBILE_COMPOSER_DOCK_SIZE_PX, dockWidth = MOBILE_COMPOSER_DOCK_SIZE_PX, minLeftOffset = 0, minTopOffset = 0, bottomOffset = 0, left, rightOffset = 0, top }) => {
|
|
2006
|
+
const bounds = resolveVisibleDockBounds();
|
|
2007
|
+
const minLeft = bounds.left + minLeftOffset;
|
|
2008
|
+
const minTop = bounds.top + minTopOffset;
|
|
2009
|
+
const maxLeft = Math.max(minLeft, bounds.right - dockWidth - rightOffset);
|
|
2010
|
+
const maxTop = Math.max(minTop, bounds.bottom - dockHeight - bottomOffset);
|
|
2011
|
+
return {
|
|
2012
|
+
left: clamp(left, minLeft, maxLeft),
|
|
2013
|
+
top: clamp(top, minTop, maxTop)
|
|
2014
|
+
};
|
|
2015
|
+
};
|
|
2016
|
+
const keepCollapsedDockInsideViewport = () => {
|
|
2017
|
+
const current = resolveCurrentDockPosition();
|
|
2018
|
+
const next = clampDockPosition(current);
|
|
2019
|
+
if (Math.abs(next.left - current.left) >= 1 || Math.abs(next.top - current.top) >= 1) applyDockPosition(next.left, next.top);
|
|
2020
|
+
};
|
|
2021
|
+
const keepExpandedControlsInsideViewport = () => {
|
|
2022
|
+
const current = resolveCurrentDockPosition();
|
|
2023
|
+
const listHeight = resolveMobileComposerExpandedListHeight();
|
|
2024
|
+
const next = clampDockPosition({
|
|
2025
|
+
...current,
|
|
2026
|
+
minTopOffset: listHeight + MOBILE_COMPOSER_EXPANDED_GAP_PX
|
|
2027
|
+
});
|
|
2028
|
+
if (Math.abs(next.left - current.left) >= 1 || Math.abs(next.top - current.top) >= 1) applyDockPosition(next.left, next.top);
|
|
2029
|
+
};
|
|
2030
|
+
const keepDirectionPadInsideViewport = () => {
|
|
2031
|
+
const current = resolveCurrentDockPosition();
|
|
2032
|
+
const next = clampDockPosition({
|
|
2033
|
+
...current,
|
|
2034
|
+
bottomOffset: MOBILE_COMPOSER_DIRECTION_BOTTOM_OUTSET_PX,
|
|
2035
|
+
minLeftOffset: MOBILE_COMPOSER_DIRECTION_OUTSET_PX,
|
|
2036
|
+
minTopOffset: MOBILE_COMPOSER_DIRECTION_OUTSET_PX,
|
|
2037
|
+
rightOffset: MOBILE_COMPOSER_DIRECTION_OUTSET_PX
|
|
2038
|
+
});
|
|
2039
|
+
if (Math.abs(next.left - current.left) >= 1 || Math.abs(next.top - current.top) >= 1) applyDockPosition(next.left, next.top);
|
|
2040
|
+
};
|
|
2041
|
+
const keepActiveDockInsideViewport = () => {
|
|
2042
|
+
if (directionModeActive) {
|
|
2043
|
+
keepDirectionPadInsideViewport();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (dockExpanded) {
|
|
2047
|
+
keepExpandedControlsInsideViewport();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
keepCollapsedDockInsideViewport();
|
|
2051
|
+
};
|
|
2052
|
+
const scheduleDockViewportClamp = () => {
|
|
2053
|
+
if (dockPositionFrame !== null) return;
|
|
2054
|
+
dockPositionFrame = options.windowObject.requestAnimationFrame(() => {
|
|
2055
|
+
dockPositionFrame = null;
|
|
2056
|
+
keepActiveDockInsideViewport();
|
|
2057
|
+
});
|
|
2058
|
+
};
|
|
2059
|
+
const setDirectionModeActive = (active) => {
|
|
2060
|
+
if (!composerEnabled) return;
|
|
2061
|
+
directionModeActive = active;
|
|
2062
|
+
if (active) dockExpanded = false;
|
|
2063
|
+
syncDockExpandedState();
|
|
2064
|
+
keepActiveDockInsideViewport();
|
|
2065
|
+
};
|
|
2066
|
+
const open = () => {
|
|
2067
|
+
if (!composerEnabled || terminalRoot.dataset.sessionInteractive === "false") return;
|
|
2068
|
+
options.focusTerminal();
|
|
2069
|
+
};
|
|
2070
|
+
syncDockExpandedState();
|
|
2071
|
+
const sendControl = (control) => {
|
|
2072
|
+
if (!composerEnabled) return;
|
|
2073
|
+
const hadTerminalFocus = isElementInside(doc.activeElement, terminalRoot);
|
|
2074
|
+
options.sendData(resolveMobileComposerControlSequence(control));
|
|
2075
|
+
if (!hadTerminalFocus && doc.activeElement instanceof HTMLElement && isElementInside(doc.activeElement, terminalRoot)) doc.activeElement.blur();
|
|
2076
|
+
};
|
|
2077
|
+
const activateControl = (control) => {
|
|
2078
|
+
if (isMobileComposerDirectionControl(control)) setDirectionModeActive(true);
|
|
2079
|
+
else setDockExpanded(false);
|
|
2080
|
+
sendControl(control);
|
|
2081
|
+
};
|
|
2082
|
+
const activateToggle = () => {
|
|
2083
|
+
if (!composerEnabled) return;
|
|
2084
|
+
if (directionModeActive) {
|
|
2085
|
+
setDirectionModeActive(false);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
setDockExpanded(!dockExpanded);
|
|
2089
|
+
};
|
|
2090
|
+
const resolvePointerAction = (target) => {
|
|
2091
|
+
if (!(target instanceof Element)) return { kind: "none" };
|
|
2092
|
+
if (target.closest(".aitty-mobile-control-toggle") === toggleButton) return { kind: "toggle" };
|
|
2093
|
+
const controlButton = target.closest("[data-aitty-mobile-composer-control]");
|
|
2094
|
+
if (!(controlButton instanceof HTMLButtonElement)) return { kind: "none" };
|
|
2095
|
+
const control = controlButton.dataset.aittyMobileComposerControl;
|
|
2096
|
+
return control ? {
|
|
2097
|
+
kind: "control",
|
|
2098
|
+
control
|
|
2099
|
+
} : { kind: "none" };
|
|
2100
|
+
};
|
|
2101
|
+
const clearToggleLongPress = () => {
|
|
2102
|
+
if (!toggleLongPressState) return;
|
|
2103
|
+
options.windowObject.clearTimeout(toggleLongPressState.timer);
|
|
2104
|
+
toggleLongPressState = null;
|
|
2105
|
+
};
|
|
2106
|
+
const startToggleLongPress = (event) => {
|
|
2107
|
+
if (directionModeActive || !(event.target instanceof Element) || !event.target.closest(".aitty-mobile-control-toggle")) return;
|
|
2108
|
+
clearToggleLongPress();
|
|
2109
|
+
const pointerId = event.pointerId;
|
|
2110
|
+
const timer = options.windowObject.setTimeout(() => {
|
|
2111
|
+
if (!toggleLongPressState || toggleLongPressState.pointerId !== pointerId) return;
|
|
2112
|
+
toggleLongPressState.triggered = true;
|
|
2113
|
+
setDirectionModeActive(true);
|
|
2114
|
+
}, MOBILE_COMPOSER_DIRECTION_LONG_PRESS_MS);
|
|
2115
|
+
toggleLongPressState = {
|
|
2116
|
+
pointerId,
|
|
2117
|
+
startX: event.clientX,
|
|
2118
|
+
startY: event.clientY,
|
|
2119
|
+
timer,
|
|
2120
|
+
triggered: false
|
|
2121
|
+
};
|
|
2122
|
+
};
|
|
2123
|
+
const cancelToggleLongPressOnMove = (event) => {
|
|
2124
|
+
if (!toggleLongPressState || toggleLongPressState.pointerId !== event.pointerId) return;
|
|
2125
|
+
if (Math.hypot(event.clientX - toggleLongPressState.startX, event.clientY - toggleLongPressState.startY) >= MOBILE_COMPOSER_TAP_SLOP_PX) clearToggleLongPress();
|
|
2126
|
+
};
|
|
2127
|
+
const onShellPointerDown = (event) => {
|
|
2128
|
+
if (event.defaultPrevented || !isCoarseMobileViewport(options.windowObject)) return;
|
|
2129
|
+
const target = event.target;
|
|
2130
|
+
if (target instanceof Element && target.closest("[data-shell-control], .aitty-mobile-control-dock")) return;
|
|
2131
|
+
if (!isElementInside(target, terminalRoot) && !isElementInside(target, shell)) {
|
|
2132
|
+
terminalTapFocusState = null;
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
if (hasTerminalSelection(terminalRoot) || !isMobileLiveInputTapTarget(terminalRoot, options.windowObject, event.clientX, event.clientY)) {
|
|
2136
|
+
terminalTapFocusState = null;
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
terminalTapFocusState = {
|
|
2140
|
+
pointerId: event.pointerId,
|
|
2141
|
+
startTime: options.windowObject.performance.now(),
|
|
2142
|
+
startX: event.clientX,
|
|
2143
|
+
startY: event.clientY
|
|
2144
|
+
};
|
|
2145
|
+
};
|
|
2146
|
+
const onShellPointerMove = (event) => {
|
|
2147
|
+
if (!terminalTapFocusState || terminalTapFocusState.pointerId !== event.pointerId) return;
|
|
2148
|
+
if (Math.hypot(event.clientX - terminalTapFocusState.startX, event.clientY - terminalTapFocusState.startY) >= 8) terminalTapFocusState = null;
|
|
2149
|
+
};
|
|
2150
|
+
const onShellPointerUp = (event) => {
|
|
2151
|
+
if (!terminalTapFocusState || terminalTapFocusState.pointerId !== event.pointerId) return;
|
|
2152
|
+
const tapState = terminalTapFocusState;
|
|
2153
|
+
terminalTapFocusState = null;
|
|
2154
|
+
if (Math.hypot(event.clientX - tapState.startX, event.clientY - tapState.startY) >= 8) return;
|
|
2155
|
+
const pressedForMs = options.windowObject.performance.now() - tapState.startTime;
|
|
2156
|
+
const isLiveInputTarget = isMobileLiveInputTapTarget(terminalRoot, options.windowObject, event.clientX, event.clientY);
|
|
2157
|
+
if (hasTerminalSelection(terminalRoot) || !isLiveInputTarget) return;
|
|
2158
|
+
if (pressedForMs >= MOBILE_COMPOSER_DIRECTION_LONG_PRESS_MS) return;
|
|
2159
|
+
options.focusTerminal();
|
|
2160
|
+
};
|
|
2161
|
+
const onShellPointerCancel = (event) => {
|
|
2162
|
+
if (terminalTapFocusState?.pointerId === event.pointerId) terminalTapFocusState = null;
|
|
2163
|
+
};
|
|
2164
|
+
const onControlClick = (event) => {
|
|
2165
|
+
event.preventDefault();
|
|
2166
|
+
if (suppressDockClickUntil > options.windowObject.performance.now()) return;
|
|
2167
|
+
const target = event.target;
|
|
2168
|
+
if (!(target instanceof HTMLButtonElement)) return;
|
|
2169
|
+
if (dockDragState?.moved) return;
|
|
2170
|
+
const control = target.dataset.aittyMobileComposerControl;
|
|
2171
|
+
if (!control) return;
|
|
2172
|
+
activateControl(control);
|
|
2173
|
+
};
|
|
2174
|
+
const onToggleClick = (event) => {
|
|
2175
|
+
event.preventDefault();
|
|
2176
|
+
if (suppressDockClickUntil > options.windowObject.performance.now()) return;
|
|
2177
|
+
if (dockDragState?.moved) return;
|
|
2178
|
+
activateToggle();
|
|
2179
|
+
};
|
|
2180
|
+
const onDockPointerDown = (event) => {
|
|
2181
|
+
if (!composerEnabled || event.button > 0 || !isCoarseMobileViewport(options.windowObject)) return;
|
|
2182
|
+
event.preventDefault();
|
|
2183
|
+
flushDockPosition();
|
|
2184
|
+
const rect = dock.getBoundingClientRect();
|
|
2185
|
+
dockDragState = {
|
|
2186
|
+
action: resolvePointerAction(event.target),
|
|
2187
|
+
dockHeight: rect.height || MOBILE_COMPOSER_DOCK_SIZE_PX,
|
|
2188
|
+
dockWidth: rect.width || MOBILE_COMPOSER_DOCK_SIZE_PX,
|
|
2189
|
+
moved: false,
|
|
2190
|
+
originLeft: rect.left,
|
|
2191
|
+
originTop: rect.top,
|
|
2192
|
+
pointerId: event.pointerId,
|
|
2193
|
+
startX: event.clientX,
|
|
2194
|
+
startY: event.clientY
|
|
2195
|
+
};
|
|
2196
|
+
startToggleLongPress(event);
|
|
2197
|
+
try {
|
|
2198
|
+
dock.setPointerCapture?.(event.pointerId);
|
|
2199
|
+
} catch {}
|
|
2200
|
+
dock.dataset.dragging = "true";
|
|
2201
|
+
};
|
|
2202
|
+
const onDockPointerMove = (event) => {
|
|
2203
|
+
if (!dockDragState || dockDragState.pointerId !== event.pointerId) return;
|
|
2204
|
+
const deltaX = event.clientX - dockDragState.startX;
|
|
2205
|
+
const deltaY = event.clientY - dockDragState.startY;
|
|
2206
|
+
cancelToggleLongPressOnMove(event);
|
|
2207
|
+
if (!dockDragState.moved && Math.hypot(deltaX, deltaY) < MOBILE_COMPOSER_TAP_SLOP_PX) return;
|
|
2208
|
+
dockDragState.moved = true;
|
|
2209
|
+
const bounds = resolveVisibleDockBounds();
|
|
2210
|
+
const maxLeft = Math.max(bounds.left, bounds.right - dockDragState.dockWidth);
|
|
2211
|
+
const maxTop = Math.max(bounds.top, bounds.bottom - dockDragState.dockHeight);
|
|
2212
|
+
queueDockPosition(clamp(dockDragState.originLeft + deltaX, bounds.left, maxLeft), clamp(dockDragState.originTop + deltaY, bounds.top, maxTop));
|
|
2213
|
+
event.preventDefault();
|
|
2214
|
+
};
|
|
2215
|
+
const onDockPointerEnd = (event) => {
|
|
2216
|
+
if (!dockDragState || dockDragState.pointerId !== event.pointerId) return;
|
|
2217
|
+
const cancelled = event.type === "pointercancel";
|
|
2218
|
+
try {
|
|
2219
|
+
dock.releasePointerCapture?.(event.pointerId);
|
|
2220
|
+
} catch {}
|
|
2221
|
+
if (toggleLongPressState?.pointerId === event.pointerId && toggleLongPressState.triggered) {
|
|
2222
|
+
suppressDockClickUntil = options.windowObject.performance.now() + MOBILE_COMPOSER_CLICK_SUPPRESS_MS;
|
|
2223
|
+
options.windowObject.setTimeout(() => {
|
|
2224
|
+
clearToggleLongPress();
|
|
2225
|
+
}, 0);
|
|
2226
|
+
} else clearToggleLongPress();
|
|
2227
|
+
if (cancelled) suppressDockClickUntil = options.windowObject.performance.now() + MOBILE_COMPOSER_CLICK_SUPPRESS_MS;
|
|
2228
|
+
else if (dockDragState.moved) {
|
|
2229
|
+
flushDockPosition();
|
|
2230
|
+
keepActiveDockInsideViewport();
|
|
2231
|
+
suppressDockClickUntil = options.windowObject.performance.now() + MOBILE_COMPOSER_CLICK_SUPPRESS_MS;
|
|
2232
|
+
event.preventDefault();
|
|
2233
|
+
} else if (suppressDockClickUntil <= options.windowObject.performance.now()) {
|
|
2234
|
+
if (dockDragState.action.kind === "toggle") {
|
|
2235
|
+
activateToggle();
|
|
2236
|
+
suppressDockClickUntil = options.windowObject.performance.now() + MOBILE_COMPOSER_CLICK_SUPPRESS_MS;
|
|
2237
|
+
event.preventDefault();
|
|
2238
|
+
} else if (dockDragState.action.kind === "control") {
|
|
2239
|
+
activateControl(dockDragState.action.control);
|
|
2240
|
+
suppressDockClickUntil = options.windowObject.performance.now() + MOBILE_COMPOSER_CLICK_SUPPRESS_MS;
|
|
2241
|
+
event.preventDefault();
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
delete dock.dataset.dragging;
|
|
2245
|
+
options.windowObject.setTimeout(() => {
|
|
2246
|
+
dockDragState = null;
|
|
2247
|
+
}, 0);
|
|
2248
|
+
};
|
|
2249
|
+
shell.addEventListener("pointerdown", onShellPointerDown, true);
|
|
2250
|
+
shell.addEventListener("pointermove", onShellPointerMove, true);
|
|
2251
|
+
shell.addEventListener("pointerup", onShellPointerUp, true);
|
|
2252
|
+
shell.addEventListener("pointercancel", onShellPointerCancel, true);
|
|
2253
|
+
toggleButton.addEventListener("click", onToggleClick);
|
|
2254
|
+
controlList.addEventListener("click", onControlClick);
|
|
2255
|
+
directionPad.addEventListener("click", onControlClick);
|
|
2256
|
+
dock.addEventListener("pointerdown", onDockPointerDown);
|
|
2257
|
+
dock.addEventListener("pointermove", onDockPointerMove);
|
|
2258
|
+
dock.addEventListener("pointerup", onDockPointerEnd);
|
|
2259
|
+
dock.addEventListener("pointercancel", onDockPointerEnd);
|
|
2260
|
+
options.windowObject.visualViewport?.addEventListener("resize", scheduleDockViewportClamp);
|
|
2261
|
+
options.windowObject.visualViewport?.addEventListener("scroll", scheduleDockViewportClamp);
|
|
2262
|
+
return {
|
|
2263
|
+
destroy() {
|
|
2264
|
+
shell.removeEventListener("pointerdown", onShellPointerDown, true);
|
|
2265
|
+
shell.removeEventListener("pointermove", onShellPointerMove, true);
|
|
2266
|
+
shell.removeEventListener("pointerup", onShellPointerUp, true);
|
|
2267
|
+
shell.removeEventListener("pointercancel", onShellPointerCancel, true);
|
|
2268
|
+
toggleButton.removeEventListener("click", onToggleClick);
|
|
2269
|
+
controlList.removeEventListener("click", onControlClick);
|
|
2270
|
+
directionPad.removeEventListener("click", onControlClick);
|
|
2271
|
+
dock.removeEventListener("pointerdown", onDockPointerDown);
|
|
2272
|
+
dock.removeEventListener("pointermove", onDockPointerMove);
|
|
2273
|
+
dock.removeEventListener("pointerup", onDockPointerEnd);
|
|
2274
|
+
dock.removeEventListener("pointercancel", onDockPointerEnd);
|
|
2275
|
+
options.windowObject.visualViewport?.removeEventListener("resize", scheduleDockViewportClamp);
|
|
2276
|
+
options.windowObject.visualViewport?.removeEventListener("scroll", scheduleDockViewportClamp);
|
|
2277
|
+
clearToggleLongPress();
|
|
2278
|
+
if (dockPositionFrame !== null) {
|
|
2279
|
+
options.windowObject.cancelAnimationFrame(dockPositionFrame);
|
|
2280
|
+
dockPositionFrame = null;
|
|
2281
|
+
}
|
|
2282
|
+
dock.remove();
|
|
2283
|
+
},
|
|
2284
|
+
focus() {
|
|
2285
|
+
open();
|
|
2286
|
+
},
|
|
2287
|
+
setEnabled(enabled) {
|
|
2288
|
+
if (composerEnabled === enabled) return;
|
|
2289
|
+
composerEnabled = enabled;
|
|
2290
|
+
if (!enabled) {
|
|
2291
|
+
dockExpanded = false;
|
|
2292
|
+
directionModeActive = false;
|
|
2293
|
+
dockDragState = null;
|
|
2294
|
+
terminalTapFocusState = null;
|
|
2295
|
+
clearToggleLongPress();
|
|
2296
|
+
delete dock.dataset.dragging;
|
|
2297
|
+
}
|
|
2298
|
+
syncDockExpandedState();
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
function installTakeoverNotice(shell, windowObject, options) {
|
|
2303
|
+
const doc = shell.ownerDocument;
|
|
2304
|
+
const notice = resolveTakeoverApprovalElement(shell);
|
|
2305
|
+
const message = notice.querySelector("[data-aitty-takeover-message]");
|
|
2306
|
+
const allowButton = notice.querySelector("[data-aitty-takeover-allow]");
|
|
2307
|
+
const denyButton = notice.querySelector("[data-aitty-takeover-deny]");
|
|
2308
|
+
const enableNotificationsButton = resolveNotificationEnableButton(shell);
|
|
2309
|
+
const pushPublicKey = resolvePushPublicKey(doc);
|
|
2310
|
+
let permissionPromptEnabled = false;
|
|
2311
|
+
let activeRequest = null;
|
|
2312
|
+
let timeoutHandle = null;
|
|
2313
|
+
let systemNotification = null;
|
|
2314
|
+
let activePushEndpoint = null;
|
|
2315
|
+
let pushRegistrationInFlight = false;
|
|
2316
|
+
let lastAutoRegisteredRole = null;
|
|
2317
|
+
const clearTimer = () => {
|
|
2318
|
+
if (timeoutHandle !== null) {
|
|
2319
|
+
windowObject.clearTimeout(timeoutHandle);
|
|
2320
|
+
timeoutHandle = null;
|
|
2321
|
+
}
|
|
2322
|
+
};
|
|
2323
|
+
const closeSystemNotification = () => {
|
|
2324
|
+
systemNotification?.close?.();
|
|
2325
|
+
systemNotification = null;
|
|
2326
|
+
};
|
|
2327
|
+
const hide = () => {
|
|
2328
|
+
clearTimer();
|
|
2329
|
+
closeSystemNotification();
|
|
2330
|
+
activeRequest = null;
|
|
2331
|
+
notice.hidden = true;
|
|
2332
|
+
};
|
|
2333
|
+
const respond = (approved) => {
|
|
2334
|
+
const request = activeRequest;
|
|
2335
|
+
if (!request) {
|
|
2336
|
+
hide();
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
options.onRespond(request.requestId, approved);
|
|
2340
|
+
hide();
|
|
2341
|
+
};
|
|
2342
|
+
const focusWindow = () => {
|
|
2343
|
+
windowObject.focus?.();
|
|
2344
|
+
notice.scrollIntoView?.({
|
|
2345
|
+
block: "nearest",
|
|
2346
|
+
inline: "nearest"
|
|
2347
|
+
});
|
|
2348
|
+
};
|
|
2349
|
+
const showSystemNotification = (request) => {
|
|
2350
|
+
const NotificationConstructor = windowObject.Notification;
|
|
2351
|
+
if (typeof NotificationConstructor !== "function" || NotificationConstructor.permission !== "granted" || doc.visibilityState !== "hidden") return;
|
|
2352
|
+
try {
|
|
2353
|
+
closeSystemNotification();
|
|
2354
|
+
systemNotification = new NotificationConstructor("Terminal control requested", {
|
|
2355
|
+
body: `${formatTakeoverRequester(request)} wants to control this terminal. Click to review.`,
|
|
2356
|
+
tag: "aitty-takeover-request"
|
|
2357
|
+
});
|
|
2358
|
+
systemNotification.onclick = () => {
|
|
2359
|
+
focusWindow();
|
|
2360
|
+
closeSystemNotification();
|
|
2361
|
+
};
|
|
2362
|
+
} catch {
|
|
2363
|
+
closeSystemNotification();
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
const hasNotificationPermission = () => {
|
|
2367
|
+
const NotificationConstructor = windowObject.Notification;
|
|
2368
|
+
return typeof NotificationConstructor === "function" && NotificationConstructor.permission === "granted";
|
|
2369
|
+
};
|
|
2370
|
+
const requestNotificationPermission = async () => {
|
|
2371
|
+
const NotificationConstructor = windowObject.Notification;
|
|
2372
|
+
if (typeof NotificationConstructor !== "function" || NotificationConstructor.permission !== "default") return;
|
|
2373
|
+
await NotificationConstructor.requestPermission?.();
|
|
2374
|
+
};
|
|
2375
|
+
const registerPushNotifications = async () => {
|
|
2376
|
+
if (pushRegistrationInFlight || activePushEndpoint || options.getClientRole() !== "controller") return;
|
|
2377
|
+
pushRegistrationInFlight = true;
|
|
2378
|
+
try {
|
|
2379
|
+
const subscription = await subscribeBrowserPush(windowObject, pushPublicKey);
|
|
2380
|
+
if (!subscription) return;
|
|
2381
|
+
const payload = normalizePushSubscription(subscription.toJSON());
|
|
2382
|
+
if (!payload) return;
|
|
2383
|
+
activePushEndpoint = payload.endpoint;
|
|
2384
|
+
options.sendControl(createPushSubscribeControlFrame({
|
|
2385
|
+
subscription: payload,
|
|
2386
|
+
url: windowObject.location?.href
|
|
2387
|
+
}));
|
|
2388
|
+
} finally {
|
|
2389
|
+
pushRegistrationInFlight = false;
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
const syncNotificationPermissionControl = () => {
|
|
2393
|
+
if (!enableNotificationsButton) return;
|
|
2394
|
+
const NotificationConstructor = windowObject.Notification;
|
|
2395
|
+
const canRequestPageNotification = typeof NotificationConstructor === "function" && NotificationConstructor.permission === "default";
|
|
2396
|
+
const canRequestPush = Boolean(pushPublicKey) && canUseBrowserPush(windowObject) && activePushEndpoint === null && options.getClientRole() === "controller";
|
|
2397
|
+
enableNotificationsButton.hidden = !(permissionPromptEnabled && options.getClientRole() === "controller" && (canRequestPageNotification || canRequestPush));
|
|
2398
|
+
enableNotificationsButton.disabled = pushRegistrationInFlight;
|
|
2399
|
+
enableNotificationsButton.textContent = pushRegistrationInFlight ? "Enabling alerts..." : "Enable alerts";
|
|
2400
|
+
enableNotificationsButton.setAttribute("aria-label", "Enable takeover alerts for this controller");
|
|
2401
|
+
};
|
|
2402
|
+
const syncNotificationState = () => {
|
|
2403
|
+
if (options.getClientRole() === "controller" && hasNotificationPermission()) registerPushNotifications().catch(() => void 0).finally(() => syncNotificationPermissionControl());
|
|
2404
|
+
syncNotificationPermissionControl();
|
|
2405
|
+
};
|
|
2406
|
+
const onAllow = (event) => {
|
|
2407
|
+
event.preventDefault();
|
|
2408
|
+
respond(true);
|
|
2409
|
+
};
|
|
2410
|
+
const onDeny = (event) => {
|
|
2411
|
+
event.preventDefault();
|
|
2412
|
+
respond(false);
|
|
893
2413
|
};
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
2414
|
+
const onVisibilityChange = () => {
|
|
2415
|
+
const request = activeRequest;
|
|
2416
|
+
if (request && doc.visibilityState === "hidden") showSystemNotification(request);
|
|
897
2417
|
};
|
|
898
|
-
const
|
|
899
|
-
|
|
2418
|
+
const onEnableNotifications = (event) => {
|
|
2419
|
+
event.preventDefault();
|
|
2420
|
+
requestNotificationPermission().then(() => registerPushNotifications()).catch(() => void 0).finally(() => syncNotificationPermissionControl());
|
|
2421
|
+
syncNotificationPermissionControl();
|
|
900
2422
|
};
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
textarea.addEventListener("compositionend", syncInputPosition, true);
|
|
907
|
-
textarea.addEventListener("input", syncInputPosition, true);
|
|
908
|
-
textarea.addEventListener("keydown", syncInputPosition, true);
|
|
909
|
-
term.input = {
|
|
910
|
-
textarea,
|
|
911
|
-
focus() {
|
|
912
|
-
syncInputPosition();
|
|
913
|
-
textarea.focus({ preventScroll: true });
|
|
914
|
-
},
|
|
2423
|
+
allowButton?.addEventListener("click", onAllow);
|
|
2424
|
+
denyButton?.addEventListener("click", onDeny);
|
|
2425
|
+
enableNotificationsButton?.addEventListener("click", onEnableNotifications);
|
|
2426
|
+
doc.addEventListener("visibilitychange", onVisibilityChange);
|
|
2427
|
+
return {
|
|
915
2428
|
destroy() {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
2429
|
+
hide();
|
|
2430
|
+
if (activePushEndpoint) {
|
|
2431
|
+
options.sendControl(createPushUnsubscribeControlFrame({ endpoint: activePushEndpoint }));
|
|
2432
|
+
activePushEndpoint = null;
|
|
2433
|
+
}
|
|
2434
|
+
allowButton?.removeEventListener("click", onAllow);
|
|
2435
|
+
denyButton?.removeEventListener("click", onDeny);
|
|
2436
|
+
enableNotificationsButton?.removeEventListener("click", onEnableNotifications);
|
|
2437
|
+
doc.removeEventListener("visibilitychange", onVisibilityChange);
|
|
2438
|
+
},
|
|
2439
|
+
show(request) {
|
|
2440
|
+
activeRequest = request;
|
|
2441
|
+
notice.hidden = false;
|
|
2442
|
+
if (message) message.textContent = `${formatTakeoverRequester(request)} wants to control this terminal. Approve only if you intend to hand off input.`;
|
|
2443
|
+
syncNotificationPermissionControl();
|
|
2444
|
+
clearTimer();
|
|
2445
|
+
timeoutHandle = windowObject.setTimeout(() => {
|
|
2446
|
+
respond(false);
|
|
2447
|
+
}, TAKEOVER_NOTICE_TIMEOUT_MS);
|
|
2448
|
+
showSystemNotification(request);
|
|
2449
|
+
},
|
|
2450
|
+
syncClient() {
|
|
2451
|
+
const role = options.getClientRole();
|
|
2452
|
+
if (role !== lastAutoRegisteredRole) {
|
|
2453
|
+
lastAutoRegisteredRole = role;
|
|
2454
|
+
syncNotificationState();
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
syncNotificationPermissionControl();
|
|
2458
|
+
},
|
|
2459
|
+
syncNotificationControl() {
|
|
2460
|
+
syncNotificationPermissionControl();
|
|
2461
|
+
},
|
|
2462
|
+
syncPermissionPrompt(enabled) {
|
|
2463
|
+
const wasEnabled = permissionPromptEnabled;
|
|
2464
|
+
const role = options.getClientRole();
|
|
2465
|
+
if (role !== lastAutoRegisteredRole) lastAutoRegisteredRole = role;
|
|
2466
|
+
permissionPromptEnabled = enabled;
|
|
2467
|
+
if (!wasEnabled && enabled && hasNotificationPermission()) {
|
|
2468
|
+
syncNotificationState();
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
syncNotificationPermissionControl();
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
function resolveNotificationEnableButton(shell) {
|
|
2476
|
+
const existing = shell.querySelector("[data-aitty-notification-enable]");
|
|
2477
|
+
if (existing) return existing;
|
|
2478
|
+
const statusActions = shell.querySelector(".shell-status-actions");
|
|
2479
|
+
if (!statusActions) return null;
|
|
2480
|
+
const button = shell.ownerDocument.createElement("button");
|
|
2481
|
+
button.className = "notification-enable-button";
|
|
2482
|
+
button.dataset.aittyNotificationEnable = "";
|
|
2483
|
+
button.hidden = true;
|
|
2484
|
+
button.type = "button";
|
|
2485
|
+
button.textContent = "Enable alerts";
|
|
2486
|
+
statusActions.append(button);
|
|
2487
|
+
return button;
|
|
2488
|
+
}
|
|
2489
|
+
function resolvePushPublicKey(doc) {
|
|
2490
|
+
const key = doc.querySelector("meta[name=\"aitty-vapid-public-key\"]")?.content.trim();
|
|
2491
|
+
return key ? key : null;
|
|
2492
|
+
}
|
|
2493
|
+
function canUseBrowserPush(windowObject) {
|
|
2494
|
+
const navigatorLike = windowObject.navigator;
|
|
2495
|
+
return Boolean(windowObject.isSecureContext && navigatorLike.serviceWorker && "PushManager" in windowObject && typeof navigatorLike.serviceWorker.register === "function");
|
|
2496
|
+
}
|
|
2497
|
+
async function subscribeBrowserPush(windowObject, publicKey) {
|
|
2498
|
+
if (!publicKey || !canUseBrowserPush(windowObject)) return null;
|
|
2499
|
+
const registration = await windowObject.navigator.serviceWorker.register("/assets/aitty-sw.js", { scope: "/" });
|
|
2500
|
+
const existing = await registration.pushManager.getSubscription();
|
|
2501
|
+
if (existing) return existing;
|
|
2502
|
+
return registration.pushManager.subscribe({
|
|
2503
|
+
applicationServerKey: urlBase64ToArrayBuffer(publicKey),
|
|
2504
|
+
userVisibleOnly: true
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
function normalizePushSubscription(subscription) {
|
|
2508
|
+
if (typeof subscription.endpoint !== "string" || !subscription.endpoint || !subscription.keys || typeof subscription.keys.auth !== "string" || !subscription.keys.auth || typeof subscription.keys.p256dh !== "string" || !subscription.keys.p256dh) return null;
|
|
2509
|
+
return {
|
|
2510
|
+
endpoint: subscription.endpoint,
|
|
2511
|
+
expirationTime: typeof subscription.expirationTime === "number" ? subscription.expirationTime : null,
|
|
2512
|
+
keys: {
|
|
2513
|
+
auth: subscription.keys.auth,
|
|
2514
|
+
p256dh: subscription.keys.p256dh
|
|
926
2515
|
}
|
|
927
2516
|
};
|
|
928
2517
|
}
|
|
2518
|
+
function urlBase64ToArrayBuffer(value) {
|
|
2519
|
+
const base64 = `${value}${"=".repeat((4 - value.length % 4) % 4)}`.replace(/-/g, "+").replace(/_/g, "/");
|
|
2520
|
+
const raw = atob(base64);
|
|
2521
|
+
const output = new Uint8Array(raw.length);
|
|
2522
|
+
for (let index = 0; index < raw.length; index += 1) output[index] = raw.charCodeAt(index);
|
|
2523
|
+
return output.buffer;
|
|
2524
|
+
}
|
|
2525
|
+
function resolveTakeoverApprovalElement(shell) {
|
|
2526
|
+
const existing = shell.querySelector("[data-aitty-takeover-approval]");
|
|
2527
|
+
if (existing) return existing;
|
|
2528
|
+
const notice = shell.ownerDocument.createElement("section");
|
|
2529
|
+
notice.className = "takeover-approval";
|
|
2530
|
+
notice.dataset.aittyTakeoverApproval = "";
|
|
2531
|
+
notice.hidden = true;
|
|
2532
|
+
notice.setAttribute("aria-live", "polite");
|
|
2533
|
+
notice.innerHTML = [
|
|
2534
|
+
"<strong>Terminal control requested</strong>",
|
|
2535
|
+
"<p data-aitty-takeover-message>Another browser wants to control this terminal.</p>",
|
|
2536
|
+
"<div class=\"takeover-approval-actions\">",
|
|
2537
|
+
"<button type=\"button\" data-aitty-takeover-deny>Deny</button>",
|
|
2538
|
+
"<button type=\"button\" data-aitty-takeover-allow>Allow</button>",
|
|
2539
|
+
"</div>"
|
|
2540
|
+
].join("");
|
|
2541
|
+
shell.append(notice);
|
|
2542
|
+
return notice;
|
|
2543
|
+
}
|
|
2544
|
+
function formatTakeoverRequester(request) {
|
|
2545
|
+
return request.requesterId ? `Browser ${request.requesterId}` : "Another browser";
|
|
2546
|
+
}
|
|
2547
|
+
function resolveTakeoverResultMessage(reason) {
|
|
2548
|
+
switch (reason) {
|
|
2549
|
+
case "controller-disconnected": return "Control request expired because the controller disconnected.";
|
|
2550
|
+
case "controller-suspended": return "Controller is reconnecting. Try again shortly.";
|
|
2551
|
+
case "pending": return "Control request already pending.";
|
|
2552
|
+
case "rejected": return "Control request rejected.";
|
|
2553
|
+
default: return "Control request failed.";
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function createMobileComposerButton(doc, control, label) {
|
|
2557
|
+
const button = doc.createElement("button");
|
|
2558
|
+
button.className = "aitty-mobile-composer-button";
|
|
2559
|
+
button.dataset.aittyMobileComposerControl = control;
|
|
2560
|
+
button.type = "button";
|
|
2561
|
+
button.textContent = label;
|
|
2562
|
+
return button;
|
|
2563
|
+
}
|
|
2564
|
+
function resolveMobileComposerControlLabel(control) {
|
|
2565
|
+
if (control === "enter") return "Enter";
|
|
2566
|
+
if (control === "esc") return "Esc";
|
|
2567
|
+
if (control === "tab") return "Tab";
|
|
2568
|
+
return control.replace("arrow-", "");
|
|
2569
|
+
}
|
|
2570
|
+
function createMobileDirectionButton(doc, control, label, glyph) {
|
|
2571
|
+
const button = createMobileComposerButton(doc, control, glyph);
|
|
2572
|
+
button.classList.add("aitty-mobile-direction-button", `aitty-mobile-direction-${control.replace("arrow-", "")}`);
|
|
2573
|
+
button.setAttribute("aria-label", `${label} arrow`);
|
|
2574
|
+
return button;
|
|
2575
|
+
}
|
|
2576
|
+
function isCoarseMobileViewport(windowObject) {
|
|
2577
|
+
return (firstFinitePositiveNumber(windowObject.visualViewport?.width, windowObject.innerWidth, windowObject.document?.documentElement?.clientWidth) ?? Number.POSITIVE_INFINITY) <= 768 && windowObject.matchMedia?.("(pointer: coarse)").matches === true;
|
|
2578
|
+
}
|
|
929
2579
|
function configureTerminalTextarea(textarea) {
|
|
930
2580
|
textarea.setAttribute("autocapitalize", "off");
|
|
931
2581
|
textarea.setAttribute("autocomplete", "off");
|
|
932
2582
|
textarea.setAttribute("autocorrect", "off");
|
|
933
2583
|
textarea.setAttribute("spellcheck", "false");
|
|
934
2584
|
textarea.setAttribute("enterkeyhint", "send");
|
|
2585
|
+
textarea.setAttribute("inputmode", "text");
|
|
935
2586
|
textarea.setAttribute("tabindex", "0");
|
|
936
2587
|
textarea.setAttribute("aria-hidden", "true");
|
|
2588
|
+
textarea.dataset.aittyInput = "";
|
|
937
2589
|
const style = textarea.style;
|
|
938
2590
|
style.position = "fixed";
|
|
939
2591
|
style.left = "0";
|
|
940
2592
|
style.top = "0";
|
|
941
|
-
style.
|
|
942
|
-
style.
|
|
943
|
-
style.
|
|
2593
|
+
style.zIndex = "1400";
|
|
2594
|
+
style.width = "2px";
|
|
2595
|
+
style.height = "2px";
|
|
2596
|
+
style.opacity = "0.01";
|
|
944
2597
|
style.overflow = "hidden";
|
|
945
2598
|
style.border = "0";
|
|
946
2599
|
style.padding = "0";
|
|
947
2600
|
style.margin = "0";
|
|
948
2601
|
style.outline = "none";
|
|
949
2602
|
style.resize = "none";
|
|
2603
|
+
style.boxSizing = "border-box";
|
|
2604
|
+
style.fontSize = "16px";
|
|
2605
|
+
style.lineHeight = "16px";
|
|
950
2606
|
style.pointerEvents = "none";
|
|
2607
|
+
style.touchAction = "manipulation";
|
|
2608
|
+
style.userSelect = "text";
|
|
2609
|
+
style.setProperty("-webkit-touch-callout", "default");
|
|
2610
|
+
style.setProperty("-webkit-user-select", "text");
|
|
951
2611
|
style.caretColor = "transparent";
|
|
952
2612
|
style.color = "transparent";
|
|
953
2613
|
style.background = "transparent";
|
|
2614
|
+
style.colorScheme = "inherit";
|
|
2615
|
+
suppressNativeTextareaChrome(textarea);
|
|
2616
|
+
}
|
|
2617
|
+
function suppressNativeTextareaChrome(textarea) {
|
|
2618
|
+
const style = textarea.style;
|
|
2619
|
+
style.borderRadius = "0";
|
|
2620
|
+
style.boxShadow = "none";
|
|
2621
|
+
style.textDecoration = "none";
|
|
2622
|
+
style.setProperty("appearance", "none");
|
|
2623
|
+
style.setProperty("-webkit-appearance", "none");
|
|
2624
|
+
style.setProperty("-webkit-text-fill-color", "transparent");
|
|
2625
|
+
style.setProperty("outline-color", "transparent");
|
|
2626
|
+
style.setProperty("text-decoration", "none");
|
|
2627
|
+
style.setProperty("text-decoration-color", "transparent");
|
|
2628
|
+
style.setProperty("text-decoration-line", "none");
|
|
2629
|
+
}
|
|
2630
|
+
function resolveMobileLiveInputAnchorRect(windowObject) {
|
|
2631
|
+
const doc = windowObject.document;
|
|
2632
|
+
if (typeof doc?.querySelector !== "function") return null;
|
|
2633
|
+
const owner = doc.querySelector("[data-terminal-root][data-aitty-input-owner]");
|
|
2634
|
+
if (!isTerminalRootLike(owner)) return null;
|
|
2635
|
+
const rect = resolveLiveInputSurfaceAnchor(owner)?.getBoundingClientRect?.();
|
|
2636
|
+
return rect && hasUsableClientRect(rect) ? rect : null;
|
|
954
2637
|
}
|
|
955
|
-
function
|
|
2638
|
+
function isTerminalRootLike(value) {
|
|
2639
|
+
return typeof value === "object" && value !== null && "querySelectorAll" in value && typeof value.querySelectorAll === "function";
|
|
2640
|
+
}
|
|
2641
|
+
function syncTerminalTextareaPosition(term, textarea, options = {}) {
|
|
2642
|
+
const windowObject = textarea.ownerDocument.defaultView;
|
|
956
2643
|
const viewport = resolveTextareaViewport(textarea);
|
|
957
|
-
const anchorRect = (resolveLiveInputSurfaceAnchor(term.element) ?? term.element).getBoundingClientRect?.();
|
|
2644
|
+
const anchorRect = (options.anchor ?? resolveLiveInputSurfaceAnchor(term.element) ?? term.element).getBoundingClientRect?.();
|
|
958
2645
|
const fallbackRect = term.element.getBoundingClientRect?.();
|
|
959
2646
|
const rect = hasUsableClientRect(anchorRect) ? anchorRect : fallbackRect;
|
|
960
|
-
if (
|
|
961
|
-
textarea.
|
|
962
|
-
textarea.style.
|
|
2647
|
+
if (options.allowViewportAvoidance !== true) {
|
|
2648
|
+
textarea.dataset.aittyImeAnchor = "safe-cursor";
|
|
2649
|
+
textarea.style.width = "2px";
|
|
2650
|
+
textarea.style.height = "2px";
|
|
2651
|
+
textarea.style.opacity = "0.01";
|
|
2652
|
+
textarea.style.padding = "0";
|
|
2653
|
+
textarea.style.fontSize = "16px";
|
|
2654
|
+
textarea.style.lineHeight = "16px";
|
|
2655
|
+
textarea.style.pointerEvents = "none";
|
|
2656
|
+
textarea.style.touchAction = "manipulation";
|
|
2657
|
+
textarea.style.caretColor = "transparent";
|
|
2658
|
+
textarea.style.color = "transparent";
|
|
2659
|
+
textarea.style.background = "transparent";
|
|
2660
|
+
suppressNativeTextareaChrome(textarea);
|
|
2661
|
+
if (!hasUsableClientRect(rect)) {
|
|
2662
|
+
textarea.style.left = `${Math.min(DESKTOP_IME_TEXTAREA_INSET_PX, Math.max(0, viewport.width - 1))}px`;
|
|
2663
|
+
textarea.style.top = `${Math.min(DESKTOP_IME_TEXTAREA_INSET_PX, Math.max(0, viewport.height - 1))}px`;
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const safeMaxLeft = Math.max(DESKTOP_IME_TEXTAREA_INSET_PX, viewport.width - DESKTOP_IME_CANDIDATE_SAFE_WIDTH_PX);
|
|
2667
|
+
const safeMaxTop = Math.max(DESKTOP_IME_TEXTAREA_INSET_PX, viewport.height - DESKTOP_IME_CANDIDATE_SAFE_HEIGHT_PX);
|
|
2668
|
+
const left = clamp(rect.left, DESKTOP_IME_TEXTAREA_INSET_PX, safeMaxLeft);
|
|
2669
|
+
const top = clamp(rect.top, DESKTOP_IME_TEXTAREA_INSET_PX, safeMaxTop);
|
|
2670
|
+
textarea.style.left = `${left}px`;
|
|
2671
|
+
textarea.style.top = `${top}px`;
|
|
963
2672
|
return;
|
|
964
2673
|
}
|
|
965
|
-
|
|
966
|
-
const
|
|
2674
|
+
textarea.dataset.aittyImeAnchor = "mobile-focus-proxy";
|
|
2675
|
+
const proxyPosition = windowObject ? resolveMobileFocusProxyPosition(windowObject) : {
|
|
2676
|
+
anchor: "mobile-focus-proxy",
|
|
2677
|
+
height: MOBILE_FOCUS_PROXY_HEIGHT_PX,
|
|
2678
|
+
left: 0,
|
|
2679
|
+
top: 0,
|
|
2680
|
+
width: MOBILE_FOCUS_PROXY_MIN_WIDTH_PX
|
|
2681
|
+
};
|
|
2682
|
+
const left = clamp(proxyPosition.left, 0, Math.max(0, viewport.width - proxyPosition.width));
|
|
2683
|
+
const top = clamp(proxyPosition.top, 0, Math.max(0, viewport.height - proxyPosition.height));
|
|
967
2684
|
textarea.style.left = `${left}px`;
|
|
968
2685
|
textarea.style.top = `${top}px`;
|
|
2686
|
+
textarea.style.width = `${proxyPosition.width}px`;
|
|
2687
|
+
textarea.style.height = `${proxyPosition.height}px`;
|
|
2688
|
+
textarea.style.opacity = MOBILE_FOCUS_PROXY_OPACITY;
|
|
2689
|
+
textarea.style.padding = "0";
|
|
2690
|
+
textarea.style.fontSize = "16px";
|
|
2691
|
+
textarea.style.lineHeight = "16px";
|
|
2692
|
+
const mobileViewport = isCoarseMobileViewport(windowObject ?? window);
|
|
2693
|
+
const keyboardOpen = windowObject ? resolveBrowserViewport(windowObject).keyboardInsetBottom > 0 : false;
|
|
2694
|
+
const textareaFocused = textarea.ownerDocument.activeElement === textarea;
|
|
2695
|
+
const proxyPointerEvents = windowObject && shouldEnableMobileFocusProxyPointerEvents(windowObject, {
|
|
2696
|
+
keyboardOpen,
|
|
2697
|
+
textareaFocused
|
|
2698
|
+
}) && !hasTerminalSelection(term.element);
|
|
2699
|
+
textarea.style.pointerEvents = mobileViewport && proxyPointerEvents ? "auto" : "none";
|
|
2700
|
+
textarea.style.touchAction = "manipulation";
|
|
2701
|
+
textarea.style.caretColor = "transparent";
|
|
2702
|
+
textarea.style.color = "transparent";
|
|
2703
|
+
textarea.style.background = "transparent";
|
|
2704
|
+
suppressNativeTextareaChrome(textarea);
|
|
2705
|
+
}
|
|
2706
|
+
function captureImeScrollLock(windowObject, surface) {
|
|
2707
|
+
const doc = windowObject.document;
|
|
2708
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2709
|
+
if (surface instanceof HTMLElement) targets.add(surface);
|
|
2710
|
+
if (doc?.scrollingElement) targets.add(doc.scrollingElement);
|
|
2711
|
+
if (doc?.documentElement) targets.add(doc.documentElement);
|
|
2712
|
+
if (doc?.body) targets.add(doc.body);
|
|
2713
|
+
return {
|
|
2714
|
+
scrollTargets: Array.from(targets).map((element) => ({
|
|
2715
|
+
element,
|
|
2716
|
+
scrollLeft: element.scrollLeft,
|
|
2717
|
+
scrollTop: element.scrollTop
|
|
2718
|
+
})),
|
|
2719
|
+
windowObject,
|
|
2720
|
+
windowScrollX: normalizeScrollMetric(windowObject.scrollX ?? windowObject.pageXOffset ?? 0),
|
|
2721
|
+
windowScrollY: normalizeScrollMetric(windowObject.scrollY ?? windowObject.pageYOffset ?? 0)
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
function restoreImeScrollLock(lock) {
|
|
2725
|
+
for (const target of lock.scrollTargets) {
|
|
2726
|
+
target.element.scrollLeft = target.scrollLeft;
|
|
2727
|
+
target.element.scrollTop = target.scrollTop;
|
|
2728
|
+
}
|
|
2729
|
+
try {
|
|
2730
|
+
lock.windowObject.scrollTo(lock.windowScrollX, lock.windowScrollY);
|
|
2731
|
+
} catch {}
|
|
969
2732
|
}
|
|
970
2733
|
function resolveTextareaViewport(textarea) {
|
|
971
2734
|
const view = textarea.ownerDocument.defaultView;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
2735
|
+
if (view) {
|
|
2736
|
+
const viewport = resolveBrowserViewport(view);
|
|
2737
|
+
return {
|
|
2738
|
+
height: viewport.safeHeight,
|
|
2739
|
+
width: viewport.width
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
975
2742
|
return {
|
|
976
|
-
height:
|
|
977
|
-
width
|
|
2743
|
+
height: 1,
|
|
2744
|
+
width: 1
|
|
978
2745
|
};
|
|
979
2746
|
}
|
|
980
|
-
function
|
|
2747
|
+
function shouldAllowImeViewportAvoidance(windowObject) {
|
|
2748
|
+
const root = windowObject.document?.documentElement;
|
|
2749
|
+
const layoutWidth = firstFinitePositiveNumber(windowObject.innerWidth, root?.clientWidth) ?? 1;
|
|
2750
|
+
const layoutHeight = firstFinitePositiveNumber(windowObject.innerHeight, root?.clientHeight) ?? 1;
|
|
2751
|
+
const visualHeight = windowObject.visualViewport?.height;
|
|
2752
|
+
const viewportShrunkForKeyboard = typeof visualHeight === "number" && Number.isFinite(visualHeight) && visualHeight > 0 && visualHeight < layoutHeight * .85;
|
|
2753
|
+
const primaryPointerIsCoarse = windowObject.matchMedia?.("(pointer: coarse)").matches === true;
|
|
2754
|
+
return viewportShrunkForKeyboard || primaryPointerIsCoarse && layoutWidth <= 1024;
|
|
2755
|
+
}
|
|
2756
|
+
function installBrowserRenderer(term, options = {}) {
|
|
981
2757
|
const container = term?._container;
|
|
982
2758
|
if (!(container instanceof HTMLElement)) return;
|
|
983
|
-
const renderer = new BrowserTerminalRenderer(container,
|
|
2759
|
+
const renderer = new BrowserTerminalRenderer(container, options);
|
|
984
2760
|
term.renderer = renderer;
|
|
985
2761
|
renderer.setup(term.cols, term.rows);
|
|
986
2762
|
if (term.bridge) renderer.render(term.bridge);
|
|
987
2763
|
}
|
|
988
2764
|
function renderTerminalNow(term, options = {}) {
|
|
2765
|
+
if (typeof term?.renderNow === "function") {
|
|
2766
|
+
term.renderNow(options);
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
989
2769
|
const bridge = term?.bridge;
|
|
990
2770
|
const renderer = term?.renderer;
|
|
991
2771
|
if (!bridge || !renderer || typeof renderer.render !== "function") return;
|
|
992
2772
|
renderer.render(bridge, options);
|
|
993
2773
|
}
|
|
994
|
-
function installTerminalViewportSizer(term, terminalRoot, windowObject, onResize) {
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
2774
|
+
function installTerminalViewportSizer(term, terminalRoot, windowObject, onResize, shouldResizeToViewport = () => true, onViewportChange = () => {}) {
|
|
2775
|
+
const resizeToViewport = () => {
|
|
2776
|
+
if (!shouldResizeToViewport()) {
|
|
2777
|
+
onViewportChange();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const target = resolveViewportSize(term, terminalRoot, windowObject);
|
|
2781
|
+
applyTerminalElementHeight(terminalRoot, target.rows);
|
|
2782
|
+
onResize(target.cols, target.rows);
|
|
2783
|
+
};
|
|
2784
|
+
resizeToViewport();
|
|
998
2785
|
let frameHandle = null;
|
|
999
2786
|
const scheduleResize = () => {
|
|
1000
2787
|
if (frameHandle !== null) return;
|
|
1001
2788
|
frameHandle = windowObject.requestAnimationFrame(() => {
|
|
1002
2789
|
frameHandle = null;
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
onResize(nextTarget.cols, nextTarget.rows);
|
|
2790
|
+
if (resolveBrowserViewport(windowObject).keyboardInsetBottom > 0) return;
|
|
2791
|
+
resizeToViewport();
|
|
1006
2792
|
});
|
|
1007
2793
|
};
|
|
1008
2794
|
windowObject.addEventListener("resize", scheduleResize);
|
|
2795
|
+
windowObject.visualViewport?.addEventListener("resize", scheduleResize);
|
|
2796
|
+
windowObject.visualViewport?.addEventListener("scroll", scheduleResize);
|
|
1009
2797
|
return () => {
|
|
1010
2798
|
if (frameHandle !== null) {
|
|
1011
2799
|
windowObject.cancelAnimationFrame(frameHandle);
|
|
1012
2800
|
frameHandle = null;
|
|
1013
2801
|
}
|
|
1014
2802
|
windowObject.removeEventListener("resize", scheduleResize);
|
|
2803
|
+
windowObject.visualViewport?.removeEventListener("resize", scheduleResize);
|
|
2804
|
+
windowObject.visualViewport?.removeEventListener("scroll", scheduleResize);
|
|
1015
2805
|
};
|
|
1016
2806
|
}
|
|
1017
2807
|
function resolveViewportSize(term, terminalRoot, windowObject) {
|
|
@@ -1027,28 +2817,50 @@ function resolveViewportSize(term, terminalRoot, windowObject) {
|
|
|
1027
2817
|
};
|
|
1028
2818
|
}
|
|
1029
2819
|
function measureTerminalCell(term, terminalRoot) {
|
|
2820
|
+
const rowHeight = syncTerminalRowHeight(terminalRoot);
|
|
1030
2821
|
const measured = typeof term?._measureCharSize === "function" ? term._measureCharSize() : null;
|
|
1031
|
-
if (measured && Number.isFinite(measured.charWidth) && measured.charWidth > 0 &&
|
|
1032
|
-
terminalRoot.style.setProperty("--term-
|
|
1033
|
-
return
|
|
2822
|
+
if (measured && Number.isFinite(measured.charWidth) && measured.charWidth > 0 && rowHeight > 0) {
|
|
2823
|
+
terminalRoot.style.setProperty("--term-cell-width", `${measured.charWidth}px`);
|
|
2824
|
+
return {
|
|
2825
|
+
charWidth: measured.charWidth,
|
|
2826
|
+
rowHeight
|
|
2827
|
+
};
|
|
1034
2828
|
}
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1037
|
-
|
|
2829
|
+
const fontSize = parseCssPixelValue(getComputedStyle(terminalRoot).fontSize) || 15;
|
|
2830
|
+
const charWidth = Math.max(1, fontSize * .6);
|
|
2831
|
+
terminalRoot.style.setProperty("--term-cell-width", `${charWidth}px`);
|
|
1038
2832
|
return {
|
|
1039
|
-
charWidth
|
|
1040
|
-
rowHeight
|
|
2833
|
+
charWidth,
|
|
2834
|
+
rowHeight
|
|
1041
2835
|
};
|
|
1042
2836
|
}
|
|
2837
|
+
function syncTerminalRowHeight(terminalRoot) {
|
|
2838
|
+
const computedStyle = getComputedStyle(terminalRoot);
|
|
2839
|
+
const fontSize = parseCssPixelValue(computedStyle.fontSize) || DEFAULT_TERMINAL_RENDER_FONT_SIZE;
|
|
2840
|
+
const configuredLineHeight = computedStyle.lineHeight;
|
|
2841
|
+
const rowHeight = configuredLineHeight === "normal" ? Math.ceil(fontSize * 1.2) : Math.ceil(parseCssPixelValue(configuredLineHeight) || fontSize * 1.2);
|
|
2842
|
+
const normalizedRowHeight = Math.max(1, rowHeight);
|
|
2843
|
+
terminalRoot.style.setProperty("--term-row-height", `${normalizedRowHeight}px`);
|
|
2844
|
+
return normalizedRowHeight;
|
|
2845
|
+
}
|
|
1043
2846
|
function resolveAvailableTerminalViewport(terminalRoot, windowObject) {
|
|
1044
2847
|
const rect = terminalRoot.getBoundingClientRect?.();
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
const
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1051
|
-
if (!Number.isFinite(
|
|
2848
|
+
const borderBoxWidth = Number.isFinite(rect?.width) && rect.width > 0 ? rect.width : 0;
|
|
2849
|
+
const scrollViewport = terminalRoot.closest("[data-aitty-scroll-viewport]");
|
|
2850
|
+
const scrollViewportRect = scrollViewport instanceof HTMLElement ? scrollViewport.getBoundingClientRect?.() : null;
|
|
2851
|
+
const shell = terminalRoot.closest("[data-shell]");
|
|
2852
|
+
const scrollViewportHeight = scrollViewportRect && hasUsableClientRect(scrollViewportRect) && Number.isFinite(scrollViewportRect.height) && scrollViewportRect.height > 0 ? scrollViewportRect.height : 0;
|
|
2853
|
+
const isDocumentViewport = !scrollViewportHeight || shell?.dataset.fullscreen === "true" || shell?.dataset.viewportFullscreen === "true";
|
|
2854
|
+
if (borderBoxWidth <= 0 || !Number.isFinite(rect?.top)) return null;
|
|
2855
|
+
const viewportHeight = isDocumentViewport ? Math.max(1, resolveBrowserViewport(windowObject).safeHeight) : scrollViewportHeight;
|
|
2856
|
+
const computedStyle = getComputedStyle(terminalRoot);
|
|
2857
|
+
const paddingLeft = parseCssPixelValue(computedStyle.paddingLeft);
|
|
2858
|
+
const paddingRight = parseCssPixelValue(computedStyle.paddingRight);
|
|
2859
|
+
const paddingTop = parseCssPixelValue(computedStyle.paddingTop);
|
|
2860
|
+
const paddingBottom = parseCssPixelValue(computedStyle.paddingBottom);
|
|
2861
|
+
const width = borderBoxWidth - paddingLeft - paddingRight;
|
|
2862
|
+
const height = viewportHeight - (isDocumentViewport ? Math.max(0, rect.top + paddingTop) : paddingTop) - paddingBottom;
|
|
2863
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1052
2864
|
return {
|
|
1053
2865
|
height,
|
|
1054
2866
|
width
|
|
@@ -1061,6 +2873,27 @@ function parseCssPixelValue(value) {
|
|
|
1061
2873
|
const parsed = Number.parseFloat(value);
|
|
1062
2874
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
1063
2875
|
}
|
|
2876
|
+
function applyTerminalAppearance(shell, terminalRoot, appearance, previousAppearance) {
|
|
2877
|
+
terminalRoot.style.setProperty("--theme-term-font-size", `${appearance.fontSize}px`);
|
|
2878
|
+
terminalRoot.dataset.fontSize = String(appearance.fontSize);
|
|
2879
|
+
shell.dataset.fontSize = String(appearance.fontSize);
|
|
2880
|
+
if (appearance.lineHeight === void 0) terminalRoot.style.removeProperty("--theme-term-line-height");
|
|
2881
|
+
else terminalRoot.style.setProperty("--theme-term-line-height", String(appearance.lineHeight));
|
|
2882
|
+
for (const name of Object.keys(previousAppearance?.cssVars ?? {})) if (!(name in appearance.cssVars)) {
|
|
2883
|
+
terminalRoot.style.removeProperty(name);
|
|
2884
|
+
shell.style.removeProperty(name);
|
|
2885
|
+
}
|
|
2886
|
+
for (const [name, value] of Object.entries(appearance.cssVars)) {
|
|
2887
|
+
terminalRoot.style.setProperty(name, value);
|
|
2888
|
+
shell.style.setProperty(name, value);
|
|
2889
|
+
}
|
|
2890
|
+
syncTerminalRowHeight(terminalRoot);
|
|
2891
|
+
}
|
|
2892
|
+
function parseOptionalNumber(value) {
|
|
2893
|
+
if (value === void 0 || value.trim().length === 0) return;
|
|
2894
|
+
const parsed = Number(value);
|
|
2895
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2896
|
+
}
|
|
1064
2897
|
function normalizeResizeDimension(value, fallback) {
|
|
1065
2898
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : Math.max(1, fallback);
|
|
1066
2899
|
}
|
|
@@ -1086,7 +2919,6 @@ function syncTerminalSurface(element, transcriptArchive, term, styleTracker, opt
|
|
|
1086
2919
|
element.dataset.altScreen = String(resolvedAltScreenState);
|
|
1087
2920
|
element.dataset.renderMode = resolvedAltScreenState ? "screen" : "transcript";
|
|
1088
2921
|
transcriptArchive.setScreenModeActive?.(resolvedAltScreenState);
|
|
1089
|
-
syncScreenModeScrollbackRows(element, resolvedAltScreenState);
|
|
1090
2922
|
if (!bridge) {
|
|
1091
2923
|
delete element.dataset.cursorVisible;
|
|
1092
2924
|
delete element.dataset.cursorRow;
|
|
@@ -1105,7 +2937,9 @@ function syncTerminalSurface(element, transcriptArchive, term, styleTracker, opt
|
|
|
1105
2937
|
element.dataset.cursorCol = String(cursor.col + 1);
|
|
1106
2938
|
}
|
|
1107
2939
|
function syncTerminalInteractivity(element, term, interactive) {
|
|
1108
|
-
|
|
2940
|
+
const nextState = String(interactive);
|
|
2941
|
+
if (element.dataset.sessionInteractive === nextState) return;
|
|
2942
|
+
element.dataset.sessionInteractive = nextState;
|
|
1109
2943
|
element.setAttribute("aria-disabled", String(!interactive));
|
|
1110
2944
|
if (interactive) element.removeAttribute("tabindex");
|
|
1111
2945
|
else element.setAttribute("tabindex", "-1");
|
|
@@ -1119,16 +2953,6 @@ function syncTerminalInteractivity(element, term, interactive) {
|
|
|
1119
2953
|
textarea.setAttribute("aria-hidden", String(!interactive));
|
|
1120
2954
|
if (!interactive) textarea.blur();
|
|
1121
2955
|
}
|
|
1122
|
-
element.querySelectorAll(".term-cursor").forEach((cursor) => {
|
|
1123
|
-
cursor.toggleAttribute("hidden", !interactive);
|
|
1124
|
-
cursor.setAttribute("aria-hidden", String(!interactive));
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
function syncScreenModeScrollbackRows(element, screenModeActive) {
|
|
1128
|
-
element.querySelectorAll(".term-scrollback-row").forEach((row) => {
|
|
1129
|
-
row.toggleAttribute("hidden", screenModeActive);
|
|
1130
|
-
row.setAttribute("aria-hidden", String(screenModeActive));
|
|
1131
|
-
});
|
|
1132
2956
|
}
|
|
1133
2957
|
function containsScreenRedrawControl(text) {
|
|
1134
2958
|
const escape = String.fromCharCode(27);
|
|
@@ -1158,14 +2982,21 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1158
2982
|
const requestFrame = scheduler.requestFrame ?? ((callback) => requestBrowserFrame(callback, view));
|
|
1159
2983
|
const cancelFrame = scheduler.cancelFrame ?? ((handle) => cancelBrowserFrame(handle, view));
|
|
1160
2984
|
const getCols = options.getCols ?? (() => 80);
|
|
1161
|
-
const scrollbackLimit =
|
|
2985
|
+
const scrollbackLimit = normalizeTranscriptScrollbackLimit(options.scrollbackLimit);
|
|
1162
2986
|
const getVisibleRows = options.getVisibleRows ?? (() => 24);
|
|
1163
2987
|
let decoder = new TextDecoder();
|
|
1164
|
-
const archive = doc.createElement("
|
|
2988
|
+
const archive = doc.createElement("div");
|
|
2989
|
+
const topSpacer = doc.createElement("div");
|
|
2990
|
+
const visibleArchive = doc.createElement("pre");
|
|
2991
|
+
const bottomSpacer = doc.createElement("div");
|
|
1165
2992
|
const archiveText = doc.createTextNode("");
|
|
1166
2993
|
archive.setAttribute("data-terminal-transcript-archive", "");
|
|
1167
2994
|
archive.className = "term-transcript-archive";
|
|
1168
|
-
|
|
2995
|
+
topSpacer.className = "term-transcript-spacer term-transcript-spacer-top";
|
|
2996
|
+
visibleArchive.className = "term-transcript-visible";
|
|
2997
|
+
bottomSpacer.className = "term-transcript-spacer term-transcript-spacer-bottom";
|
|
2998
|
+
visibleArchive.append(archiveText);
|
|
2999
|
+
archive.append(topSpacer, visibleArchive, bottomSpacer);
|
|
1169
3000
|
element.insertBefore(archive, element.firstChild);
|
|
1170
3001
|
let preservedLines = [];
|
|
1171
3002
|
const finalizedLines = [];
|
|
@@ -1180,9 +3011,14 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1180
3011
|
let screenModeActive = false;
|
|
1181
3012
|
let concealedPreservedArchive = false;
|
|
1182
3013
|
let screenRedrawArchiveSuppressedUntil = 0;
|
|
3014
|
+
let reconciledDroppedTranscriptIndex = 0;
|
|
3015
|
+
const getScrollSurface = options.getScrollSurface ?? (() => element);
|
|
3016
|
+
const getRowHeight = () => {
|
|
3017
|
+
const computedStyle = getComputedStyle(element);
|
|
3018
|
+
return parseCssPixelValue(computedStyle.getPropertyValue("--term-row-height")) || parseCssPixelValue(computedStyle.lineHeight) || 17;
|
|
3019
|
+
};
|
|
1183
3020
|
const setPreservedLines = (lines) => {
|
|
1184
|
-
|
|
1185
|
-
preservedLines = scrollbackLimit > 0 ? normalizedLines.slice(-scrollbackLimit) : [];
|
|
3021
|
+
preservedLines = trimTranscriptLines(normalizePreservedTranscriptLines(lines), scrollbackLimit);
|
|
1186
3022
|
};
|
|
1187
3023
|
const syncArchiveVisibility = () => {
|
|
1188
3024
|
const hasArchivedContent = preservedLines.length > 0 || archiveText.data.length > 0;
|
|
@@ -1210,12 +3046,19 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1210
3046
|
const syncArchive = () => {
|
|
1211
3047
|
reconcilePreservedLines();
|
|
1212
3048
|
const liveLineCount = finalizedLines.length + 1;
|
|
1213
|
-
const visibleCapacity = Math.max(1, scrollbackLimit + Math.max(1, getVisibleRows()));
|
|
3049
|
+
const visibleCapacity = scrollbackLimit === null ? Math.max(1, getVisibleRows()) : Math.max(1, scrollbackLimit + Math.max(1, getVisibleRows()));
|
|
1214
3050
|
const hiddenLineCount = Math.max(0, liveLineCount - visibleCapacity);
|
|
1215
|
-
const archivedStart = Math.max(0, hiddenLineCount - Math.min(hiddenLineCount, scrollbackLimit));
|
|
3051
|
+
const archivedStart = Math.max(0, hiddenLineCount - (scrollbackLimit === null ? hiddenLineCount : Math.min(hiddenLineCount, scrollbackLimit)));
|
|
1216
3052
|
const liveArchiveWindow = finalizedLines.slice(archivedStart, hiddenLineCount);
|
|
1217
3053
|
const archiveWindow = preservedLines.length > 0 ? [...preservedLines, ...liveArchiveWindow] : liveArchiveWindow;
|
|
1218
|
-
|
|
3054
|
+
renderVirtualTranscriptArchive(archiveWindow, {
|
|
3055
|
+
archiveText,
|
|
3056
|
+
bottomSpacer,
|
|
3057
|
+
rowHeight: getRowHeight(),
|
|
3058
|
+
scrollSurface: getScrollSurface(),
|
|
3059
|
+
topSpacer,
|
|
3060
|
+
visibleArchive
|
|
3061
|
+
});
|
|
1219
3062
|
archivedLineCount = archiveWindow.length;
|
|
1220
3063
|
syncTranscriptMetadata(element, preservedLines.length + liveLineCount, archivedLineCount);
|
|
1221
3064
|
syncArchiveVisibility();
|
|
@@ -1223,6 +3066,7 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1223
3066
|
const resetLiveTranscript = () => {
|
|
1224
3067
|
finalizedLines.length = 0;
|
|
1225
3068
|
currentLine = "";
|
|
3069
|
+
reconciledDroppedTranscriptIndex = 0;
|
|
1226
3070
|
parserState = "text";
|
|
1227
3071
|
csiParams = "";
|
|
1228
3072
|
csiPrivate = "";
|
|
@@ -1352,6 +3196,23 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1352
3196
|
};
|
|
1353
3197
|
syncArchive();
|
|
1354
3198
|
return {
|
|
3199
|
+
appendPreservedLines(lines) {
|
|
3200
|
+
const normalizedLines = normalizePreservedTranscriptLines(lines);
|
|
3201
|
+
if (normalizedLines.length === 0) return;
|
|
3202
|
+
const liveLines = normalizePreservedTranscriptLines([...finalizedLines, currentLine]);
|
|
3203
|
+
const missingLines = [];
|
|
3204
|
+
for (const line of normalizedLines) {
|
|
3205
|
+
if (liveLines[reconciledDroppedTranscriptIndex] === line) {
|
|
3206
|
+
reconciledDroppedTranscriptIndex += 1;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
missingLines.push(line);
|
|
3210
|
+
}
|
|
3211
|
+
if (missingLines.length === 0) return;
|
|
3212
|
+
setPreservedLines([...preservedLines, ...missingLines]);
|
|
3213
|
+
concealedPreservedArchive = preservedLines.length > 0;
|
|
3214
|
+
scheduleSync();
|
|
3215
|
+
},
|
|
1355
3216
|
append(chunk) {
|
|
1356
3217
|
if (screenModeActive) return;
|
|
1357
3218
|
const text = decoder.decode(chunk, { stream: true });
|
|
@@ -1393,6 +3254,13 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1393
3254
|
refresh() {
|
|
1394
3255
|
scheduleSync();
|
|
1395
3256
|
},
|
|
3257
|
+
refreshNow() {
|
|
3258
|
+
if (frameHandle !== null) {
|
|
3259
|
+
cancelFrame(frameHandle);
|
|
3260
|
+
frameHandle = null;
|
|
3261
|
+
}
|
|
3262
|
+
syncArchive();
|
|
3263
|
+
},
|
|
1396
3264
|
revealPreservedArchive() {
|
|
1397
3265
|
if (!concealedPreservedArchive) return;
|
|
1398
3266
|
concealedPreservedArchive = false;
|
|
@@ -1402,6 +3270,47 @@ function createTranscriptArchive(element, scheduler = {}, options = {}) {
|
|
|
1402
3270
|
setScreenModeActive
|
|
1403
3271
|
};
|
|
1404
3272
|
}
|
|
3273
|
+
function renderVirtualTranscriptArchive(lines, context) {
|
|
3274
|
+
if (lines.length === 0) {
|
|
3275
|
+
context.archiveText.data = "";
|
|
3276
|
+
context.topSpacer.style.height = "";
|
|
3277
|
+
context.bottomSpacer.style.height = "";
|
|
3278
|
+
context.visibleArchive.dataset.virtualStart = "0";
|
|
3279
|
+
context.visibleArchive.dataset.virtualEnd = "0";
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
const rowHeight = Math.max(1, context.rowHeight);
|
|
3283
|
+
const windowState = resolveVirtualTranscriptWindow({
|
|
3284
|
+
archiveTop: resolveElementScrollOffset(context.visibleArchive.parentElement, context.scrollSurface),
|
|
3285
|
+
rowHeight,
|
|
3286
|
+
scrollTop: context.scrollSurface.scrollTop,
|
|
3287
|
+
thresholdRows: 600,
|
|
3288
|
+
totalRows: lines.length,
|
|
3289
|
+
viewportHeight: context.scrollSurface.clientHeight || rowHeight,
|
|
3290
|
+
overscanRows: 80
|
|
3291
|
+
});
|
|
3292
|
+
if (!windowState.virtualized) {
|
|
3293
|
+
context.archiveText.data = `${lines.join("\n")}\n`;
|
|
3294
|
+
context.topSpacer.style.height = "";
|
|
3295
|
+
context.bottomSpacer.style.height = "";
|
|
3296
|
+
context.visibleArchive.dataset.virtualStart = "0";
|
|
3297
|
+
context.visibleArchive.dataset.virtualEnd = String(lines.length);
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
const visibleLines = lines.slice(windowState.startRow, windowState.endRow);
|
|
3301
|
+
context.archiveText.data = `${visibleLines.join("\n")}\n`;
|
|
3302
|
+
context.topSpacer.style.height = `${windowState.topSpacerHeight}px`;
|
|
3303
|
+
context.bottomSpacer.style.height = `${windowState.bottomSpacerHeight}px`;
|
|
3304
|
+
context.visibleArchive.dataset.virtualStart = String(windowState.startRow);
|
|
3305
|
+
context.visibleArchive.dataset.virtualEnd = String(windowState.endRow);
|
|
3306
|
+
}
|
|
3307
|
+
function resolveElementScrollOffset(element, scrollSurface) {
|
|
3308
|
+
if (!(element instanceof HTMLElement)) return 0;
|
|
3309
|
+
const elementRect = element.getBoundingClientRect?.();
|
|
3310
|
+
const surfaceRect = scrollSurface.getBoundingClientRect?.();
|
|
3311
|
+
if (elementRect && surfaceRect) return Math.max(0, elementRect.top - surfaceRect.top + scrollSurface.scrollTop);
|
|
3312
|
+
return Math.max(0, element.offsetTop ?? 0);
|
|
3313
|
+
}
|
|
1405
3314
|
function createBrowserTransport(handlers) {
|
|
1406
3315
|
const pendingMessages = [];
|
|
1407
3316
|
const encoder = new TextEncoder();
|
|
@@ -1423,7 +3332,7 @@ function createBrowserTransport(handlers) {
|
|
|
1423
3332
|
return;
|
|
1424
3333
|
}
|
|
1425
3334
|
const payload = typeof message.data === "string" ? encoder.encode(message.data) : message.data;
|
|
1426
|
-
socket.send(
|
|
3335
|
+
socket.send(toWebSocketPayload(payload));
|
|
1427
3336
|
};
|
|
1428
3337
|
return {
|
|
1429
3338
|
close() {
|
|
@@ -1507,8 +3416,9 @@ function installScrollbackInteractions(element, callbacks = {}) {
|
|
|
1507
3416
|
const nextScrollTop = clamp(scrollSurface.scrollTop + event.deltaY, 0, maxScrollTop);
|
|
1508
3417
|
if (nextScrollTop === scrollSurface.scrollTop) return;
|
|
1509
3418
|
onManualScroll({
|
|
3419
|
+
maxScrollTop,
|
|
1510
3420
|
scrollTop: nextScrollTop,
|
|
1511
|
-
|
|
3421
|
+
source: "wheel"
|
|
1512
3422
|
});
|
|
1513
3423
|
event.preventDefault();
|
|
1514
3424
|
event.stopPropagation();
|
|
@@ -1523,7 +3433,8 @@ function installScrollbackInteractions(element, callbacks = {}) {
|
|
|
1523
3433
|
const nextScrollTop = event.key === "PageUp" ? scrollSurface.scrollTop - Math.max(scrollSurface.clientHeight, 1) : maxScrollTop;
|
|
1524
3434
|
onManualScroll({
|
|
1525
3435
|
scrollTop: clamp(nextScrollTop, 0, maxScrollTop),
|
|
1526
|
-
maxScrollTop
|
|
3436
|
+
maxScrollTop,
|
|
3437
|
+
source: "keyboard"
|
|
1527
3438
|
});
|
|
1528
3439
|
setScrollTop(scrollSurface, clamp(nextScrollTop, 0, getScrollbackMax(scrollSurface)));
|
|
1529
3440
|
return;
|
|
@@ -1540,24 +3451,57 @@ function installScrollbackInteractions(element, callbacks = {}) {
|
|
|
1540
3451
|
element.removeEventListener("keydown", onKeyDown, true);
|
|
1541
3452
|
};
|
|
1542
3453
|
}
|
|
1543
|
-
function
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
3454
|
+
function installTerminalFocusInteractions(shell, terminalRoot, callbacks) {
|
|
3455
|
+
const doc = shell.ownerDocument;
|
|
3456
|
+
const focusTerminal = ({ alignToPrompt = false } = {}) => {
|
|
3457
|
+
if (terminalRoot.dataset.sessionInteractive === "false") return;
|
|
3458
|
+
callbacks.focusTerminal({ alignToPrompt });
|
|
3459
|
+
};
|
|
3460
|
+
const onPointerDown = (event) => {
|
|
3461
|
+
if (shouldIgnoreTerminalFocusPointer(event, shell, terminalRoot)) return;
|
|
3462
|
+
const view = shell.ownerDocument.defaultView;
|
|
3463
|
+
if (view && shouldAllowImeViewportAvoidance(view)) return;
|
|
3464
|
+
focusTerminal();
|
|
3465
|
+
};
|
|
3466
|
+
const onKeyDownCapture = (event) => {
|
|
3467
|
+
if (!isShellFullscreenLike(shell) || event.defaultPrevented || isEditableEventTarget(event.target) || isCopyShortcutWithShellSelection(shell, event)) return;
|
|
3468
|
+
if (isElementInside(event.target, terminalRoot)) return;
|
|
3469
|
+
const data = callbacks.resolveKeyData(event);
|
|
3470
|
+
if (data === null) return;
|
|
3471
|
+
event.preventDefault();
|
|
3472
|
+
event.stopPropagation();
|
|
3473
|
+
focusTerminal({ alignToPrompt: true });
|
|
3474
|
+
callbacks.sendData(data);
|
|
3475
|
+
};
|
|
3476
|
+
shell.addEventListener("pointerdown", onPointerDown, true);
|
|
3477
|
+
doc.addEventListener("keydown", onKeyDownCapture, true);
|
|
3478
|
+
return () => {
|
|
3479
|
+
shell.removeEventListener("pointerdown", onPointerDown, true);
|
|
3480
|
+
doc.removeEventListener("keydown", onKeyDownCapture, true);
|
|
3481
|
+
};
|
|
1554
3482
|
}
|
|
1555
3483
|
function getScrollbackMax(element) {
|
|
1556
3484
|
return Math.max(0, element.scrollHeight - element.clientHeight);
|
|
1557
3485
|
}
|
|
1558
|
-
function
|
|
3486
|
+
function setScrollSurfaceScrollTop(element, scrollTop) {
|
|
3487
|
+
const doc = element.ownerDocument;
|
|
3488
|
+
const view = doc.defaultView;
|
|
3489
|
+
if (!(element === doc.scrollingElement || element === doc.documentElement || element === doc.body) || !view) {
|
|
3490
|
+
element.scrollTop = scrollTop;
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
element.scrollTop = scrollTop;
|
|
3494
|
+
try {
|
|
3495
|
+
view.scrollTo(view.scrollX ?? view.pageXOffset ?? 0, scrollTop);
|
|
3496
|
+
} catch {
|
|
3497
|
+
element.scrollTop = scrollTop;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
function resolveScrollSurface(doc, shell, fallbackElement, scrollOptions) {
|
|
1559
3501
|
const configuredViewport = scrollOptions?.getViewport?.() ?? scrollOptions?.viewport;
|
|
1560
3502
|
if (configuredViewport instanceof HTMLElement) return configuredViewport;
|
|
3503
|
+
const shellViewport = shell.querySelector("[data-aitty-scroll-viewport]");
|
|
3504
|
+
if (shellViewport instanceof HTMLElement) return shellViewport;
|
|
1561
3505
|
const scrollingElement = doc.scrollingElement;
|
|
1562
3506
|
if (scrollingElement instanceof HTMLElement) return scrollingElement;
|
|
1563
3507
|
if (doc.documentElement instanceof HTMLElement) return doc.documentElement;
|
|
@@ -1586,6 +3530,91 @@ function getScrollMetrics(element) {
|
|
|
1586
3530
|
thumbSizeRatio
|
|
1587
3531
|
};
|
|
1588
3532
|
}
|
|
3533
|
+
function captureScrollSurfaceAnchor(element, shell) {
|
|
3534
|
+
const maxScrollTop = getScrollbackMax(element);
|
|
3535
|
+
const scrollTop = normalizeScrollMetric(element.scrollTop);
|
|
3536
|
+
const rowAnchor = captureVisibleTerminalRowAnchor(element, shell);
|
|
3537
|
+
const inputBottomOffset = captureLiveInputBottomOffset(element, shell);
|
|
3538
|
+
const stickToBottom = isAtScrollbackBottom(scrollTop, maxScrollTop) || isLiveInputSurfaceAtBottom(shell, element);
|
|
3539
|
+
return {
|
|
3540
|
+
inputBottomOffset,
|
|
3541
|
+
maxScrollTop,
|
|
3542
|
+
ratio: maxScrollTop <= 0 ? 0 : clamp(scrollTop / maxScrollTop, 0, 1),
|
|
3543
|
+
row: rowAnchor ?? void 0,
|
|
3544
|
+
scrollTop,
|
|
3545
|
+
stickToBottom
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function restoreScrollSurfaceAnchor(element, shell, anchor) {
|
|
3549
|
+
const maxScrollTop = getScrollbackMax(element);
|
|
3550
|
+
setScrollSurfaceScrollTop(element, resolveScrollAnchorRestoreTop({
|
|
3551
|
+
anchoredScrollTop: anchor.row ? resolveScrollTopForRowAnchor(element, shell, anchor.row) : null,
|
|
3552
|
+
inputAnchoredScrollTop: anchor.inputBottomOffset === null ? null : resolveScrollTopForLiveInputBottomOffset(element, shell, anchor.inputBottomOffset),
|
|
3553
|
+
maxScrollTop,
|
|
3554
|
+
ratio: anchor.ratio,
|
|
3555
|
+
scrollTop: anchor.scrollTop,
|
|
3556
|
+
stickToBottom: anchor.stickToBottom
|
|
3557
|
+
}));
|
|
3558
|
+
}
|
|
3559
|
+
function captureLiveInputBottomOffset(element, shell) {
|
|
3560
|
+
const anchor = resolveLiveInputSurfaceAnchor(shell);
|
|
3561
|
+
const scrollRect = element.getBoundingClientRect?.() ?? null;
|
|
3562
|
+
const anchorRect = anchor?.getBoundingClientRect?.() ?? null;
|
|
3563
|
+
if (!anchor || scrollRect === null || anchorRect === null || !hasUsableClientRect(scrollRect) || !hasUsableClientRect(anchorRect)) return null;
|
|
3564
|
+
return scrollRect.bottom - anchorRect.bottom;
|
|
3565
|
+
}
|
|
3566
|
+
function resolveScrollTopForLiveInputBottomOffset(element, shell, inputBottomOffset) {
|
|
3567
|
+
const anchor = resolveLiveInputSurfaceAnchor(shell);
|
|
3568
|
+
const scrollRect = element.getBoundingClientRect?.() ?? null;
|
|
3569
|
+
const anchorRect = anchor?.getBoundingClientRect?.() ?? null;
|
|
3570
|
+
if (!anchor || scrollRect === null || anchorRect === null || !hasUsableClientRect(scrollRect) || !hasUsableClientRect(anchorRect)) return null;
|
|
3571
|
+
return element.scrollTop + anchorRect.bottom - (scrollRect.bottom - inputBottomOffset);
|
|
3572
|
+
}
|
|
3573
|
+
function captureVisibleTerminalRowAnchor(element, shell) {
|
|
3574
|
+
const scrollRect = element.getBoundingClientRect();
|
|
3575
|
+
if (!hasUsableClientRect(scrollRect)) return null;
|
|
3576
|
+
const topBoundary = Math.max(scrollRect.top, getViewportTopBoundary(shell));
|
|
3577
|
+
const rows = Array.from(shell.querySelectorAll(".term-grid .term-row"));
|
|
3578
|
+
const seenTexts = /* @__PURE__ */ new Map();
|
|
3579
|
+
let firstVisibleRow = null;
|
|
3580
|
+
for (const row of rows) {
|
|
3581
|
+
if (!(row instanceof HTMLElement)) continue;
|
|
3582
|
+
const text = row.textContent?.trimEnd() ?? "";
|
|
3583
|
+
const occurrenceIndex = seenTexts.get(text) ?? 0;
|
|
3584
|
+
seenTexts.set(text, occurrenceIndex + 1);
|
|
3585
|
+
const rect = row.getBoundingClientRect();
|
|
3586
|
+
if (!hasUsableClientRect(rect) || rect.bottom < topBoundary) continue;
|
|
3587
|
+
const rowAnchor = {
|
|
3588
|
+
offsetTop: rect.top - topBoundary,
|
|
3589
|
+
occurrenceIndex,
|
|
3590
|
+
row,
|
|
3591
|
+
text
|
|
3592
|
+
};
|
|
3593
|
+
if (text.length > 0) return rowAnchor;
|
|
3594
|
+
firstVisibleRow = firstVisibleRow ?? rowAnchor;
|
|
3595
|
+
}
|
|
3596
|
+
return firstVisibleRow;
|
|
3597
|
+
}
|
|
3598
|
+
function resolveScrollTopForRowAnchor(element, shell, anchor) {
|
|
3599
|
+
const row = anchor.row.isConnected ? anchor.row : resolveAnchoredTerminalRow(shell, anchor);
|
|
3600
|
+
if (!element.isConnected || !row) return null;
|
|
3601
|
+
const scrollRect = element.getBoundingClientRect();
|
|
3602
|
+
const rowRect = row.getBoundingClientRect();
|
|
3603
|
+
if (!hasUsableClientRect(scrollRect) || !hasUsableClientRect(rowRect)) return null;
|
|
3604
|
+
const topBoundary = Math.max(scrollRect.top, getViewportTopBoundary(shell));
|
|
3605
|
+
return element.scrollTop + rowRect.top - topBoundary - anchor.offsetTop;
|
|
3606
|
+
}
|
|
3607
|
+
function resolveAnchoredTerminalRow(shell, anchor) {
|
|
3608
|
+
if (anchor.text.length === 0) return null;
|
|
3609
|
+
let occurrenceIndex = 0;
|
|
3610
|
+
for (const row of shell.querySelectorAll(".term-grid .term-row")) {
|
|
3611
|
+
if (!(row instanceof HTMLElement)) continue;
|
|
3612
|
+
if ((row.textContent?.trimEnd() ?? "") !== anchor.text) continue;
|
|
3613
|
+
if (occurrenceIndex === anchor.occurrenceIndex) return row;
|
|
3614
|
+
occurrenceIndex += 1;
|
|
3615
|
+
}
|
|
3616
|
+
return null;
|
|
3617
|
+
}
|
|
1589
3618
|
function normalizeScrollMetric(value) {
|
|
1590
3619
|
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
1591
3620
|
}
|
|
@@ -1599,7 +3628,7 @@ function installScrollMetricsObserver(element, onScroll) {
|
|
|
1599
3628
|
};
|
|
1600
3629
|
}
|
|
1601
3630
|
function isAtScrollbackBottom(scrollTop, maxScrollTop) {
|
|
1602
|
-
return
|
|
3631
|
+
return isScrollAtBottom(scrollTop, maxScrollTop);
|
|
1603
3632
|
}
|
|
1604
3633
|
function clamp(value, min, max) {
|
|
1605
3634
|
return Math.min(max, Math.max(min, value));
|
|
@@ -1607,6 +3636,37 @@ function clamp(value, min, max) {
|
|
|
1607
3636
|
function isScrollbackShortcut(event) {
|
|
1608
3637
|
return event.shiftKey && (event.key === "PageUp" || event.key === "PageDown");
|
|
1609
3638
|
}
|
|
3639
|
+
function isShellFullscreenLike(shell) {
|
|
3640
|
+
return shell.dataset.fullscreen === "true" || shell.dataset.viewportFullscreen === "true";
|
|
3641
|
+
}
|
|
3642
|
+
function shouldIgnoreTerminalFocusPointer(event, shell, terminalRoot) {
|
|
3643
|
+
if (event.button !== 0 || event.defaultPrevented) return true;
|
|
3644
|
+
const target = event.target;
|
|
3645
|
+
if (!isElementInside(target, shell) || isInteractiveEventTarget(target)) return true;
|
|
3646
|
+
const selection = shell.ownerDocument.getSelection?.();
|
|
3647
|
+
if (selection && !selection.isCollapsed && isElementInside(target, terminalRoot)) return true;
|
|
3648
|
+
return false;
|
|
3649
|
+
}
|
|
3650
|
+
function isElementInside(target, element) {
|
|
3651
|
+
return target instanceof Node && element.contains(target);
|
|
3652
|
+
}
|
|
3653
|
+
function isInteractiveEventTarget(target) {
|
|
3654
|
+
if (!(target instanceof Element)) return false;
|
|
3655
|
+
return target.closest("button, a, input, textarea, select, option, [contenteditable=\"\"], [contenteditable=\"true\"], [data-shell-control]") !== null;
|
|
3656
|
+
}
|
|
3657
|
+
function isEditableEventTarget(target) {
|
|
3658
|
+
if (!(target instanceof Element)) return false;
|
|
3659
|
+
return target.closest("input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]") !== null;
|
|
3660
|
+
}
|
|
3661
|
+
function isCopyShortcutWithShellSelection(shell, event) {
|
|
3662
|
+
if (event.altKey || event.key.toLowerCase() !== "c" || !(event.metaKey || event.ctrlKey)) return false;
|
|
3663
|
+
const selection = shell.ownerDocument.getSelection?.();
|
|
3664
|
+
if (!selection || selection.isCollapsed || !selection.toString()) return false;
|
|
3665
|
+
return isNodeInside(selection.anchorNode, shell) && isNodeInside(selection.focusNode, shell);
|
|
3666
|
+
}
|
|
3667
|
+
function isNodeInside(node, element) {
|
|
3668
|
+
return node !== null && element.contains(node);
|
|
3669
|
+
}
|
|
1610
3670
|
function shouldPreserveVisiblePromptOnInput(event) {
|
|
1611
3671
|
if (!(event instanceof KeyboardEvent)) return false;
|
|
1612
3672
|
if (event.altKey || event.ctrlKey || event.metaKey) return false;
|
|
@@ -1616,15 +3676,201 @@ function shouldFollowLiveOutputOnInput(event) {
|
|
|
1616
3676
|
if (!(event instanceof KeyboardEvent)) return false;
|
|
1617
3677
|
return !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && event.key === "Enter";
|
|
1618
3678
|
}
|
|
3679
|
+
function shouldFollowLiveOutputOnUserInputData(data) {
|
|
3680
|
+
if (typeof data === "string") return data.includes("\r") || data.includes("\n");
|
|
3681
|
+
return data.includes(13) || data.includes(10);
|
|
3682
|
+
}
|
|
3683
|
+
function resolveMobileComposerControlSequence(control) {
|
|
3684
|
+
if (control === "enter") return "\r";
|
|
3685
|
+
if (control === "esc") return "\x1B";
|
|
3686
|
+
if (control === "tab") return " ";
|
|
3687
|
+
if (control === "arrow-up") return "\x1B[A";
|
|
3688
|
+
if (control === "arrow-down") return "\x1B[B";
|
|
3689
|
+
if (control === "arrow-right") return "\x1B[C";
|
|
3690
|
+
return "\x1B[D";
|
|
3691
|
+
}
|
|
3692
|
+
function isMobileComposerDirectionControl(control) {
|
|
3693
|
+
return control === "arrow-down" || control === "arrow-left" || control === "arrow-right" || control === "arrow-up";
|
|
3694
|
+
}
|
|
3695
|
+
function isLiveInputSurfaceAtBottom(shell, scrollSurface) {
|
|
3696
|
+
const terminalRoot = shell.querySelector("[data-terminal-root]");
|
|
3697
|
+
if (!(terminalRoot instanceof HTMLElement)) return false;
|
|
3698
|
+
const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
|
|
3699
|
+
if (!(anchor instanceof HTMLElement)) return false;
|
|
3700
|
+
const rect = anchor.getBoundingClientRect();
|
|
3701
|
+
if (!hasUsableClientRect(rect)) return false;
|
|
3702
|
+
const bottomBoundary = getScrollSurfaceBottomBoundary(scrollSurface, shell.ownerDocument?.defaultView ?? window);
|
|
3703
|
+
return rect.bottom >= bottomBoundary - 96;
|
|
3704
|
+
}
|
|
3705
|
+
function alignLiveInputSurfaceToBottom(scrollSurface, shell, terminalRoot, windowObject) {
|
|
3706
|
+
const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
|
|
3707
|
+
if (!(anchor instanceof HTMLElement)) return;
|
|
3708
|
+
const rect = anchor.getBoundingClientRect();
|
|
3709
|
+
if (!hasUsableClientRect(rect)) return;
|
|
3710
|
+
const topBoundary = getViewportTopBoundary(shell);
|
|
3711
|
+
const bottomBoundary = getScrollSurfaceBottomBoundary(scrollSurface, windowObject);
|
|
3712
|
+
const targetBottom = Math.max(topBoundary + 1, bottomBoundary - 24);
|
|
3713
|
+
if (rect.bottom <= targetBottom && rect.top >= topBoundary) return;
|
|
3714
|
+
setScrollSurfaceScrollTop(scrollSurface, clamp(scrollSurface.scrollTop + rect.bottom - targetBottom, 0, getScrollbackMax(scrollSurface)));
|
|
3715
|
+
}
|
|
3716
|
+
function alignLiveInputSurfaceIntoViewport(shell, terminalRoot, windowObject, scrollSurface) {
|
|
3717
|
+
if (!shouldAlignDocumentViewportForShell(shell, scrollSurface)) return;
|
|
3718
|
+
const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
|
|
3719
|
+
if (!(anchor instanceof HTMLElement)) return;
|
|
3720
|
+
const rect = anchor.getBoundingClientRect();
|
|
3721
|
+
if (!hasUsableClientRect(rect)) return;
|
|
3722
|
+
const topBoundary = getViewportTopBoundary(shell);
|
|
3723
|
+
const bottomBoundary = getVisibleViewportBottomBoundary(windowObject);
|
|
3724
|
+
const targetBottom = Math.max(topBoundary + 1, bottomBoundary - 24);
|
|
3725
|
+
const inputGap = targetBottom - rect.bottom;
|
|
3726
|
+
const inputTooFarAboveKeyboard = resolveBrowserViewport(windowObject).keyboardInsetBottom > 0 && inputGap > LIVE_INPUT_KEYBOARD_MAX_GAP_PX;
|
|
3727
|
+
if (rect.bottom <= targetBottom && rect.top >= topBoundary && !inputTooFarAboveKeyboard) return;
|
|
3728
|
+
const currentScrollY = normalizeScrollMetric(windowObject.scrollY ?? windowObject.pageYOffset ?? 0);
|
|
3729
|
+
const maxScrollY = getDocumentScrollMax(windowObject);
|
|
3730
|
+
const nextScrollY = clamp(currentScrollY + rect.bottom - targetBottom, 0, maxScrollY);
|
|
3731
|
+
if (Math.abs(nextScrollY - currentScrollY) < 1) return;
|
|
3732
|
+
try {
|
|
3733
|
+
windowObject.scrollTo(windowObject.scrollX ?? windowObject.pageXOffset ?? 0, nextScrollY);
|
|
3734
|
+
} catch {
|
|
3735
|
+
const scrollingElement = windowObject.document?.scrollingElement;
|
|
3736
|
+
if (scrollingElement instanceof HTMLElement) scrollingElement.scrollTop = nextScrollY;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
function shouldAlignDocumentViewportForShell(shell, scrollSurface) {
|
|
3740
|
+
const doc = shell.ownerDocument;
|
|
3741
|
+
const isDocumentScrollSurface = scrollSurface === doc.scrollingElement || scrollSurface === doc.documentElement || scrollSurface === doc.body;
|
|
3742
|
+
if (shell.dataset.viewportFullscreen === "true") return false;
|
|
3743
|
+
return isDocumentScrollSurface || shell.dataset.keyboardOpen === "true" || shell.dataset.fullscreen === "true" || shell.dataset.viewportFullscreen === "true";
|
|
3744
|
+
}
|
|
3745
|
+
function getDocumentScrollMax(windowObject) {
|
|
3746
|
+
const doc = windowObject.document;
|
|
3747
|
+
const scrollingElement = doc?.scrollingElement;
|
|
3748
|
+
if (scrollingElement instanceof HTMLElement) return getScrollbackMax(scrollingElement);
|
|
3749
|
+
const documentElement = doc?.documentElement;
|
|
3750
|
+
const body = doc?.body;
|
|
3751
|
+
const scrollHeight = Math.max(normalizeScrollMetric(documentElement?.scrollHeight ?? 0), normalizeScrollMetric(body?.scrollHeight ?? 0));
|
|
3752
|
+
const clientHeight = Math.max(normalizeScrollMetric(documentElement?.clientHeight ?? 0), normalizeScrollMetric(body?.clientHeight ?? 0), normalizeScrollMetric(windowObject.innerHeight ?? 0));
|
|
3753
|
+
return Math.max(0, scrollHeight - clientHeight);
|
|
3754
|
+
}
|
|
3755
|
+
function getScrollSurfaceBottomBoundary(scrollSurface, windowObject) {
|
|
3756
|
+
const scrollRect = scrollSurface.getBoundingClientRect();
|
|
3757
|
+
const doc = scrollSurface.ownerDocument;
|
|
3758
|
+
const isDocumentScrollSurface = scrollSurface === doc.scrollingElement || scrollSurface === doc.documentElement || scrollSurface === doc.body;
|
|
3759
|
+
const viewportBottom = getVisibleViewportBottomBoundary(windowObject);
|
|
3760
|
+
if (!isDocumentScrollSurface && hasUsableClientRect(scrollRect) && scrollRect.height > 0) return Math.min(scrollRect.bottom, viewportBottom);
|
|
3761
|
+
return viewportBottom;
|
|
3762
|
+
}
|
|
1619
3763
|
function isLiveInputSurfaceVisible(shell, terminalRoot, windowObject) {
|
|
1620
3764
|
const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
|
|
1621
3765
|
if (!(anchor instanceof HTMLElement)) return false;
|
|
1622
3766
|
const rect = anchor.getBoundingClientRect();
|
|
1623
3767
|
if (!hasUsableClientRect(rect)) return false;
|
|
1624
3768
|
const topBoundary = getViewportTopBoundary(shell);
|
|
1625
|
-
const bottomBoundary =
|
|
3769
|
+
const bottomBoundary = getVisibleViewportBottomBoundary(windowObject);
|
|
1626
3770
|
return rect.bottom > topBoundary + 1 && rect.top < bottomBoundary - 1;
|
|
1627
3771
|
}
|
|
3772
|
+
function isLiveInputSurfaceSafelyAboveViewportBottom(shell, terminalRoot, windowObject) {
|
|
3773
|
+
const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
|
|
3774
|
+
if (!(anchor instanceof HTMLElement)) return false;
|
|
3775
|
+
const rect = anchor.getBoundingClientRect();
|
|
3776
|
+
if (!hasUsableClientRect(rect)) return false;
|
|
3777
|
+
const topBoundary = getViewportTopBoundary(shell);
|
|
3778
|
+
const bottomBoundary = getVisibleViewportBottomBoundary(windowObject);
|
|
3779
|
+
const inputGap = bottomBoundary - rect.bottom;
|
|
3780
|
+
const keyboardOpen = resolveBrowserViewport(windowObject).keyboardInsetBottom > 0;
|
|
3781
|
+
return rect.top >= topBoundary && rect.bottom <= bottomBoundary - 24 && (!keyboardOpen || inputGap <= LIVE_INPUT_KEYBOARD_MAX_GAP_PX);
|
|
3782
|
+
}
|
|
3783
|
+
function shouldInstallViewportDebugOverlay(windowObject, shell) {
|
|
3784
|
+
if (shell.dataset.aittyViewportDebug === "true") return true;
|
|
3785
|
+
try {
|
|
3786
|
+
return new URLSearchParams(windowObject.location?.search ?? "").has("aittyViewportDebug");
|
|
3787
|
+
} catch {
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
function installViewportDebugOverlay(shell, terminalRoot, getScrollSurface, windowObject) {
|
|
3792
|
+
const doc = shell.ownerDocument;
|
|
3793
|
+
const overlay = doc.createElement("pre");
|
|
3794
|
+
let frameHandle = null;
|
|
3795
|
+
let observedScrollSurface = null;
|
|
3796
|
+
overlay.className = "aitty-viewport-debug";
|
|
3797
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
3798
|
+
doc.body?.append(overlay);
|
|
3799
|
+
const render = () => {
|
|
3800
|
+
frameHandle = null;
|
|
3801
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
3802
|
+
const layoutHeight = largestFinitePositiveNumber(windowObject.innerHeight, windowObject.document?.documentElement?.clientHeight) ?? firstFinitePositiveNumber(windowObject.document?.body?.clientHeight) ?? 0;
|
|
3803
|
+
const visualViewport = windowObject.visualViewport;
|
|
3804
|
+
const scrollSurface = getScrollSurface();
|
|
3805
|
+
if (scrollSurface !== observedScrollSurface) {
|
|
3806
|
+
observedScrollSurface?.removeEventListener("scroll", schedule);
|
|
3807
|
+
observedScrollSurface = scrollSurface;
|
|
3808
|
+
observedScrollSurface.addEventListener("scroll", schedule);
|
|
3809
|
+
}
|
|
3810
|
+
const scrollRect = scrollSurface.getBoundingClientRect?.();
|
|
3811
|
+
const anchorRect = resolveLiveInputSurfaceAnchor(terminalRoot)?.getBoundingClientRect?.() ?? null;
|
|
3812
|
+
const anchorBottom = anchorRect && hasUsableClientRect(anchorRect) ? anchorRect.bottom : NaN;
|
|
3813
|
+
const safeViewportBottom = getViewportBottomBoundary(windowObject);
|
|
3814
|
+
const viewportBottom = getVisibleViewportBottomBoundary(windowObject);
|
|
3815
|
+
const inputGap = Number.isFinite(anchorBottom) ? viewportBottom - anchorBottom : NaN;
|
|
3816
|
+
const activeElement = windowObject.document?.activeElement;
|
|
3817
|
+
const activeElementLabel = activeElement instanceof HTMLElement ? `${activeElement.tagName.toLowerCase()}${activeElement.hasAttribute("data-aitty-input") ? "[aitty-input]" : ""}` : "none";
|
|
3818
|
+
overlay.textContent = [
|
|
3819
|
+
"aitty viewport debug",
|
|
3820
|
+
`ua: ${navigator.userAgent}`,
|
|
3821
|
+
`ios: ${String(isLikelyIosBrowser(windowObject))}`,
|
|
3822
|
+
`standalone: ${String(isStandaloneDisplayMode(windowObject))}`,
|
|
3823
|
+
`fullscreen: ${shell.dataset.fullscreen === "true" ? "system" : "false"}`,
|
|
3824
|
+
`viewportFullscreen: ${shell.dataset.viewportFullscreen === "true"}`,
|
|
3825
|
+
`active: ${activeElementLabel}`,
|
|
3826
|
+
`inner: ${formatNumber(windowObject.innerWidth)} x ${formatNumber(windowObject.innerHeight)}`,
|
|
3827
|
+
`docEl: ${formatNumber(windowObject.document?.documentElement?.clientWidth)} x ${formatNumber(windowObject.document?.documentElement?.clientHeight)}`,
|
|
3828
|
+
`layoutH: ${formatNumber(layoutHeight)}`,
|
|
3829
|
+
`vv: ${formatNumber(visualViewport?.width)} x ${formatNumber(visualViewport?.height)} @ top ${formatNumber(visualViewport?.offsetTop)}`,
|
|
3830
|
+
`safeH: ${formatNumber(viewport.safeHeight)}`,
|
|
3831
|
+
`kbd: ${formatNumber(viewport.keyboardInsetBottom)}`,
|
|
3832
|
+
`chrome: ${formatNumber(viewport.browserChromeInsetBottom)}`,
|
|
3833
|
+
`bottom: ${formatNumber(viewportBottom)}`,
|
|
3834
|
+
`safeBottom: ${formatNumber(safeViewportBottom)}`,
|
|
3835
|
+
`scroll: top ${formatNumber(scrollSurface.scrollTop)} / max ${formatNumber(getScrollbackMax(scrollSurface))}`,
|
|
3836
|
+
`scrollRect: ${formatNumber(scrollRect?.top)}..${formatNumber(scrollRect?.bottom)} h ${formatNumber(scrollRect?.height)}`,
|
|
3837
|
+
`inputBottom: ${formatNumber(anchorBottom)}`,
|
|
3838
|
+
`inputGap: ${formatNumber(inputGap)}`
|
|
3839
|
+
].join("\n");
|
|
3840
|
+
};
|
|
3841
|
+
const schedule = () => {
|
|
3842
|
+
if (frameHandle !== null) return;
|
|
3843
|
+
frameHandle = windowObject.requestAnimationFrame(render);
|
|
3844
|
+
};
|
|
3845
|
+
const observer = new MutationObserver(schedule);
|
|
3846
|
+
observer.observe(terminalRoot, {
|
|
3847
|
+
attributes: true,
|
|
3848
|
+
childList: true,
|
|
3849
|
+
subtree: true
|
|
3850
|
+
});
|
|
3851
|
+
render();
|
|
3852
|
+
windowObject.addEventListener("resize", schedule);
|
|
3853
|
+
windowObject.addEventListener("scroll", schedule, true);
|
|
3854
|
+
windowObject.visualViewport?.addEventListener("resize", schedule);
|
|
3855
|
+
windowObject.visualViewport?.addEventListener("scroll", schedule);
|
|
3856
|
+
return () => {
|
|
3857
|
+
if (frameHandle !== null) {
|
|
3858
|
+
windowObject.cancelAnimationFrame(frameHandle);
|
|
3859
|
+
frameHandle = null;
|
|
3860
|
+
}
|
|
3861
|
+
observer.disconnect();
|
|
3862
|
+
windowObject.removeEventListener("resize", schedule);
|
|
3863
|
+
windowObject.removeEventListener("scroll", schedule, true);
|
|
3864
|
+
windowObject.visualViewport?.removeEventListener("resize", schedule);
|
|
3865
|
+
windowObject.visualViewport?.removeEventListener("scroll", schedule);
|
|
3866
|
+
observedScrollSurface?.removeEventListener("scroll", schedule);
|
|
3867
|
+
observedScrollSurface = null;
|
|
3868
|
+
overlay.remove();
|
|
3869
|
+
};
|
|
3870
|
+
}
|
|
3871
|
+
function formatNumber(value) {
|
|
3872
|
+
return typeof value === "number" && Number.isFinite(value) ? String(Math.round(value)) : "n/a";
|
|
3873
|
+
}
|
|
1628
3874
|
function resolveLiveInputSurfaceAnchor(terminalRoot) {
|
|
1629
3875
|
const visibleCursors = Array.from(terminalRoot.querySelectorAll(".term-grid .term-cursor")).filter((cursor) => cursor instanceof HTMLElement && !cursor.hidden && cursor.getAttribute("aria-hidden") !== "true");
|
|
1630
3876
|
if (visibleCursors.length > 0) return visibleCursors[visibleCursors.length - 1];
|
|
@@ -1649,18 +3895,38 @@ function getViewportTopBoundary(shell) {
|
|
|
1649
3895
|
return 0;
|
|
1650
3896
|
}
|
|
1651
3897
|
function getViewportBottomBoundary(windowObject) {
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
return Number.POSITIVE_INFINITY;
|
|
3898
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
3899
|
+
return viewport.offsetTop + viewport.safeHeight;
|
|
3900
|
+
}
|
|
3901
|
+
function getVisibleViewportBottomBoundary(windowObject) {
|
|
3902
|
+
const viewport = resolveBrowserViewport(windowObject);
|
|
3903
|
+
return viewport.offsetTop + viewport.height - viewport.browserChromeInsetBottom;
|
|
1659
3904
|
}
|
|
1660
3905
|
function firstFinitePositiveNumber(...candidates) {
|
|
1661
3906
|
for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) return candidate;
|
|
1662
3907
|
return null;
|
|
1663
3908
|
}
|
|
3909
|
+
function largestFinitePositiveNumber(...candidates) {
|
|
3910
|
+
const values = candidates.filter((candidate) => typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0);
|
|
3911
|
+
return values.length > 0 ? Math.max(...values) : null;
|
|
3912
|
+
}
|
|
3913
|
+
function resolveBrowserChromeKeyboardGuard(windowObject, keyboardVisible) {
|
|
3914
|
+
if (isStandaloneDisplayMode(windowObject) || !isLikelyIosBrowser(windowObject)) return 0;
|
|
3915
|
+
return IOS_BROWSER_CHROME_KEYBOARD_GUARD_PX;
|
|
3916
|
+
}
|
|
3917
|
+
function isStandaloneDisplayMode(windowObject) {
|
|
3918
|
+
return windowObject.navigator?.standalone === true || windowObject.matchMedia?.("(display-mode: standalone)").matches === true || windowObject.matchMedia?.("(display-mode: fullscreen)").matches === true;
|
|
3919
|
+
}
|
|
3920
|
+
function isLikelyIosBrowser(windowObject) {
|
|
3921
|
+
const navigatorLike = windowObject.navigator;
|
|
3922
|
+
const platform = navigatorLike?.platform ?? "";
|
|
3923
|
+
const userAgent = navigatorLike?.userAgent ?? "";
|
|
3924
|
+
const maxTouchPoints = navigatorLike?.maxTouchPoints ?? 0;
|
|
3925
|
+
return /iP(?:hone|ad|od)/.test(platform) || /iP(?:hone|ad|od)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
|
|
3926
|
+
}
|
|
3927
|
+
function normalizeViewportOffset(value) {
|
|
3928
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
3929
|
+
}
|
|
1664
3930
|
function hasUsableClientRect(rect) {
|
|
1665
3931
|
if (!rect) return false;
|
|
1666
3932
|
return Number.isFinite(rect.top) && Number.isFinite(rect.bottom) && Number.isFinite(rect.left) && Number.isFinite(rect.right) && !(rect.top === 0 && rect.bottom === 0 && rect.left === 0 && rect.right === 0);
|
|
@@ -1721,11 +3987,12 @@ function getOptionalElement(doc, selector) {
|
|
|
1721
3987
|
function hasElementOverride(elements, key) {
|
|
1722
3988
|
return Object.prototype.hasOwnProperty.call(elements, key);
|
|
1723
3989
|
}
|
|
1724
|
-
function createTerminalThemeController(doc, shell, terminalRoot, theme) {
|
|
3990
|
+
function createTerminalThemeController(doc, shell, terminalRoot, theme, target) {
|
|
1725
3991
|
const explicitTheme = normalizeThemeName(theme);
|
|
1726
3992
|
let observingTheme = false;
|
|
3993
|
+
const usesDocumentTheme = target === "document";
|
|
1727
3994
|
const clearProtocolOverrides = () => {
|
|
1728
|
-
clearTerminalThemeProtocolOverrides(doc.documentElement);
|
|
3995
|
+
if (usesDocumentTheme) clearTerminalThemeProtocolOverrides(doc.documentElement);
|
|
1729
3996
|
clearTerminalThemeProtocolOverrides(shell);
|
|
1730
3997
|
clearTerminalThemeProtocolOverrides(terminalRoot);
|
|
1731
3998
|
};
|
|
@@ -1752,17 +4019,26 @@ function createTerminalThemeController(doc, shell, terminalRoot, theme) {
|
|
|
1752
4019
|
const syncTheme = (nextTheme) => {
|
|
1753
4020
|
const configuredTheme = normalizeThemeName(nextTheme);
|
|
1754
4021
|
if (!configuredTheme) {
|
|
1755
|
-
if (doc.documentElement.dataset.theme !== void 0) delete doc.documentElement.dataset.theme;
|
|
1756
|
-
doc.documentElement.style.removeProperty("color-scheme");
|
|
4022
|
+
if (usesDocumentTheme && doc.documentElement.dataset.theme !== void 0) delete doc.documentElement.dataset.theme;
|
|
4023
|
+
if (usesDocumentTheme) doc.documentElement.style.removeProperty("color-scheme");
|
|
4024
|
+
shell.style.removeProperty("color-scheme");
|
|
1757
4025
|
delete shell.dataset.theme;
|
|
1758
4026
|
delete terminalRoot.dataset.theme;
|
|
1759
4027
|
return;
|
|
1760
4028
|
}
|
|
1761
|
-
|
|
4029
|
+
const colorScheme = configuredTheme === "light" || configuredTheme === "dark" ? configuredTheme : void 0;
|
|
4030
|
+
if (usesDocumentTheme) if (colorScheme) doc.documentElement.style.setProperty("color-scheme", colorScheme);
|
|
1762
4031
|
else doc.documentElement.style.removeProperty("color-scheme");
|
|
1763
|
-
if (
|
|
4032
|
+
if (colorScheme) shell.style.setProperty("color-scheme", colorScheme);
|
|
4033
|
+
else shell.style.removeProperty("color-scheme");
|
|
4034
|
+
if (usesDocumentTheme && doc.documentElement.dataset.theme !== configuredTheme) doc.documentElement.dataset.theme = configuredTheme;
|
|
1764
4035
|
if (shell.dataset.theme !== configuredTheme) shell.dataset.theme = configuredTheme;
|
|
1765
4036
|
if (terminalRoot.dataset.theme !== configuredTheme) terminalRoot.dataset.theme = configuredTheme;
|
|
4037
|
+
const textarea = resolveTerminalTextarea(terminalRoot);
|
|
4038
|
+
if (textarea) {
|
|
4039
|
+
const themeValue = configuredTheme === "light" || configuredTheme === "dark" ? configuredTheme : "inherit";
|
|
4040
|
+
textarea.style.colorScheme = themeValue;
|
|
4041
|
+
}
|
|
1766
4042
|
};
|
|
1767
4043
|
const syncInternalTheme = (nextTheme) => {
|
|
1768
4044
|
pauseThemeObservation();
|
|
@@ -1780,7 +4056,7 @@ function createTerminalThemeController(doc, shell, terminalRoot, theme) {
|
|
|
1780
4056
|
mutationObserver?.disconnect();
|
|
1781
4057
|
},
|
|
1782
4058
|
getTheme() {
|
|
1783
|
-
return doc.documentElement.dataset.theme;
|
|
4059
|
+
return shell.dataset.theme ?? doc.documentElement.dataset.theme;
|
|
1784
4060
|
},
|
|
1785
4061
|
syncProtocolUpdate(update) {
|
|
1786
4062
|
let themeChanged = false;
|
|
@@ -1789,7 +4065,7 @@ function createTerminalThemeController(doc, shell, terminalRoot, theme) {
|
|
|
1789
4065
|
syncInternalTheme(update.theme);
|
|
1790
4066
|
themeChanged = true;
|
|
1791
4067
|
}
|
|
1792
|
-
applyTerminalThemeProtocolUpdate(doc.documentElement, update);
|
|
4068
|
+
if (usesDocumentTheme) applyTerminalThemeProtocolUpdate(doc.documentElement, update);
|
|
1793
4069
|
applyTerminalThemeProtocolUpdate(shell, update);
|
|
1794
4070
|
applyTerminalThemeProtocolUpdate(terminalRoot, update);
|
|
1795
4071
|
return themeChanged || hasTerminalThemeProtocolUpdate(update);
|
|
@@ -1803,6 +4079,17 @@ function createTerminalThemeController(doc, shell, terminalRoot, theme) {
|
|
|
1803
4079
|
}
|
|
1804
4080
|
};
|
|
1805
4081
|
}
|
|
4082
|
+
function resolveTerminalTextarea(terminalRoot) {
|
|
4083
|
+
const inputOwner = terminalRoot.dataset.aittyInputOwner;
|
|
4084
|
+
if (inputOwner) {
|
|
4085
|
+
const ownedTextarea = Array.from(terminalRoot.ownerDocument.querySelectorAll("textarea[data-aitty-input]")).find((textarea) => {
|
|
4086
|
+
return textarea instanceof HTMLTextAreaElement && textarea.dataset.aittyInputOwner === inputOwner;
|
|
4087
|
+
});
|
|
4088
|
+
if (ownedTextarea) return ownedTextarea;
|
|
4089
|
+
}
|
|
4090
|
+
const localTextarea = terminalRoot.querySelector("textarea[data-aitty-input]");
|
|
4091
|
+
return localTextarea instanceof HTMLTextAreaElement ? localTextarea : null;
|
|
4092
|
+
}
|
|
1806
4093
|
function clearTerminalThemeProtocolOverrides(element) {
|
|
1807
4094
|
for (const variableName of TERMINAL_THEME_PROTOCOL_VARIABLES) element.style.removeProperty(variableName);
|
|
1808
4095
|
}
|
|
@@ -1816,9 +4103,6 @@ function applyTerminalThemeProtocolUpdate(element, update) {
|
|
|
1816
4103
|
function hasTerminalThemeProtocolUpdate(update) {
|
|
1817
4104
|
return Object.keys(update.colors).length > 0 || update.palette.length > 0 || Boolean(update.theme);
|
|
1818
4105
|
}
|
|
1819
|
-
function normalizeThemeName(theme) {
|
|
1820
|
-
return normalizeTheme(theme);
|
|
1821
|
-
}
|
|
1822
4106
|
function normalizeConnectionState(value) {
|
|
1823
4107
|
switch (value) {
|
|
1824
4108
|
case "open":
|
|
@@ -1852,12 +4136,55 @@ function cloneTerminalStatusSnapshot(status) {
|
|
|
1852
4136
|
function areTerminalStatusSnapshotsEqual(left, right) {
|
|
1853
4137
|
return left.connection === right.connection && left.message === right.message && left.output === right.output && left.sessionState === right.sessionState && left.loading.message === right.loading.message && left.loading.visible === right.loading.visible;
|
|
1854
4138
|
}
|
|
1855
|
-
function toWebSocketUrl(sessionUrl) {
|
|
4139
|
+
function toWebSocketUrl(sessionUrl, clientId) {
|
|
1856
4140
|
const url = new URL(sessionUrl.href);
|
|
1857
4141
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1858
4142
|
url.pathname = "/ws";
|
|
4143
|
+
if (clientId) url.searchParams.set("cid", clientId);
|
|
1859
4144
|
return url.toString();
|
|
1860
4145
|
}
|
|
4146
|
+
function resolveStoredAittyClientId(windowObject, sessionUrl) {
|
|
4147
|
+
const key = resolveAittyClientIdStorageKey(sessionUrl);
|
|
4148
|
+
const stored = normalizeStoredAittyClientId(readSessionStorageItem(windowObject, key));
|
|
4149
|
+
if (stored) return stored;
|
|
4150
|
+
const generated = createAittyBrowserClientId(windowObject);
|
|
4151
|
+
writeSessionStorageItem(windowObject, key, generated);
|
|
4152
|
+
return generated;
|
|
4153
|
+
}
|
|
4154
|
+
function persistAittyClientId(windowObject, sessionUrl, clientId) {
|
|
4155
|
+
const normalized = normalizeStoredAittyClientId(clientId);
|
|
4156
|
+
if (!normalized) return;
|
|
4157
|
+
writeSessionStorageItem(windowObject, resolveAittyClientIdStorageKey(sessionUrl), normalized);
|
|
4158
|
+
}
|
|
4159
|
+
function resolveAittyClientIdStorageKey(sessionUrl) {
|
|
4160
|
+
const token = sessionUrl.searchParams.get("t") ?? "";
|
|
4161
|
+
return `aitty:client-id:${sessionUrl.origin}${sessionUrl.pathname}:${token.slice(0, 12)}`;
|
|
4162
|
+
}
|
|
4163
|
+
function createAittyBrowserClientId(windowObject) {
|
|
4164
|
+
const crypto = windowObject.crypto;
|
|
4165
|
+
if (crypto && typeof crypto.randomUUID === "function") return `b-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
4166
|
+
const values = new Uint8Array(12);
|
|
4167
|
+
if (crypto && typeof crypto.getRandomValues === "function") crypto.getRandomValues(values);
|
|
4168
|
+
else for (let index = 0; index < values.length; index += 1) values[index] = Math.floor(Math.random() * 256);
|
|
4169
|
+
return `b-${Array.from(values, (value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
4170
|
+
}
|
|
4171
|
+
function normalizeStoredAittyClientId(value) {
|
|
4172
|
+
if (typeof value !== "string") return null;
|
|
4173
|
+
const normalized = value.trim();
|
|
4174
|
+
return /^[A-Za-z0-9_-]{1,80}$/.test(normalized) ? normalized : null;
|
|
4175
|
+
}
|
|
4176
|
+
function readSessionStorageItem(windowObject, key) {
|
|
4177
|
+
try {
|
|
4178
|
+
return windowObject.sessionStorage?.getItem(key) ?? null;
|
|
4179
|
+
} catch {
|
|
4180
|
+
return null;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
function writeSessionStorageItem(windowObject, key, value) {
|
|
4184
|
+
try {
|
|
4185
|
+
windowObject.sessionStorage?.setItem(key, value);
|
|
4186
|
+
} catch {}
|
|
4187
|
+
}
|
|
1861
4188
|
function resolveAittySessionUrl(windowObject, src) {
|
|
1862
4189
|
if (src instanceof URL) return new URL(src.href);
|
|
1863
4190
|
if (typeof src === "string" && src.trim()) return new URL(src, windowObject.location.href);
|
|
@@ -1884,26 +4211,81 @@ async function fetchRuntimeKindFromSession(sessionUrl, fetchRuntimeKind) {
|
|
|
1884
4211
|
return "pty";
|
|
1885
4212
|
}
|
|
1886
4213
|
}
|
|
4214
|
+
async function uploadClipboardImage(file, sessionUrl, fetchRuntime) {
|
|
4215
|
+
if (!fetchRuntime || !sessionUrl || !file.type.startsWith("image/")) return null;
|
|
4216
|
+
const uploadUrl = new URL("/clipboard/image", sessionUrl);
|
|
4217
|
+
const token = sessionUrl.searchParams.get("t");
|
|
4218
|
+
uploadUrl.hash = "";
|
|
4219
|
+
if (token) uploadUrl.searchParams.set("t", token);
|
|
4220
|
+
const response = await fetchRuntime(uploadUrl.toString(), {
|
|
4221
|
+
body: file,
|
|
4222
|
+
headers: { "Content-Type": file.type },
|
|
4223
|
+
method: "POST"
|
|
4224
|
+
});
|
|
4225
|
+
if (!response.ok) return null;
|
|
4226
|
+
const body = await response.json();
|
|
4227
|
+
if (!body || typeof body !== "object" || !("path" in body)) return null;
|
|
4228
|
+
const { path } = body;
|
|
4229
|
+
return typeof path === "string" && path.length > 0 ? path : null;
|
|
4230
|
+
}
|
|
4231
|
+
async function pasteClipboardImageFromShortcut(windowObject, sessionUrl, event) {
|
|
4232
|
+
const image = readClipboardImageFromPasteEvent(event) ?? await readClipboardImage(windowObject);
|
|
4233
|
+
if (!image) return {
|
|
4234
|
+
allowTerminalFallback: isLoopbackBrowserHost(windowObject.location?.hostname),
|
|
4235
|
+
data: null
|
|
4236
|
+
};
|
|
4237
|
+
const path = await uploadClipboardImage(image, sessionUrl, windowObject.fetch?.bind(windowObject));
|
|
4238
|
+
if (path) return `${path} `;
|
|
4239
|
+
return {
|
|
4240
|
+
allowTerminalFallback: isLoopbackBrowserHost(windowObject.location?.hostname),
|
|
4241
|
+
data: null
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
function isLoopbackBrowserHost(hostname) {
|
|
4245
|
+
const normalizedHostname = (hostname ?? "").toLowerCase();
|
|
4246
|
+
return normalizedHostname === "localhost" || normalizedHostname === "127.0.0.1" || normalizedHostname === "::1" || normalizedHostname.endsWith(".localhost");
|
|
4247
|
+
}
|
|
4248
|
+
function resolveBrowserPasteShortcut(windowObject) {
|
|
4249
|
+
if (windowObject.isSecureContext || isLoopbackBrowserHost(windowObject.location?.hostname)) return "ctrl-v";
|
|
4250
|
+
return "cmd-v";
|
|
4251
|
+
}
|
|
4252
|
+
function readClipboardImageFromPasteEvent(event) {
|
|
4253
|
+
const clipboardData = event?.clipboardData;
|
|
4254
|
+
if (!clipboardData) return null;
|
|
4255
|
+
for (const item of Array.from(clipboardData.items)) {
|
|
4256
|
+
if (item.kind !== "file" || !item.type.startsWith("image/")) continue;
|
|
4257
|
+
const file = item.getAsFile();
|
|
4258
|
+
if (file) return file;
|
|
4259
|
+
}
|
|
4260
|
+
for (const file of Array.from(clipboardData.files)) if (file.type.startsWith("image/")) return file;
|
|
4261
|
+
return null;
|
|
4262
|
+
}
|
|
4263
|
+
async function readClipboardImage(windowObject) {
|
|
4264
|
+
const clipboard = windowObject.navigator?.clipboard;
|
|
4265
|
+
if (!clipboard || typeof clipboard.read !== "function") return null;
|
|
4266
|
+
try {
|
|
4267
|
+
const items = await clipboard.read();
|
|
4268
|
+
for (const item of items) {
|
|
4269
|
+
const imageType = item.types.find((type) => type.startsWith("image/"));
|
|
4270
|
+
if (!imageType) continue;
|
|
4271
|
+
return item.getType(imageType);
|
|
4272
|
+
}
|
|
4273
|
+
} catch {
|
|
4274
|
+
return null;
|
|
4275
|
+
}
|
|
4276
|
+
return null;
|
|
4277
|
+
}
|
|
1887
4278
|
function syncDimensions(element, cols, rows) {
|
|
1888
4279
|
element.dataset.cols = String(cols);
|
|
1889
4280
|
element.dataset.rows = String(rows);
|
|
4281
|
+
element.style.setProperty("--term-cols", String(cols));
|
|
4282
|
+
element.style.setProperty("--term-rows", String(rows));
|
|
1890
4283
|
}
|
|
1891
4284
|
function syncTranscriptMetadata(element, transcriptLineCount, archivedLineCount) {
|
|
1892
4285
|
element.dataset.transcriptLines = String(transcriptLineCount);
|
|
1893
4286
|
element.dataset.archivedLines = String(archivedLineCount);
|
|
1894
4287
|
element.classList.toggle("has-scrollback", archivedLineCount > 0 || element.querySelector(".term-scrollback-row") !== null);
|
|
1895
4288
|
}
|
|
1896
|
-
function syncTrailingTerminalBlankRows(element) {
|
|
1897
|
-
const rows = Array.from(element.querySelectorAll(".term-grid .term-row"));
|
|
1898
|
-
let trailingBlank = true;
|
|
1899
|
-
for (let index = rows.length - 1; index >= 0; index -= 1) {
|
|
1900
|
-
const row = rows[index];
|
|
1901
|
-
if (!(row instanceof HTMLElement)) continue;
|
|
1902
|
-
const isBlank = (row.textContent ?? "").trim().length === 0 && !hasVisibleTerminalPaint(row);
|
|
1903
|
-
row.classList.toggle("term-row--trailing-blank", trailingBlank && isBlank);
|
|
1904
|
-
if (!isBlank) trailingBlank = false;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
4289
|
function trimReconciledPreservedTranscriptLines(preservedLines, liveLines) {
|
|
1908
4290
|
if (preservedLines.length === 0 || liveLines.length === 0) return preservedLines;
|
|
1909
4291
|
const normalizedLiveLines = normalizePreservedTranscriptLines(liveLines);
|
|
@@ -1913,30 +4295,30 @@ function trimReconciledPreservedTranscriptLines(preservedLines, liveLines) {
|
|
|
1913
4295
|
}
|
|
1914
4296
|
return preservedLines;
|
|
1915
4297
|
}
|
|
4298
|
+
function normalizeTranscriptScrollbackLimit(value) {
|
|
4299
|
+
if (value === void 0) return null;
|
|
4300
|
+
if (!Number.isFinite(value)) return null;
|
|
4301
|
+
return Math.max(0, Math.floor(value));
|
|
4302
|
+
}
|
|
4303
|
+
function trimTranscriptLines(lines, limit) {
|
|
4304
|
+
if (limit === null) return lines;
|
|
4305
|
+
return limit > 0 ? lines.slice(-limit) : [];
|
|
4306
|
+
}
|
|
1916
4307
|
function normalizePreservedTranscriptLines(lines) {
|
|
1917
4308
|
return lines.map((line) => line.trimEnd()).filter((line, index, array) => line.length > 0 || index > 0 && index < array.length - 1);
|
|
1918
4309
|
}
|
|
1919
4310
|
function looksLikePromptLine(text) {
|
|
1920
4311
|
return /^\s*(?:[$>#]|[^\s]+[@:][^\s]+[$#])\s?/.test(text);
|
|
1921
4312
|
}
|
|
1922
|
-
function
|
|
4313
|
+
function toWebSocketPayload(bytes) {
|
|
4314
|
+
if (bytes.buffer instanceof ArrayBuffer) return bytes;
|
|
1923
4315
|
const copy = new Uint8Array(bytes.byteLength);
|
|
1924
4316
|
copy.set(bytes);
|
|
1925
|
-
return copy
|
|
1926
|
-
}
|
|
1927
|
-
function concatChunks(chunks) {
|
|
1928
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1929
|
-
const payload = new Uint8Array(totalLength);
|
|
1930
|
-
let offset = 0;
|
|
1931
|
-
for (const chunk of chunks) {
|
|
1932
|
-
payload.set(chunk, offset);
|
|
1933
|
-
offset += chunk.length;
|
|
1934
|
-
}
|
|
1935
|
-
return payload;
|
|
4317
|
+
return copy;
|
|
1936
4318
|
}
|
|
1937
4319
|
if (typeof document !== "undefined") {
|
|
1938
4320
|
defineAittyTerminalElement();
|
|
1939
|
-
if (shouldAutoMount(document)) mountTerminalApp().catch((error) => {
|
|
4321
|
+
if (shouldAutoMount(document)) mountTerminalApp(document, resolveAutoMountOptions(document)).catch((error) => {
|
|
1940
4322
|
const shell = document.querySelector("[data-shell]");
|
|
1941
4323
|
const status = document.querySelector("[data-terminal-status]");
|
|
1942
4324
|
const loading = document.querySelector("[data-terminal-loading]");
|
|
@@ -1951,7 +4333,19 @@ if (typeof document !== "undefined") {
|
|
|
1951
4333
|
}
|
|
1952
4334
|
function shouldAutoMount(doc) {
|
|
1953
4335
|
const shell = doc.querySelector("[data-shell]");
|
|
1954
|
-
return shell instanceof HTMLElement && !shell.hasAttribute("data-aitty-manual");
|
|
4336
|
+
return shell instanceof HTMLElement && shell.hasAttribute("data-aitty-autostart") && !shell.hasAttribute("data-aitty-manual");
|
|
4337
|
+
}
|
|
4338
|
+
function resolveAutoMountOptions(doc) {
|
|
4339
|
+
const shell = doc.querySelector("[data-shell]");
|
|
4340
|
+
if (!(shell instanceof HTMLElement)) return {};
|
|
4341
|
+
return {
|
|
4342
|
+
config: { appearance: {
|
|
4343
|
+
fontSize: parseOptionalNumber(shell.dataset.aittyFontSize),
|
|
4344
|
+
lineHeight: parseOptionalNumber(shell.dataset.aittyLineHeight),
|
|
4345
|
+
themeTarget: shell.hasAttribute("data-aitty-document-theme") ? "document" : "container"
|
|
4346
|
+
} },
|
|
4347
|
+
shellControls: shell.hasAttribute("data-aitty-shell-controls")
|
|
4348
|
+
};
|
|
1955
4349
|
}
|
|
1956
4350
|
//#endregion
|
|
1957
|
-
export { createBufferedTerminalWriter, defineAittyTerminalElement, mountAitty, mountTerminalApp };
|
|
4351
|
+
export { MOBILE_COMPOSER_PRIMARY_CONTROLS, createBufferedTerminalWriter, defineAittyTerminalElement, isMobileLiveInputTapTarget, mountAitty, mountTerminalApp, resolveBrowserViewport, resolveMobileComposerControlSequence, resolveMobileComposerExpandedListHeight, resolveMobileFocusProxyPosition, resolveMobileKeyboardPromptAlignDelays, resolveTerminalOutputFrameIntervalMs, shouldEnableMobileFocusProxyPointerEvents, shouldFollowLiveOutputOnUserInputData };
|