@aitty/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1957 @@
1
+ import { nextAltScreenState, parseCsiParams } from "./ansi-sequences.js";
2
+ import { createAnsiStyleTracker } from "./ansi-style-tracker.js";
3
+ import { BrowserTerminalRenderer } from "./browser-terminal-renderer.js";
4
+ import { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey } from "./terminal-input-policies.js";
5
+ import { createTerminalThemeProtocolParser, resolveTerminalThemeQueryResponse } from "./terminal-theme-protocol.js";
6
+ import { WTerm } from "@wterm/dom";
7
+ import { createPongControlFrame, createResizeControlFrame, normalizeTheme, parseAittyControlFrame } from "@aitty/protocol";
8
+ //#region src/frontend/terminal-app.ts
9
+ const SUPERSEDED_CLOSE_CODE = 4001;
10
+ const DEFAULT_MAX_BYTES_PER_FRAME = 32 * 1024;
11
+ const DEFAULT_TRANSCRIPT_SCROLLBACK_LIMIT = 1e3;
12
+ const DEFAULT_RECONNECT_DELAY_MS = 250;
13
+ const SCREEN_REDRAW_ARCHIVE_SUPPRESS_MS = 500;
14
+ const TERMINAL_THEME_PROTOCOL_VARIABLES = [
15
+ "--theme-term-bg",
16
+ "--theme-term-cursor",
17
+ "--theme-term-fg",
18
+ ...Array.from({ length: 16 }, (_, index) => `--theme-term-color-${index}`)
19
+ ];
20
+ /**
21
+ * Batches raw PTY bytes into animation frames without splitting UTF-8 codepoints or
22
+ * incomplete ANSI sequences. This is the only throttling layer in the browser path.
23
+ */
24
+ function createBufferedTerminalWriter(target, scheduler = {}, options = {}) {
25
+ const requestFrame = scheduler.requestFrame ?? ((callback) => requestBrowserFrame(callback));
26
+ const cancelFrame = scheduler.cancelFrame ?? ((handle) => cancelBrowserFrame(handle));
27
+ const maxBytesPerFrame = Math.max(1, options.maxBytesPerFrame ?? DEFAULT_MAX_BYTES_PER_FRAME);
28
+ let chunks = [];
29
+ let frameHandle = null;
30
+ let headOffset = 0;
31
+ const getPendingBytes = () => {
32
+ if (chunks.length === 0) return new Uint8Array(0);
33
+ if (chunks.length === 1) return headOffset === 0 ? chunks[0] : chunks[0].subarray(headOffset);
34
+ return concatChunks([chunks[0].subarray(headOffset), ...chunks.slice(1)]);
35
+ };
36
+ const consumePendingBytes = (byteCount) => {
37
+ let remainingBytes = byteCount;
38
+ while (chunks.length > 0 && remainingBytes > 0) {
39
+ const availableBytes = chunks[0].length - headOffset;
40
+ if (availableBytes <= remainingBytes) {
41
+ chunks.shift();
42
+ headOffset = 0;
43
+ remainingBytes -= availableBytes;
44
+ continue;
45
+ }
46
+ headOffset += remainingBytes;
47
+ remainingBytes = 0;
48
+ }
49
+ };
50
+ const isUtf8ContinuationByte = (value) => (value & 192) === 128;
51
+ const isUtf8LeadByte = (value) => (value & 128) !== 0 && !isUtf8ContinuationByte(value);
52
+ const resolveUtf8SafeBoundary = (bytes, preferredEnd) => {
53
+ if (preferredEnd <= 0 || preferredEnd >= bytes.length) return preferredEnd;
54
+ let index = preferredEnd;
55
+ while (index > 0 && isUtf8ContinuationByte(bytes[index])) index -= 1;
56
+ if (index < preferredEnd && isUtf8LeadByte(bytes[index])) return index;
57
+ return preferredEnd;
58
+ };
59
+ const resolveAnsiSafeBoundary = (bytes, preferredEnd) => {
60
+ if (preferredEnd <= 0 || preferredEnd >= bytes.length) return preferredEnd;
61
+ let lastEscapeIndex = -1;
62
+ for (let index = 0; index < preferredEnd; index += 1) if (bytes[index] === 27) lastEscapeIndex = index;
63
+ if (lastEscapeIndex < 0) return preferredEnd;
64
+ let index = lastEscapeIndex + 1;
65
+ if (index >= preferredEnd) return lastEscapeIndex;
66
+ const nextByte = bytes[index];
67
+ if (nextByte === 91) {
68
+ index += 1;
69
+ while (index < preferredEnd) {
70
+ const value = bytes[index];
71
+ if (value >= 64 && value <= 126) return preferredEnd;
72
+ index += 1;
73
+ }
74
+ return lastEscapeIndex;
75
+ }
76
+ if (nextByte === 93) {
77
+ index += 1;
78
+ while (index < preferredEnd) {
79
+ const value = bytes[index];
80
+ if (value === 7) return preferredEnd;
81
+ if (value === 27 && index + 1 < preferredEnd && bytes[index + 1] === 92) return preferredEnd;
82
+ index += 1;
83
+ }
84
+ return lastEscapeIndex;
85
+ }
86
+ return preferredEnd;
87
+ };
88
+ const resolveTrailingAnsiCarryLength = (bytes) => {
89
+ let lastEscapeIndex = -1;
90
+ for (let index = 0; index < bytes.length; index += 1) if (bytes[index] === 27) lastEscapeIndex = index;
91
+ if (lastEscapeIndex < 0 || lastEscapeIndex >= bytes.length - 1) return lastEscapeIndex < 0 ? 0 : bytes.length - lastEscapeIndex;
92
+ const nextByte = bytes[lastEscapeIndex + 1];
93
+ if (nextByte === 91) {
94
+ for (let index = lastEscapeIndex + 2; index < bytes.length; index += 1) {
95
+ const value = bytes[index];
96
+ if (value >= 64 && value <= 126) return 0;
97
+ }
98
+ return bytes.length - lastEscapeIndex;
99
+ }
100
+ if (nextByte === 93) {
101
+ for (let index = lastEscapeIndex + 2; index < bytes.length; index += 1) {
102
+ const value = bytes[index];
103
+ if (value === 7) return 0;
104
+ if (value === 27 && index + 1 < bytes.length && bytes[index + 1] === 92) return 0;
105
+ }
106
+ return bytes.length - lastEscapeIndex;
107
+ }
108
+ return 0;
109
+ };
110
+ const resolveSafeSplitBoundary = (bytes, preferredEnd) => {
111
+ let boundary = resolveUtf8SafeBoundary(bytes, preferredEnd);
112
+ boundary = resolveAnsiSafeBoundary(bytes, boundary);
113
+ return boundary > 0 ? boundary : preferredEnd;
114
+ };
115
+ const flush = () => {
116
+ frameHandle = null;
117
+ if (chunks.length === 0) return;
118
+ const pendingBytes = getPendingBytes();
119
+ if (pendingBytes.length === 0) {
120
+ chunks = [];
121
+ headOffset = 0;
122
+ return;
123
+ }
124
+ const preferredEnd = Math.min(maxBytesPerFrame, pendingBytes.length);
125
+ const safeEnd = resolveSafeSplitBoundary(pendingBytes, preferredEnd);
126
+ const trailingAnsiCarryLength = preferredEnd === pendingBytes.length ? resolveTrailingAnsiCarryLength(pendingBytes) : 0;
127
+ 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
+ }
134
+ const payload = pendingBytes.subarray(0, payloadByteCount);
135
+ target.write(payload);
136
+ consumePendingBytes(payloadByteCount);
137
+ if (chunks.length > 0) frameHandle = requestFrame(() => {
138
+ flush();
139
+ });
140
+ };
141
+ return {
142
+ destroy() {
143
+ if (frameHandle !== null) {
144
+ cancelFrame(frameHandle);
145
+ frameHandle = null;
146
+ }
147
+ chunks = [];
148
+ headOffset = 0;
149
+ },
150
+ discardPending() {
151
+ if (frameHandle !== null) {
152
+ cancelFrame(frameHandle);
153
+ frameHandle = null;
154
+ }
155
+ chunks = [];
156
+ headOffset = 0;
157
+ },
158
+ enqueue(chunk) {
159
+ if (chunk.length === 0) return;
160
+ chunks.push(chunk);
161
+ if (frameHandle === null) frameHandle = requestFrame(() => {
162
+ flush();
163
+ });
164
+ }
165
+ };
166
+ }
167
+ async function mountTerminalApp(doc = document, dependencies = {}) {
168
+ const elements = dependencies.elements ?? {};
169
+ const shell = elements.shell ?? getRequiredElement(doc, "[data-shell]");
170
+ const status = hasElementOverride(elements, "status") ? elements.status ?? null : getOptionalElement(doc, "[data-terminal-status]");
171
+ const loading = hasElementOverride(elements, "loading") ? elements.loading ?? null : getOptionalElement(doc, "[data-terminal-loading]");
172
+ const terminalRoot = elements.terminalRoot ?? getRequiredElement(doc, "[data-terminal-root]");
173
+ terminalRoot.classList.add("aitty-terminal-root");
174
+ const windowObject = doc.defaultView ?? window;
175
+ const sessionUrl = resolveAittySessionUrl(windowObject, dependencies.src);
176
+ const themeController = createTerminalThemeController(doc, shell, terminalRoot, dependencies.theme);
177
+ const resolvedRuntimeKind = (dependencies.resolveRuntimeKind ?? ((resolverDoc, resolverShell) => resolveShellRuntimeKind(resolverDoc, resolverShell, sessionUrl)))(doc, shell);
178
+ const runtimeKind = typeof resolvedRuntimeKind === "string" ? resolvedRuntimeKind : await resolvedRuntimeKind;
179
+ shell.dataset.runtime = runtimeKind;
180
+ shell.dataset.terminalPipeline = "raw";
181
+ terminalRoot.dataset.terminalPipeline = "raw";
182
+ const frameScheduler = dependencies.frameScheduler ?? {};
183
+ const requestFrame = frameScheduler.requestFrame ?? ((callback) => windowObject.requestAnimationFrame(callback));
184
+ const cancelFrame = frameScheduler.cancelFrame ?? ((handle) => windowObject.cancelAnimationFrame(handle));
185
+ const reconnectDelayMs = Math.max(0, dependencies.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS);
186
+ const getScrollSurface = () => resolveScrollSurface(doc, terminalRoot, dependencies.scroll);
187
+ const encoder = new TextEncoder();
188
+ const ansiStyleTracker = createAnsiStyleTracker();
189
+ const themeProtocolParser = createTerminalThemeProtocolParser();
190
+ let transport = null;
191
+ let term = null;
192
+ let transcriptArchive = null;
193
+ let writer = null;
194
+ let archiveWriter = null;
195
+ let sessionState = "running";
196
+ let destroyed = false;
197
+ let helloReceived = false;
198
+ let outputReady = false;
199
+ let reconnecting = false;
200
+ let termReady = false;
201
+ let snapScrollbackOnNextWrite = false;
202
+ let followLiveOutput = false;
203
+ const pendingFrames = [];
204
+ const pendingInputFrames = [];
205
+ let reconnectHandle = null;
206
+ let scrollbackSnapHandle = null;
207
+ let scrollbackSnapPassesRemaining = 0;
208
+ let disposeViewportSizer = null;
209
+ let disposeScrollMetricsObserver = null;
210
+ let lastResizeSignature = null;
211
+ let lastScrollMetrics = null;
212
+ let suppressTermResizeCallback = false;
213
+ let statusSnapshot = {
214
+ connection: normalizeConnectionState(shell.dataset.connection),
215
+ loading: {
216
+ message: loading?.textContent ?? "Booting terminal...",
217
+ visible: loading ? !loading.hidden : false
218
+ },
219
+ message: status?.textContent ?? "Connecting to local session...",
220
+ output: normalizeOutputState(shell.dataset.output),
221
+ sessionState: normalizeSessionPresentationState(shell.dataset.sessionState)
222
+ };
223
+ const getStatusSnapshot = () => cloneTerminalStatusSnapshot(statusSnapshot);
224
+ const publishScrollMetrics = () => {
225
+ const onMetricsChange = dependencies.scroll?.onMetricsChange;
226
+ if (!onMetricsChange) return;
227
+ const nextMetrics = getScrollMetrics(getScrollSurface());
228
+ if (lastScrollMetrics && areScrollMetricsEqual(lastScrollMetrics, nextMetrics)) return;
229
+ lastScrollMetrics = nextMetrics;
230
+ onMetricsChange({ ...nextMetrics });
231
+ };
232
+ const presentStatus = (patch) => {
233
+ const nextStatus = {
234
+ ...statusSnapshot,
235
+ ...patch,
236
+ loading: {
237
+ ...statusSnapshot.loading,
238
+ ...patch.loading
239
+ }
240
+ };
241
+ const changed = !areTerminalStatusSnapshotsEqual(statusSnapshot, nextStatus);
242
+ statusSnapshot = nextStatus;
243
+ shell.dataset.connection = nextStatus.connection;
244
+ shell.dataset.output = nextStatus.output;
245
+ shell.dataset.sessionState = nextStatus.sessionState;
246
+ if (status) status.textContent = nextStatus.message;
247
+ if (loading) {
248
+ loading.textContent = nextStatus.loading.visible ? nextStatus.loading.message : "";
249
+ loading.hidden = !nextStatus.loading.visible;
250
+ }
251
+ if (changed) dependencies.onStatusChange?.(getStatusSnapshot());
252
+ };
253
+ dependencies.onStatusChange?.(getStatusSnapshot());
254
+ const syncTerminalPresentation = () => {
255
+ if (!transcriptArchive) return;
256
+ syncTerminalSurface(terminalRoot, transcriptArchive, term, ansiStyleTracker, { interactive: sessionState === "running" });
257
+ syncTrailingTerminalBlankRows(terminalRoot);
258
+ requestScrollUiUpdate(dependencies.scroll);
259
+ publishScrollMetrics();
260
+ };
261
+ const markLiveSession = () => {
262
+ reconnecting = false;
263
+ presentStatus({
264
+ connection: "open",
265
+ loading: {
266
+ message: "Booting terminal...",
267
+ visible: false
268
+ },
269
+ message: "Live shell on loopback",
270
+ output: "ready",
271
+ sessionState: "live"
272
+ });
273
+ };
274
+ const markWaitingForOutput = () => {
275
+ presentStatus({
276
+ connection: "open",
277
+ loading: {
278
+ message: "Booting terminal...",
279
+ visible: !outputReady
280
+ },
281
+ message: outputReady ? "Live shell on loopback" : "Connected. Waiting for shell output...",
282
+ sessionState: outputReady ? "live" : "starting"
283
+ });
284
+ };
285
+ const markOutputReady = () => {
286
+ if (outputReady) return false;
287
+ outputReady = true;
288
+ markLiveSession();
289
+ return true;
290
+ };
291
+ const cancelScrollbackSnap = () => {
292
+ if (scrollbackSnapHandle === null) {
293
+ scrollbackSnapPassesRemaining = 0;
294
+ return;
295
+ }
296
+ cancelFrame(scrollbackSnapHandle);
297
+ scrollbackSnapHandle = null;
298
+ scrollbackSnapPassesRemaining = 0;
299
+ };
300
+ const cancelReconnect = () => {
301
+ if (reconnectHandle === null) return;
302
+ clearTimeout(reconnectHandle);
303
+ reconnectHandle = null;
304
+ };
305
+ const getTargetScrollTop = (scrollSurface, { preferTranscriptFollow = false } = {}) => {
306
+ return getScrollbackMax(scrollSurface);
307
+ };
308
+ const scheduleScrollbackSnapToBottom = (passes = 1, options = {}) => {
309
+ scrollbackSnapPassesRemaining = Math.max(scrollbackSnapPassesRemaining, passes);
310
+ if (scrollbackSnapHandle !== null) return;
311
+ const runSnap = () => {
312
+ scrollbackSnapHandle = requestFrame(() => {
313
+ scrollbackSnapHandle = null;
314
+ const scrollSurface = getScrollSurface();
315
+ scrollSurface.scrollTop = getTargetScrollTop(scrollSurface, options);
316
+ requestScrollUiUpdate(dependencies.scroll);
317
+ publishScrollMetrics();
318
+ scrollbackSnapPassesRemaining = Math.max(0, scrollbackSnapPassesRemaining - 1);
319
+ if (scrollbackSnapPassesRemaining > 0) runSnap();
320
+ });
321
+ };
322
+ runSnap();
323
+ };
324
+ const syncLiveOutputFollowScroll = () => {
325
+ if (!followLiveOutput) return;
326
+ scheduleScrollbackSnapToBottom(2, { preferTranscriptFollow: true });
327
+ };
328
+ const syncPreservedArchiveVisibility = ({ allowReveal = false, scrollTop = getScrollSurface().scrollTop, maxScrollTop = getScrollbackMax(getScrollSurface()) } = {}) => {
329
+ if (!transcriptArchive) return;
330
+ if (isAtScrollbackBottom(scrollTop, maxScrollTop)) {
331
+ transcriptArchive.concealPreservedArchive();
332
+ return;
333
+ }
334
+ if (allowReveal) transcriptArchive.revealPreservedArchive();
335
+ };
336
+ const disposeHotkeys = installScrollbackInteractions(terminalRoot, {
337
+ getScrollSurface,
338
+ setScrollTop(scrollSurface, scrollTop) {
339
+ scrollSurface.scrollTop = scrollTop;
340
+ requestScrollUiUpdate(dependencies.scroll);
341
+ publishScrollMetrics();
342
+ },
343
+ onInput({ event }) {
344
+ const preserveVisiblePrompt = shouldPreserveVisiblePromptOnInput(event) && isLiveInputSurfaceVisible(shell, terminalRoot, windowObject);
345
+ const shouldFollowLiveOutput = shouldFollowLiveOutputOnInput(event);
346
+ snapScrollbackOnNextWrite = !preserveVisiblePrompt;
347
+ followLiveOutput = shouldFollowLiveOutput;
348
+ cancelScrollbackSnap();
349
+ if (!preserveVisiblePrompt) {
350
+ const scrollSurface = getScrollSurface();
351
+ scrollSurface.scrollTop = getTargetScrollTop(scrollSurface);
352
+ requestScrollUiUpdate(dependencies.scroll);
353
+ publishScrollMetrics();
354
+ }
355
+ syncPreservedArchiveVisibility();
356
+ },
357
+ onManualScroll({ scrollTop, maxScrollTop }) {
358
+ syncPreservedArchiveVisibility({
359
+ allowReveal: true,
360
+ scrollTop,
361
+ maxScrollTop
362
+ });
363
+ snapScrollbackOnNextWrite = false;
364
+ followLiveOutput = false;
365
+ cancelScrollbackSnap();
366
+ requestScrollUiUpdate(dependencies.scroll);
367
+ publishScrollMetrics();
368
+ }
369
+ });
370
+ const resetTerminalBuffer = () => {
371
+ if (!term) return;
372
+ writer?.discardPending();
373
+ archiveWriter?.discardPending();
374
+ transcriptArchive?.preserveSnapshot([]);
375
+ ansiStyleTracker.reset(term.cols, term.rows);
376
+ if (term.bridge && typeof term.bridge.init === "function") {
377
+ term.bridge.init(term.cols, term.rows);
378
+ term.renderer?.setup?.(term.cols, term.rows);
379
+ term.renderer?.render?.(term.bridge);
380
+ ansiStyleTracker.syncFromBridge(term.bridge);
381
+ syncTerminalPresentation();
382
+ return;
383
+ }
384
+ term.write?.(encoder.encode("\x1Bc"));
385
+ syncTerminalPresentation();
386
+ };
387
+ const flushPendingInput = () => {
388
+ if (!helloReceived || sessionState !== "running" || pendingInputFrames.length === 0) return;
389
+ for (const chunk of pendingInputFrames.splice(0)) transport?.send(chunk);
390
+ };
391
+ const processVisiblePayload = (data) => {
392
+ const firstOutput = markOutputReady();
393
+ const themeUpdate = themeProtocolParser.append(data);
394
+ let themeChanged = false;
395
+ if (themeUpdate) themeChanged = themeController.syncProtocolUpdate(themeUpdate);
396
+ const themeQueryResponse = resolveTerminalThemeQueryResponse(data, {
397
+ root: terminalRoot,
398
+ theme: themeController.getTheme()
399
+ });
400
+ if (themeQueryResponse) sendInputToPty(themeQueryResponse);
401
+ if (!termReady || !writer || !archiveWriter) {
402
+ pendingFrames.push(data);
403
+ if (themeChanged) renderTerminalNow(term, { force: true });
404
+ return;
405
+ }
406
+ writer.enqueue(data);
407
+ archiveWriter.enqueue(data);
408
+ if (themeChanged) renderTerminalNow(term, { force: true });
409
+ syncTerminalPresentation();
410
+ if (followLiveOutput) {
411
+ snapScrollbackOnNextWrite = false;
412
+ syncLiveOutputFollowScroll();
413
+ } else if (snapScrollbackOnNextWrite || firstOutput) {
414
+ snapScrollbackOnNextWrite = false;
415
+ scheduleScrollbackSnapToBottom(2);
416
+ }
417
+ };
418
+ const sendInputToPty = (data) => {
419
+ if (sessionState !== "running") return;
420
+ if (!helloReceived) {
421
+ pendingInputFrames.push(data);
422
+ return;
423
+ }
424
+ transport?.send(data);
425
+ };
426
+ const sendUserInput = (data) => {
427
+ sendInputToPty(data);
428
+ };
429
+ const emitResize = (cols, rows) => {
430
+ const normalizedCols = normalizeResizeDimension(cols, term?.cols ?? 80);
431
+ const normalizedRows = normalizeResizeDimension(rows, term?.rows ?? 24);
432
+ ansiStyleTracker.resize(normalizedCols, normalizedRows);
433
+ syncDimensions(terminalRoot, normalizedCols, normalizedRows);
434
+ transcriptArchive?.refresh();
435
+ syncTerminalPresentation();
436
+ const signature = `${normalizedCols}x${normalizedRows}`;
437
+ if (lastResizeSignature === signature) return;
438
+ lastResizeSignature = signature;
439
+ if (sessionState !== "running") return;
440
+ transport?.sendControl?.(createResizeControlFrame(normalizedCols, normalizedRows));
441
+ };
442
+ const syncTermDimensionsFromBridge = (requestedCols = term?.cols ?? 80, requestedRows = term?.rows ?? 24) => {
443
+ if (!term) return {
444
+ cols: normalizeResizeDimension(requestedCols, 80),
445
+ rows: normalizeResizeDimension(requestedRows, 24)
446
+ };
447
+ const dimensions = resolveTerminalBridgeDimensions(term, requestedCols, requestedRows);
448
+ term.cols = dimensions.cols;
449
+ term.rows = dimensions.rows;
450
+ 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
+ return dimensions;
452
+ };
453
+ const resizeTerminalTo = (cols, rows) => {
454
+ if (!term) return;
455
+ const requestedCols = normalizeResizeDimension(cols, term.cols);
456
+ const requestedRows = normalizeResizeDimension(rows, term.rows);
457
+ if (term.cols !== requestedCols || term.rows !== requestedRows) {
458
+ suppressTermResizeCallback = true;
459
+ try {
460
+ term.resize(requestedCols, requestedRows);
461
+ } finally {
462
+ suppressTermResizeCallback = false;
463
+ }
464
+ }
465
+ const actual = syncTermDimensionsFromBridge(requestedCols, requestedRows);
466
+ applyTerminalElementHeight(terminalRoot, actual.rows);
467
+ emitResize(actual.cols, actual.rows);
468
+ };
469
+ const handleTermResize = (cols, rows) => {
470
+ if (suppressTermResizeCallback) return;
471
+ const actual = syncTermDimensionsFromBridge(cols, rows);
472
+ emitResize(actual.cols, actual.rows);
473
+ };
474
+ const showReconnectState = (statusText) => {
475
+ reconnecting = true;
476
+ helloReceived = false;
477
+ presentStatus({
478
+ connection: "reconnecting",
479
+ loading: {
480
+ message: "Reconnecting...",
481
+ visible: true
482
+ },
483
+ message: statusText,
484
+ sessionState: outputReady ? "reconnecting" : "starting"
485
+ });
486
+ };
487
+ const scheduleReconnect = () => {
488
+ if (destroyed || reconnectHandle !== null || sessionState !== "running") return;
489
+ showReconnectState("Connection lost. Reconnecting...");
490
+ reconnectHandle = setTimeout(() => {
491
+ reconnectHandle = null;
492
+ if (destroyed || sessionState !== "running") return;
493
+ transport?.connect();
494
+ }, reconnectDelayMs);
495
+ };
496
+ const handleBrowserOffline = () => {
497
+ if (destroyed || sessionState !== "running" || reconnecting) return;
498
+ showReconnectState("Connection interrupted. Reconnecting...");
499
+ syncTerminalPresentation();
500
+ transport?.disconnect?.("offline");
501
+ };
502
+ const handleBrowserOnline = () => {
503
+ if (destroyed || sessionState !== "running" || !reconnecting) return;
504
+ cancelReconnect();
505
+ transport?.connect();
506
+ };
507
+ term = (dependencies.createTerm ?? ((element, options) => new WTerm(element, options)))(terminalRoot, {
508
+ autoResize: false,
509
+ cursorBlink: true,
510
+ onData(data) {
511
+ sendUserInput(data);
512
+ },
513
+ onResize(cols, rows) {
514
+ handleTermResize(cols, rows);
515
+ },
516
+ onTitle(title) {
517
+ if (title) doc.title = `${title} - aitty`;
518
+ }
519
+ });
520
+ terminalRoot._aittyTerm = term;
521
+ transcriptArchive = createTranscriptArchive(terminalRoot, frameScheduler, {
522
+ getCols() {
523
+ return term?.cols ?? 80;
524
+ },
525
+ getVisibleRows() {
526
+ return term?.rows ?? 24;
527
+ },
528
+ scrollbackLimit: dependencies.transcriptArchiveOptions?.scrollbackLimit
529
+ });
530
+ archiveWriter = createBufferedTerminalWriter({ write(data) {
531
+ transcriptArchive?.append(data);
532
+ } }, frameScheduler);
533
+ writer = createBufferedTerminalWriter({ write(data) {
534
+ ansiStyleTracker.append(data);
535
+ term?.write?.(data);
536
+ ansiStyleTracker.syncFromBridge(term?.bridge);
537
+ const renderHints = ansiStyleTracker.consumeRenderHints();
538
+ renderTerminalNow(term, { force: renderHints.clearScreen || renderHints.styleChanged });
539
+ syncTerminalPresentation();
540
+ } }, frameScheduler);
541
+ transport = (dependencies.createTransport ?? ((handlers) => createBrowserTransport(handlers)))({
542
+ onBinary(data) {
543
+ if (!helloReceived) helloReceived = true;
544
+ processVisiblePayload(data);
545
+ if (reconnecting && outputReady) markLiveSession();
546
+ flushPendingInput();
547
+ },
548
+ onClose(event) {
549
+ if (destroyed) return;
550
+ if (sessionState === "running" && event?.code === SUPERSEDED_CLOSE_CODE) {
551
+ cancelReconnect();
552
+ reconnecting = false;
553
+ sessionState = "superseded";
554
+ presentStatus({
555
+ connection: "superseded",
556
+ loading: {
557
+ message: "Booting terminal...",
558
+ visible: false
559
+ },
560
+ message: "Session was taken over in another tab",
561
+ sessionState: "superseded"
562
+ });
563
+ syncTerminalPresentation();
564
+ return;
565
+ }
566
+ if (sessionState === "running") {
567
+ scheduleReconnect();
568
+ syncTerminalPresentation();
569
+ return;
570
+ }
571
+ presentStatus({ connection: sessionState === "error" ? "error" : "closed" });
572
+ syncTerminalPresentation();
573
+ },
574
+ onControlMessage(data) {
575
+ const message = parseAittyControlFrame(data);
576
+ if (!message) return;
577
+ if (message.type === "hello") {
578
+ helloReceived = true;
579
+ const replay = decodeHelloReplay(message.replay);
580
+ if (reconnecting && replay.length > 0) resetTerminalBuffer();
581
+ if (replay.length > 0) processVisiblePayload(replay);
582
+ cancelReconnect();
583
+ if (reconnecting) if (outputReady) markLiveSession();
584
+ else {
585
+ reconnecting = false;
586
+ markWaitingForOutput();
587
+ }
588
+ flushPendingInput();
589
+ return;
590
+ }
591
+ if (message.type === "ping") {
592
+ transport?.sendControl?.(createPongControlFrame());
593
+ return;
594
+ }
595
+ if (message.type === "theme") {
596
+ let themeChanged = false;
597
+ if (message.theme === null) {
598
+ themeChanged = themeController.setTheme(void 0);
599
+ if (themeChanged) renderTerminalNow(term, { force: true });
600
+ return;
601
+ }
602
+ const theme = normalizeThemeName(message.theme);
603
+ if (theme) themeChanged = themeController.setTheme(theme);
604
+ if (themeChanged) renderTerminalNow(term, { force: true });
605
+ return;
606
+ }
607
+ if (message.type !== "exit") return;
608
+ cancelReconnect();
609
+ const exitPresentation = resolveExitPresentation(message);
610
+ sessionState = exitPresentation.sessionState;
611
+ reconnecting = false;
612
+ presentStatus({
613
+ connection: "closed",
614
+ loading: {
615
+ message: "Booting terminal...",
616
+ visible: false
617
+ },
618
+ message: exitPresentation.statusText,
619
+ sessionState: exitPresentation.sessionState
620
+ });
621
+ syncTerminalPresentation();
622
+ },
623
+ onError(event) {
624
+ if (destroyed || sessionState !== "running") return;
625
+ showReconnectState("Connection interrupted. Reconnecting...");
626
+ syncTerminalPresentation();
627
+ if (event instanceof ErrorEvent && event.message) event.message;
628
+ },
629
+ onOpen() {
630
+ cancelReconnect();
631
+ if (reconnecting) {
632
+ presentStatus({
633
+ connection: "open",
634
+ loading: {
635
+ message: "Reconnecting...",
636
+ visible: true
637
+ },
638
+ message: outputReady ? "Reconnected. Syncing transcript..." : "Connected. Waiting for shell output...",
639
+ sessionState: outputReady ? "reconnecting" : "starting"
640
+ });
641
+ return;
642
+ }
643
+ if (!outputReady) {
644
+ presentStatus({
645
+ connection: "open",
646
+ message: "Connected. Waiting for shell output..."
647
+ });
648
+ return;
649
+ }
650
+ presentStatus({ connection: "open" });
651
+ }
652
+ });
653
+ await term.init();
654
+ replaceTerminalInputHandler(term);
655
+ const disposeInputPolicies = installTerminalInputPolicies(term, {
656
+ captureDocumentCtrlN: false,
657
+ onData(data) {
658
+ sendUserInput(data);
659
+ },
660
+ resolveKey: dependencies.input?.resolveKey
661
+ });
662
+ installBrowserRenderer(term, ansiStyleTracker);
663
+ termReady = true;
664
+ const initialSize = syncTermDimensionsFromBridge(term.cols, term.rows);
665
+ ansiStyleTracker.reset(initialSize.cols, initialSize.rows);
666
+ disposeViewportSizer = installTerminalViewportSizer(term, terminalRoot, windowObject, resizeTerminalTo);
667
+ syncTermDimensionsFromBridge(term.cols, term.rows);
668
+ emitResize(term.cols, term.rows);
669
+ syncTerminalPresentation();
670
+ term.focus();
671
+ disposeScrollMetricsObserver = installScrollMetricsObserver(getScrollSurface(), publishScrollMetrics);
672
+ publishScrollMetrics();
673
+ windowObject.addEventListener("offline", handleBrowserOffline);
674
+ windowObject.addEventListener("online", handleBrowserOnline);
675
+ transport.connect(toWebSocketUrl(sessionUrl));
676
+ for (const frame of pendingFrames.splice(0)) writer.enqueue(frame);
677
+ return {
678
+ destroy() {
679
+ if (destroyed) return;
680
+ destroyed = true;
681
+ delete terminalRoot._aittyTerm;
682
+ cancelScrollbackSnap();
683
+ cancelReconnect();
684
+ disposeViewportSizer?.();
685
+ disposeViewportSizer = null;
686
+ disposeScrollMetricsObserver?.();
687
+ disposeScrollMetricsObserver = null;
688
+ archiveWriter?.destroy();
689
+ transcriptArchive?.destroy();
690
+ writer?.destroy();
691
+ disposeHotkeys();
692
+ disposeInputPolicies();
693
+ themeController.destroy();
694
+ windowObject.removeEventListener("offline", handleBrowserOffline);
695
+ windowObject.removeEventListener("online", handleBrowserOnline);
696
+ transport?.close();
697
+ term?.destroy();
698
+ },
699
+ getStatus: getStatusSnapshot,
700
+ getTheme: themeController.getTheme,
701
+ setTheme: themeController.setTheme,
702
+ term,
703
+ transport
704
+ };
705
+ }
706
+ function mountAitty(containerOrOptions = {}, maybeOptions = {}) {
707
+ if (typeof containerOrOptions === "string" || isElementLike(containerOrOptions)) return mountAittyInContainer(containerOrOptions, maybeOptions);
708
+ return mountAittyWithElements(containerOrOptions);
709
+ }
710
+ function mountAittyWithElements(options = {}) {
711
+ const { document: doc = document, loading, shell, status, terminalRoot, ...dependencies } = options;
712
+ const elements = { ...dependencies.elements };
713
+ if ("loading" in options) elements.loading = loading;
714
+ if (shell) elements.shell = shell;
715
+ if ("status" in options) elements.status = status;
716
+ if (terminalRoot) elements.terminalRoot = terminalRoot;
717
+ return mountTerminalApp(doc, {
718
+ ...dependencies,
719
+ elements
720
+ });
721
+ }
722
+ const AITTY_TERMINAL_TAG_NAME = "aitty-terminal";
723
+ function defineAittyTerminalElement(registry = globalThis.customElements) {
724
+ if (!registry || registry.get(AITTY_TERMINAL_TAG_NAME) || typeof HTMLElement === "undefined") return;
725
+ registry.define(AITTY_TERMINAL_TAG_NAME, createAittyTerminalElementConstructor());
726
+ }
727
+ function createAittyTerminalElementConstructor() {
728
+ return class AittyTerminalCustomElement extends HTMLElement {
729
+ static get observedAttributes() {
730
+ return ["src", "theme"];
731
+ }
732
+ #mounted = null;
733
+ #mountVersion = 0;
734
+ connectedCallback() {
735
+ this.#mount();
736
+ }
737
+ disconnectedCallback() {
738
+ this.#destroyMounted();
739
+ }
740
+ attributeChangedCallback(name, oldValue, nextValue) {
741
+ if (oldValue === nextValue || !this.isConnected) return;
742
+ if (name === "theme" && this.#mounted) {
743
+ this.#mounted.setTheme(normalizeThemeName(nextValue ?? void 0));
744
+ return;
745
+ }
746
+ this.#mount();
747
+ }
748
+ #destroyMounted() {
749
+ this.#mountVersion += 1;
750
+ this.#mounted?.destroy();
751
+ this.#mounted = null;
752
+ }
753
+ async #mount() {
754
+ this.#destroyMounted();
755
+ const mountVersion = this.#mountVersion;
756
+ try {
757
+ const mounted = await mountAitty(this, {
758
+ document: this.ownerDocument,
759
+ onStatusChange: (status) => {
760
+ this.dispatchEvent(new CustomEvent("aitty:status", {
761
+ bubbles: true,
762
+ composed: true,
763
+ detail: status
764
+ }));
765
+ },
766
+ src: this.getAttribute("src") ?? void 0,
767
+ theme: normalizeThemeName(this.getAttribute("theme") ?? void 0)
768
+ });
769
+ if (!this.isConnected || this.#mountVersion !== mountVersion) {
770
+ mounted.destroy();
771
+ return;
772
+ }
773
+ this.#mounted = mounted;
774
+ this.dispatchEvent(new CustomEvent("aitty:mount", {
775
+ bubbles: true,
776
+ composed: true,
777
+ detail: mounted
778
+ }));
779
+ } catch (error) {
780
+ this.dispatchEvent(new CustomEvent("aitty:error", {
781
+ bubbles: true,
782
+ composed: true,
783
+ detail: error
784
+ }));
785
+ }
786
+ }
787
+ };
788
+ }
789
+ async function mountAittyInContainer(container, options) {
790
+ const { scrollAdapter, ...dependencies } = options;
791
+ const doc = dependencies.document ?? resolveAittyDocument(container);
792
+ const mount = resolveAittyMountContainer(doc, container);
793
+ const view = createAittyEmbedView(doc, mount);
794
+ const scrollOptions = {
795
+ ...dependencies.scroll,
796
+ viewport: view.viewport
797
+ };
798
+ const adapter = scrollAdapter?.({
799
+ content: view.content,
800
+ scrollOptions,
801
+ target: view.scrollTarget,
802
+ viewport: view.viewport
803
+ });
804
+ Object.assign(scrollOptions, adapter?.scrollOptions);
805
+ const mounted = await mountTerminalApp(doc, {
806
+ ...dependencies,
807
+ scroll: scrollOptions,
808
+ elements: {
809
+ shell: view.shell,
810
+ terminalRoot: view.terminalRoot
811
+ }
812
+ });
813
+ let destroyed = false;
814
+ return {
815
+ ...mounted,
816
+ destroy() {
817
+ if (destroyed) return;
818
+ destroyed = true;
819
+ mounted.destroy();
820
+ adapter?.destroy?.();
821
+ view.shell.remove();
822
+ mount.classList.remove("aitty-embed");
823
+ }
824
+ };
825
+ }
826
+ function resolveAittyDocument(container) {
827
+ if (isElementLike(container)) return container.ownerDocument;
828
+ if (typeof document !== "undefined") return document;
829
+ throw new Error("mountAitty requires a browser document when mounting by selector");
830
+ }
831
+ function resolveAittyMountContainer(doc, container) {
832
+ if (isDocumentElement(doc, container)) return container;
833
+ if (isElementLike(container)) throw new Error("aitty mount container belongs to a different document");
834
+ const element = doc.querySelector(container);
835
+ if (!(element instanceof HTMLElement)) throw new Error(`Missing aitty mount container: ${container}`);
836
+ return element;
837
+ }
838
+ function isElementLike(value) {
839
+ return typeof value === "object" && value !== null && "nodeType" in value && "ownerDocument" in value;
840
+ }
841
+ function isDocumentElement(doc, value) {
842
+ const HTMLElementConstructor = doc.defaultView?.HTMLElement;
843
+ return Boolean(HTMLElementConstructor && value instanceof HTMLElementConstructor);
844
+ }
845
+ function createAittyEmbedView(doc, mount) {
846
+ mount.replaceChildren();
847
+ mount.classList.add("aitty-embed");
848
+ const shell = doc.createElement("div");
849
+ shell.className = "aitty-shell";
850
+ shell.dataset.shell = "";
851
+ shell.dataset.runtime = "session";
852
+ shell.dataset.connection = "connecting";
853
+ shell.dataset.output = "pending";
854
+ shell.dataset.sessionState = "starting";
855
+ const scrollTarget = doc.createElement("div");
856
+ scrollTarget.className = "aitty-scroll-target";
857
+ scrollTarget.dataset.aittyScrollTarget = "";
858
+ const viewport = doc.createElement("div");
859
+ viewport.className = "aitty-scroll-viewport";
860
+ viewport.dataset.aittyScrollViewport = "";
861
+ viewport.tabIndex = -1;
862
+ const content = doc.createElement("div");
863
+ content.className = "aitty-scroll-content";
864
+ content.dataset.aittyScrollContent = "";
865
+ const terminalRoot = doc.createElement("div");
866
+ terminalRoot.className = "aitty-terminal-root terminal-root";
867
+ terminalRoot.dataset.terminalRoot = "";
868
+ terminalRoot.role = "application";
869
+ terminalRoot.setAttribute("aria-label", "Terminal");
870
+ content.append(terminalRoot);
871
+ viewport.append(content);
872
+ scrollTarget.append(viewport);
873
+ shell.append(scrollTarget);
874
+ mount.append(shell);
875
+ return {
876
+ content,
877
+ scrollTarget,
878
+ shell,
879
+ terminalRoot,
880
+ viewport
881
+ };
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
+ configureTerminalTextarea(textarea);
889
+ term.element.appendChild(textarea);
890
+ syncTerminalTextareaPosition(term, textarea);
891
+ const syncInputPosition = () => {
892
+ syncTerminalTextareaPosition(term, textarea);
893
+ };
894
+ const onFocus = () => {
895
+ syncInputPosition();
896
+ term.element.classList.add("focused");
897
+ };
898
+ const onBlur = () => {
899
+ term.element.classList.remove("focused");
900
+ };
901
+ textarea.addEventListener("focus", onFocus);
902
+ textarea.addEventListener("blur", onBlur);
903
+ textarea.addEventListener("beforeinput", syncInputPosition, true);
904
+ textarea.addEventListener("compositionstart", syncInputPosition, true);
905
+ textarea.addEventListener("compositionupdate", syncInputPosition, true);
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
+ },
915
+ destroy() {
916
+ textarea.removeEventListener("focus", onFocus);
917
+ textarea.removeEventListener("blur", onBlur);
918
+ textarea.removeEventListener("beforeinput", syncInputPosition, true);
919
+ textarea.removeEventListener("compositionstart", syncInputPosition, true);
920
+ textarea.removeEventListener("compositionupdate", syncInputPosition, true);
921
+ textarea.removeEventListener("compositionend", syncInputPosition, true);
922
+ textarea.removeEventListener("input", syncInputPosition, true);
923
+ textarea.removeEventListener("keydown", syncInputPosition, true);
924
+ term.element.classList.remove("focused");
925
+ textarea.remove();
926
+ }
927
+ };
928
+ }
929
+ function configureTerminalTextarea(textarea) {
930
+ textarea.setAttribute("autocapitalize", "off");
931
+ textarea.setAttribute("autocomplete", "off");
932
+ textarea.setAttribute("autocorrect", "off");
933
+ textarea.setAttribute("spellcheck", "false");
934
+ textarea.setAttribute("enterkeyhint", "send");
935
+ textarea.setAttribute("tabindex", "0");
936
+ textarea.setAttribute("aria-hidden", "true");
937
+ const style = textarea.style;
938
+ style.position = "fixed";
939
+ style.left = "0";
940
+ style.top = "0";
941
+ style.width = "1px";
942
+ style.height = "1px";
943
+ style.opacity = "0";
944
+ style.overflow = "hidden";
945
+ style.border = "0";
946
+ style.padding = "0";
947
+ style.margin = "0";
948
+ style.outline = "none";
949
+ style.resize = "none";
950
+ style.pointerEvents = "none";
951
+ style.caretColor = "transparent";
952
+ style.color = "transparent";
953
+ style.background = "transparent";
954
+ }
955
+ function syncTerminalTextareaPosition(term, textarea) {
956
+ const viewport = resolveTextareaViewport(textarea);
957
+ const anchorRect = (resolveLiveInputSurfaceAnchor(term.element) ?? term.element).getBoundingClientRect?.();
958
+ const fallbackRect = term.element.getBoundingClientRect?.();
959
+ const rect = hasUsableClientRect(anchorRect) ? anchorRect : fallbackRect;
960
+ if (!hasUsableClientRect(rect)) {
961
+ textarea.style.left = "0";
962
+ textarea.style.top = "0";
963
+ return;
964
+ }
965
+ const left = clamp(rect.left, 0, Math.max(0, viewport.width - 1));
966
+ const top = clamp(rect.top, 0, Math.max(0, viewport.height - 1));
967
+ textarea.style.left = `${left}px`;
968
+ textarea.style.top = `${top}px`;
969
+ }
970
+ function resolveTextareaViewport(textarea) {
971
+ const view = textarea.ownerDocument.defaultView;
972
+ const root = textarea.ownerDocument.documentElement;
973
+ const body = textarea.ownerDocument.body;
974
+ const width = firstFinitePositiveNumber(view?.innerWidth, root?.clientWidth, body?.clientWidth) ?? 1;
975
+ return {
976
+ height: firstFinitePositiveNumber(view?.innerHeight, root?.clientHeight, body?.clientHeight) ?? 1,
977
+ width
978
+ };
979
+ }
980
+ function installBrowserRenderer(term, styleTracker) {
981
+ const container = term?._container;
982
+ if (!(container instanceof HTMLElement)) return;
983
+ const renderer = new BrowserTerminalRenderer(container, styleTracker);
984
+ term.renderer = renderer;
985
+ renderer.setup(term.cols, term.rows);
986
+ if (term.bridge) renderer.render(term.bridge);
987
+ }
988
+ function renderTerminalNow(term, options = {}) {
989
+ const bridge = term?.bridge;
990
+ const renderer = term?.renderer;
991
+ if (!bridge || !renderer || typeof renderer.render !== "function") return;
992
+ renderer.render(bridge, options);
993
+ }
994
+ function installTerminalViewportSizer(term, terminalRoot, windowObject, onResize) {
995
+ const target = resolveViewportSize(term, terminalRoot, windowObject);
996
+ applyTerminalElementHeight(terminalRoot, target.rows);
997
+ onResize(target.cols, target.rows);
998
+ let frameHandle = null;
999
+ const scheduleResize = () => {
1000
+ if (frameHandle !== null) return;
1001
+ frameHandle = windowObject.requestAnimationFrame(() => {
1002
+ frameHandle = null;
1003
+ const nextTarget = resolveViewportSize(term, terminalRoot, windowObject);
1004
+ applyTerminalElementHeight(terminalRoot, nextTarget.rows);
1005
+ onResize(nextTarget.cols, nextTarget.rows);
1006
+ });
1007
+ };
1008
+ windowObject.addEventListener("resize", scheduleResize);
1009
+ return () => {
1010
+ if (frameHandle !== null) {
1011
+ windowObject.cancelAnimationFrame(frameHandle);
1012
+ frameHandle = null;
1013
+ }
1014
+ windowObject.removeEventListener("resize", scheduleResize);
1015
+ };
1016
+ }
1017
+ function resolveViewportSize(term, terminalRoot, windowObject) {
1018
+ const metrics = measureTerminalCell(term, terminalRoot);
1019
+ const viewport = resolveAvailableTerminalViewport(terminalRoot, windowObject);
1020
+ if (!viewport) return {
1021
+ cols: normalizeResizeDimension(term?.cols, 80),
1022
+ rows: normalizeResizeDimension(term?.rows, 24)
1023
+ };
1024
+ return {
1025
+ cols: Math.max(1, Math.floor(viewport.width / metrics.charWidth)),
1026
+ rows: Math.max(1, Math.floor(viewport.height / metrics.rowHeight))
1027
+ };
1028
+ }
1029
+ function measureTerminalCell(term, terminalRoot) {
1030
+ const measured = typeof term?._measureCharSize === "function" ? term._measureCharSize() : null;
1031
+ if (measured && Number.isFinite(measured.charWidth) && measured.charWidth > 0 && Number.isFinite(measured.rowHeight) && measured.rowHeight > 0) {
1032
+ terminalRoot.style.setProperty("--term-row-height", `${measured.rowHeight}px`);
1033
+ return measured;
1034
+ }
1035
+ const computedStyle = getComputedStyle(terminalRoot);
1036
+ const rowHeight = parseCssPixelValue(computedStyle.getPropertyValue("--term-row-height")) || parseCssPixelValue(computedStyle.lineHeight) || 17;
1037
+ const fontSize = parseCssPixelValue(computedStyle.fontSize) || 15;
1038
+ return {
1039
+ charWidth: Math.max(1, fontSize * .6),
1040
+ rowHeight: Math.max(1, rowHeight)
1041
+ };
1042
+ }
1043
+ function resolveAvailableTerminalViewport(terminalRoot, windowObject) {
1044
+ const rect = terminalRoot.getBoundingClientRect?.();
1045
+ const width = Number.isFinite(rect?.width) && rect.width > 0 ? rect.width : 0;
1046
+ if (width <= 0 || !Number.isFinite(rect?.top)) return null;
1047
+ const viewportHeight = Math.max(1, windowObject.innerHeight || terminalRoot.ownerDocument?.documentElement?.clientHeight || 24);
1048
+ const top = Math.max(0, rect.top);
1049
+ const paddingBottom = parseCssPixelValue(getComputedStyle(terminalRoot).paddingBottom);
1050
+ const height = viewportHeight - top - paddingBottom;
1051
+ if (!Number.isFinite(height) || height <= 0) return null;
1052
+ return {
1053
+ height,
1054
+ width
1055
+ };
1056
+ }
1057
+ function applyTerminalElementHeight(terminalRoot, rows) {
1058
+ terminalRoot.style.height = `calc(var(--term-row-height, 17px) * ${Math.max(1, rows)})`;
1059
+ }
1060
+ function parseCssPixelValue(value) {
1061
+ const parsed = Number.parseFloat(value);
1062
+ return Number.isFinite(parsed) ? parsed : 0;
1063
+ }
1064
+ function normalizeResizeDimension(value, fallback) {
1065
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : Math.max(1, fallback);
1066
+ }
1067
+ function resolveTerminalBridgeDimensions(term, requestedCols, requestedRows) {
1068
+ const bridge = term?.bridge ?? null;
1069
+ if (!bridge) return {
1070
+ cols: normalizeResizeDimension(requestedCols, 80),
1071
+ rows: normalizeResizeDimension(requestedRows, 24)
1072
+ };
1073
+ const bridgeCols = typeof bridge.getCols === "function" ? bridge.getCols() : requestedCols;
1074
+ const bridgeRows = typeof bridge.getRows === "function" ? bridge.getRows() : requestedRows;
1075
+ return {
1076
+ cols: normalizeResizeDimension(bridgeCols, requestedCols),
1077
+ rows: normalizeResizeDimension(bridgeRows, requestedRows)
1078
+ };
1079
+ }
1080
+ function syncTerminalSurface(element, transcriptArchive, term, styleTracker, options = {}) {
1081
+ const interactive = options.interactive ?? true;
1082
+ syncTerminalInteractivity(element, term, interactive);
1083
+ const bridge = term?.bridge ?? null;
1084
+ const resolvedAltScreenState = (bridge ? bridge.usingAltScreen() : transcriptArchive.isAlternateScreenActive()) || styleTracker.isAltScreenActive();
1085
+ transcriptArchive.setAlternateScreenActive?.(resolvedAltScreenState);
1086
+ element.dataset.altScreen = String(resolvedAltScreenState);
1087
+ element.dataset.renderMode = resolvedAltScreenState ? "screen" : "transcript";
1088
+ transcriptArchive.setScreenModeActive?.(resolvedAltScreenState);
1089
+ syncScreenModeScrollbackRows(element, resolvedAltScreenState);
1090
+ if (!bridge) {
1091
+ delete element.dataset.cursorVisible;
1092
+ delete element.dataset.cursorRow;
1093
+ delete element.dataset.cursorCol;
1094
+ return;
1095
+ }
1096
+ const cursor = bridge.getCursor();
1097
+ if (!interactive) {
1098
+ element.dataset.cursorVisible = "false";
1099
+ element.dataset.cursorRow = String(cursor.row + 1);
1100
+ element.dataset.cursorCol = String(cursor.col + 1);
1101
+ return;
1102
+ }
1103
+ element.dataset.cursorVisible = String(cursor.visible);
1104
+ element.dataset.cursorRow = String(cursor.row + 1);
1105
+ element.dataset.cursorCol = String(cursor.col + 1);
1106
+ }
1107
+ function syncTerminalInteractivity(element, term, interactive) {
1108
+ element.dataset.sessionInteractive = String(interactive);
1109
+ element.setAttribute("aria-disabled", String(!interactive));
1110
+ if (interactive) element.removeAttribute("tabindex");
1111
+ else element.setAttribute("tabindex", "-1");
1112
+ const activeElement = element.ownerDocument?.activeElement;
1113
+ if (!interactive && activeElement instanceof HTMLElement && element.contains(activeElement)) activeElement.blur?.();
1114
+ const textarea = term?.input?.textarea;
1115
+ if (textarea instanceof HTMLTextAreaElement) {
1116
+ textarea.disabled = !interactive;
1117
+ textarea.readOnly = !interactive;
1118
+ textarea.tabIndex = interactive ? 0 : -1;
1119
+ textarea.setAttribute("aria-hidden", String(!interactive));
1120
+ if (!interactive) textarea.blur();
1121
+ }
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
+ }
1133
+ function containsScreenRedrawControl(text) {
1134
+ const escape = String.fromCharCode(27);
1135
+ return new RegExp(`(?:${escape}\\[[?0-9;:]*[ABCDEFGHJKSTf]|${escape}[78c])`).test(text);
1136
+ }
1137
+ function getMonotonicTime() {
1138
+ return globalThis.performance?.now?.() ?? Date.now();
1139
+ }
1140
+ function requestBrowserFrame(callback, view) {
1141
+ const requestFrame = view?.requestAnimationFrame ?? globalThis.requestAnimationFrame;
1142
+ if (typeof requestFrame === "function") return requestFrame.call(view ?? globalThis, callback);
1143
+ return setTimeout(() => {
1144
+ callback(getMonotonicTime());
1145
+ }, 0);
1146
+ }
1147
+ function cancelBrowserFrame(handle, view) {
1148
+ const cancelFrame = view?.cancelAnimationFrame ?? globalThis.cancelAnimationFrame;
1149
+ if (typeof cancelFrame === "function") {
1150
+ cancelFrame.call(view ?? globalThis, handle);
1151
+ return;
1152
+ }
1153
+ clearTimeout(handle);
1154
+ }
1155
+ function createTranscriptArchive(element, scheduler = {}, options = {}) {
1156
+ const doc = element.ownerDocument ?? document;
1157
+ const view = doc.defaultView;
1158
+ const requestFrame = scheduler.requestFrame ?? ((callback) => requestBrowserFrame(callback, view));
1159
+ const cancelFrame = scheduler.cancelFrame ?? ((handle) => cancelBrowserFrame(handle, view));
1160
+ const getCols = options.getCols ?? (() => 80);
1161
+ const scrollbackLimit = Math.max(0, options.scrollbackLimit ?? DEFAULT_TRANSCRIPT_SCROLLBACK_LIMIT);
1162
+ const getVisibleRows = options.getVisibleRows ?? (() => 24);
1163
+ let decoder = new TextDecoder();
1164
+ const archive = doc.createElement("pre");
1165
+ const archiveText = doc.createTextNode("");
1166
+ archive.setAttribute("data-terminal-transcript-archive", "");
1167
+ archive.className = "term-transcript-archive";
1168
+ archive.append(archiveText);
1169
+ element.insertBefore(archive, element.firstChild);
1170
+ let preservedLines = [];
1171
+ const finalizedLines = [];
1172
+ let currentLine = "";
1173
+ let parserState = "text";
1174
+ let csiParams = "";
1175
+ let csiPrivate = "";
1176
+ let carriageReturnPending = false;
1177
+ let frameHandle = null;
1178
+ let archivedLineCount = 0;
1179
+ let alternateScreenActive = false;
1180
+ let screenModeActive = false;
1181
+ let concealedPreservedArchive = false;
1182
+ let screenRedrawArchiveSuppressedUntil = 0;
1183
+ const setPreservedLines = (lines) => {
1184
+ const normalizedLines = normalizePreservedTranscriptLines(lines);
1185
+ preservedLines = scrollbackLimit > 0 ? normalizedLines.slice(-scrollbackLimit) : [];
1186
+ };
1187
+ const syncArchiveVisibility = () => {
1188
+ const hasArchivedContent = preservedLines.length > 0 || archiveText.data.length > 0;
1189
+ const archiveHidden = alternateScreenActive || screenModeActive;
1190
+ const archiveConcealed = !archiveHidden && concealedPreservedArchive && hasArchivedContent;
1191
+ archive.hidden = archiveHidden;
1192
+ archive.style.visibility = archiveConcealed ? "hidden" : "";
1193
+ archive.setAttribute("aria-hidden", String(archiveHidden || archiveConcealed));
1194
+ element.dataset.archiveConcealed = String(archiveConcealed);
1195
+ };
1196
+ const scheduleSync = () => {
1197
+ if (frameHandle !== null) return;
1198
+ frameHandle = requestFrame(() => {
1199
+ frameHandle = null;
1200
+ syncArchive();
1201
+ });
1202
+ };
1203
+ const reconcilePreservedLines = () => {
1204
+ if (preservedLines.length === 0) return;
1205
+ const nextPreservedLines = trimReconciledPreservedTranscriptLines(preservedLines, [...finalizedLines, currentLine]);
1206
+ if (nextPreservedLines.length === preservedLines.length && nextPreservedLines.every((line, index) => line === preservedLines[index])) return;
1207
+ preservedLines = nextPreservedLines;
1208
+ if (preservedLines.length === 0) concealedPreservedArchive = false;
1209
+ };
1210
+ const syncArchive = () => {
1211
+ reconcilePreservedLines();
1212
+ const liveLineCount = finalizedLines.length + 1;
1213
+ const visibleCapacity = Math.max(1, scrollbackLimit + Math.max(1, getVisibleRows()));
1214
+ const hiddenLineCount = Math.max(0, liveLineCount - visibleCapacity);
1215
+ const archivedStart = Math.max(0, hiddenLineCount - Math.min(hiddenLineCount, scrollbackLimit));
1216
+ const liveArchiveWindow = finalizedLines.slice(archivedStart, hiddenLineCount);
1217
+ const archiveWindow = preservedLines.length > 0 ? [...preservedLines, ...liveArchiveWindow] : liveArchiveWindow;
1218
+ archiveText.data = archiveWindow.length > 0 ? `${archiveWindow.join("\n")}\n` : "";
1219
+ archivedLineCount = archiveWindow.length;
1220
+ syncTranscriptMetadata(element, preservedLines.length + liveLineCount, archivedLineCount);
1221
+ syncArchiveVisibility();
1222
+ };
1223
+ const resetLiveTranscript = () => {
1224
+ finalizedLines.length = 0;
1225
+ currentLine = "";
1226
+ parserState = "text";
1227
+ csiParams = "";
1228
+ csiPrivate = "";
1229
+ carriageReturnPending = false;
1230
+ };
1231
+ const resetParser = () => {
1232
+ resetLiveTranscript();
1233
+ decoder = new TextDecoder();
1234
+ };
1235
+ const setAlternateScreenActive = (nextState) => {
1236
+ if (alternateScreenActive === nextState) return;
1237
+ alternateScreenActive = nextState;
1238
+ syncArchiveVisibility();
1239
+ };
1240
+ const setScreenModeActive = (nextState) => {
1241
+ if (screenModeActive === nextState) return;
1242
+ screenModeActive = nextState;
1243
+ syncArchiveVisibility();
1244
+ };
1245
+ const appendPrintableCharacter = (character) => {
1246
+ const cols = Math.max(1, getCols());
1247
+ if (currentLine.length >= cols) {
1248
+ finalizedLines.push(currentLine);
1249
+ currentLine = "";
1250
+ }
1251
+ currentLine += character;
1252
+ };
1253
+ const handleEraseDisplay = (params) => {
1254
+ if (params.includes(3)) {
1255
+ setPreservedLines([]);
1256
+ concealedPreservedArchive = false;
1257
+ resetParser();
1258
+ return;
1259
+ }
1260
+ if (params.includes(2)) {
1261
+ setPreservedLines([
1262
+ ...preservedLines,
1263
+ ...finalizedLines,
1264
+ currentLine
1265
+ ]);
1266
+ concealedPreservedArchive = preservedLines.length > 0;
1267
+ resetLiveTranscript();
1268
+ }
1269
+ };
1270
+ const processDecodedText = (text, { recordPrintable = true } = {}) => {
1271
+ for (const character of text) {
1272
+ if (parserState === "escape") {
1273
+ if (character === "[") {
1274
+ parserState = "csi";
1275
+ csiParams = "";
1276
+ csiPrivate = "";
1277
+ continue;
1278
+ }
1279
+ if (character === "]") {
1280
+ parserState = "osc";
1281
+ continue;
1282
+ }
1283
+ if (character === "c") resetParser();
1284
+ parserState = "text";
1285
+ continue;
1286
+ }
1287
+ if (parserState === "csi") {
1288
+ if ((character === "?" || character === ">" || character === "!") && csiParams.length === 0) {
1289
+ csiPrivate = character;
1290
+ continue;
1291
+ }
1292
+ if (character >= "0" && character <= "9" || character === ";" || character === ":") {
1293
+ csiParams += character;
1294
+ continue;
1295
+ }
1296
+ if (character >= "@" && character <= "~") {
1297
+ const params = parseCsiParams(csiParams);
1298
+ setAlternateScreenActive(nextAltScreenState(alternateScreenActive, csiPrivate, csiParams, character));
1299
+ if (character === "J") handleEraseDisplay(params);
1300
+ parserState = "text";
1301
+ }
1302
+ continue;
1303
+ }
1304
+ if (parserState === "osc") {
1305
+ if (character === "\x07") {
1306
+ parserState = "text";
1307
+ continue;
1308
+ }
1309
+ if (character === "\x1B") parserState = "osc_escape";
1310
+ continue;
1311
+ }
1312
+ if (parserState === "osc_escape") {
1313
+ parserState = character === "\\" ? "text" : "osc";
1314
+ continue;
1315
+ }
1316
+ if (character === "\x1B") {
1317
+ parserState = "escape";
1318
+ continue;
1319
+ }
1320
+ if (alternateScreenActive) continue;
1321
+ if (!recordPrintable) continue;
1322
+ if (carriageReturnPending) {
1323
+ if (character === "\n") {
1324
+ finalizedLines.push(currentLine);
1325
+ currentLine = "";
1326
+ carriageReturnPending = false;
1327
+ continue;
1328
+ }
1329
+ currentLine = "";
1330
+ carriageReturnPending = false;
1331
+ }
1332
+ if (character === "\r") {
1333
+ carriageReturnPending = true;
1334
+ continue;
1335
+ }
1336
+ if (character === "\n") {
1337
+ finalizedLines.push(currentLine);
1338
+ currentLine = "";
1339
+ continue;
1340
+ }
1341
+ if (character === "\b") {
1342
+ currentLine = currentLine.slice(0, -1);
1343
+ continue;
1344
+ }
1345
+ if (character === " ") {
1346
+ currentLine += character;
1347
+ continue;
1348
+ }
1349
+ if (character >= "\0" && character < " " || character === "") continue;
1350
+ appendPrintableCharacter(character);
1351
+ }
1352
+ };
1353
+ syncArchive();
1354
+ return {
1355
+ append(chunk) {
1356
+ if (screenModeActive) return;
1357
+ const text = decoder.decode(chunk, { stream: true });
1358
+ const now = getMonotonicTime();
1359
+ const hasScreenRedrawControl = containsScreenRedrawControl(text);
1360
+ if (hasScreenRedrawControl) screenRedrawArchiveSuppressedUntil = now + SCREEN_REDRAW_ARCHIVE_SUPPRESS_MS;
1361
+ processDecodedText(text, { recordPrintable: !hasScreenRedrawControl && now >= screenRedrawArchiveSuppressedUntil });
1362
+ scheduleSync();
1363
+ },
1364
+ concealPreservedArchive() {
1365
+ const nextState = preservedLines.length > 0;
1366
+ if (concealedPreservedArchive === nextState) return;
1367
+ concealedPreservedArchive = nextState;
1368
+ syncArchiveVisibility();
1369
+ },
1370
+ destroy() {
1371
+ if (frameHandle !== null) {
1372
+ cancelFrame(frameHandle);
1373
+ frameHandle = null;
1374
+ }
1375
+ archive.remove();
1376
+ },
1377
+ getAllLines() {
1378
+ return normalizePreservedTranscriptLines([
1379
+ ...preservedLines,
1380
+ ...finalizedLines,
1381
+ currentLine
1382
+ ]);
1383
+ },
1384
+ isAlternateScreenActive() {
1385
+ return alternateScreenActive;
1386
+ },
1387
+ preserveSnapshot(lines = []) {
1388
+ setPreservedLines(lines);
1389
+ concealedPreservedArchive = preservedLines.length > 0;
1390
+ resetParser();
1391
+ scheduleSync();
1392
+ },
1393
+ refresh() {
1394
+ scheduleSync();
1395
+ },
1396
+ revealPreservedArchive() {
1397
+ if (!concealedPreservedArchive) return;
1398
+ concealedPreservedArchive = false;
1399
+ syncArchiveVisibility();
1400
+ },
1401
+ setAlternateScreenActive,
1402
+ setScreenModeActive
1403
+ };
1404
+ }
1405
+ function createBrowserTransport(handlers) {
1406
+ const pendingMessages = [];
1407
+ const encoder = new TextEncoder();
1408
+ let socketUrl;
1409
+ let socket = null;
1410
+ let closed = false;
1411
+ const flushPendingMessages = () => {
1412
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
1413
+ for (const message of pendingMessages.splice(0)) sendMessage(message);
1414
+ };
1415
+ const sendMessage = (message) => {
1416
+ if (closed) return;
1417
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
1418
+ pendingMessages.push(message);
1419
+ return;
1420
+ }
1421
+ if (message.kind === "control") {
1422
+ socket.send(String(message.data));
1423
+ return;
1424
+ }
1425
+ const payload = typeof message.data === "string" ? encoder.encode(message.data) : message.data;
1426
+ socket.send(copyToArrayBuffer(payload));
1427
+ };
1428
+ return {
1429
+ close() {
1430
+ closed = true;
1431
+ pendingMessages.length = 0;
1432
+ socket?.close();
1433
+ },
1434
+ connect(url) {
1435
+ if (closed) return;
1436
+ if (url) socketUrl = url;
1437
+ if (!socketUrl) throw new Error("No WebSocket URL provided");
1438
+ if (socket && socket.readyState !== WebSocket.CLOSED) return;
1439
+ const currentSocket = new WebSocket(socketUrl);
1440
+ socket = currentSocket;
1441
+ currentSocket.binaryType = "arraybuffer";
1442
+ currentSocket.onopen = () => {
1443
+ flushPendingMessages();
1444
+ handlers.onOpen();
1445
+ };
1446
+ currentSocket.onmessage = (event) => {
1447
+ if (event.data instanceof ArrayBuffer) {
1448
+ handlers.onBinary(new Uint8Array(event.data));
1449
+ return;
1450
+ }
1451
+ if (typeof event.data === "string") {
1452
+ handlers.onControlMessage(event.data);
1453
+ return;
1454
+ }
1455
+ if (event.data instanceof Blob) {
1456
+ event.data.arrayBuffer().then((buffer) => {
1457
+ handlers.onBinary(new Uint8Array(buffer));
1458
+ }).catch(() => {
1459
+ handlers.onError(new Event("error"));
1460
+ });
1461
+ return;
1462
+ }
1463
+ handlers.onControlMessage(String(event.data));
1464
+ };
1465
+ currentSocket.onclose = (event) => {
1466
+ if (socket === currentSocket) socket = null;
1467
+ handlers.onClose({
1468
+ code: event.code,
1469
+ reason: event.reason,
1470
+ wasClean: event.wasClean
1471
+ });
1472
+ };
1473
+ currentSocket.onerror = (event) => {
1474
+ handlers.onError(event);
1475
+ if (!closed && currentSocket.readyState < WebSocket.CLOSING) currentSocket.close();
1476
+ };
1477
+ },
1478
+ disconnect(reason = "offline") {
1479
+ if (!socket || socket.readyState >= WebSocket.CLOSING) return;
1480
+ socket.close(1e3, reason);
1481
+ },
1482
+ send(data) {
1483
+ sendMessage({
1484
+ kind: "binary",
1485
+ data
1486
+ });
1487
+ },
1488
+ sendControl(data) {
1489
+ sendMessage({
1490
+ kind: "control",
1491
+ data
1492
+ });
1493
+ }
1494
+ };
1495
+ }
1496
+ function installScrollbackInteractions(element, callbacks = {}) {
1497
+ const getScrollSurface = callbacks.getScrollSurface ?? (() => element);
1498
+ const onInput = callbacks.onInput ?? (() => {});
1499
+ const onManualScroll = callbacks.onManualScroll ?? (() => {});
1500
+ const setScrollTop = callbacks.setScrollTop ?? ((scrollSurface, scrollTop) => {
1501
+ scrollSurface.scrollTop = scrollTop;
1502
+ });
1503
+ const onWheel = (event) => {
1504
+ const scrollSurface = getScrollSurface();
1505
+ const maxScrollTop = getScrollbackMax(scrollSurface);
1506
+ if (maxScrollTop <= 0 || event.deltaY === 0) return;
1507
+ const nextScrollTop = clamp(scrollSurface.scrollTop + event.deltaY, 0, maxScrollTop);
1508
+ if (nextScrollTop === scrollSurface.scrollTop) return;
1509
+ onManualScroll({
1510
+ scrollTop: nextScrollTop,
1511
+ maxScrollTop
1512
+ });
1513
+ event.preventDefault();
1514
+ event.stopPropagation();
1515
+ setScrollTop(scrollSurface, nextScrollTop);
1516
+ };
1517
+ const onKeyDown = (event) => {
1518
+ if (isScrollbackShortcut(event)) {
1519
+ event.preventDefault();
1520
+ event.stopPropagation();
1521
+ const scrollSurface = getScrollSurface();
1522
+ const maxScrollTop = getScrollbackMax(scrollSurface);
1523
+ const nextScrollTop = event.key === "PageUp" ? scrollSurface.scrollTop - Math.max(scrollSurface.clientHeight, 1) : maxScrollTop;
1524
+ onManualScroll({
1525
+ scrollTop: clamp(nextScrollTop, 0, maxScrollTop),
1526
+ maxScrollTop
1527
+ });
1528
+ setScrollTop(scrollSurface, clamp(nextScrollTop, 0, getScrollbackMax(scrollSurface)));
1529
+ return;
1530
+ }
1531
+ if (shouldSnapScrollbackOnKeyDown(event)) onInput({ event });
1532
+ };
1533
+ element.addEventListener("wheel", onWheel, {
1534
+ capture: true,
1535
+ passive: false
1536
+ });
1537
+ element.addEventListener("keydown", onKeyDown, true);
1538
+ return () => {
1539
+ element.removeEventListener("wheel", onWheel, true);
1540
+ element.removeEventListener("keydown", onKeyDown, true);
1541
+ };
1542
+ }
1543
+ function hasVisibleTerminalPaint(element) {
1544
+ const style = getComputedStyle(element);
1545
+ return hasVisibleCssPaint(style.backgroundColor) || hasVisibleCssBoxShadow(style.boxShadow);
1546
+ }
1547
+ function hasVisibleCssBoxShadow(value) {
1548
+ return typeof value === "string" && value.trim().length > 0 && value !== "none";
1549
+ }
1550
+ function hasVisibleCssPaint(value) {
1551
+ if (typeof value !== "string") return false;
1552
+ const normalizedValue = value.trim().replace(/\s+/g, "").toLowerCase();
1553
+ return normalizedValue.length > 0 && normalizedValue !== "transparent" && !/rgba\((?:[^,]+,){3}0(?:\.0+)?\)/.test(normalizedValue);
1554
+ }
1555
+ function getScrollbackMax(element) {
1556
+ return Math.max(0, element.scrollHeight - element.clientHeight);
1557
+ }
1558
+ function resolveScrollSurface(doc, fallbackElement, scrollOptions) {
1559
+ const configuredViewport = scrollOptions?.getViewport?.() ?? scrollOptions?.viewport;
1560
+ if (configuredViewport instanceof HTMLElement) return configuredViewport;
1561
+ const scrollingElement = doc.scrollingElement;
1562
+ if (scrollingElement instanceof HTMLElement) return scrollingElement;
1563
+ if (doc.documentElement instanceof HTMLElement) return doc.documentElement;
1564
+ if (doc.body instanceof HTMLElement) return doc.body;
1565
+ return fallbackElement;
1566
+ }
1567
+ function requestScrollUiUpdate(scrollOptions) {
1568
+ scrollOptions?.requestUiUpdate?.();
1569
+ }
1570
+ function getScrollMetrics(element) {
1571
+ const scrollTop = normalizeScrollMetric(element.scrollTop);
1572
+ const scrollHeight = normalizeScrollMetric(element.scrollHeight);
1573
+ const clientHeight = normalizeScrollMetric(element.clientHeight);
1574
+ const maxScrollTop = getScrollbackMax(element);
1575
+ const atTop = scrollTop <= 1;
1576
+ const atBottom = isAtScrollbackBottom(scrollTop, maxScrollTop);
1577
+ const thumbSizeRatio = scrollHeight <= 0 ? 1 : clamp(clientHeight / scrollHeight, 0, 1);
1578
+ return {
1579
+ atBottom,
1580
+ atTop,
1581
+ clientHeight,
1582
+ maxScrollTop,
1583
+ scrollHeight,
1584
+ scrollTop,
1585
+ thumbOffsetRatio: maxScrollTop <= 0 ? 0 : clamp(scrollTop / maxScrollTop, 0, 1),
1586
+ thumbSizeRatio
1587
+ };
1588
+ }
1589
+ function normalizeScrollMetric(value) {
1590
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
1591
+ }
1592
+ function areScrollMetricsEqual(left, right) {
1593
+ return left.atBottom === right.atBottom && left.atTop === right.atTop && left.clientHeight === right.clientHeight && left.maxScrollTop === right.maxScrollTop && left.scrollHeight === right.scrollHeight && left.scrollTop === right.scrollTop && left.thumbOffsetRatio === right.thumbOffsetRatio && left.thumbSizeRatio === right.thumbSizeRatio;
1594
+ }
1595
+ function installScrollMetricsObserver(element, onScroll) {
1596
+ element.addEventListener("scroll", onScroll, { passive: true });
1597
+ return () => {
1598
+ element.removeEventListener("scroll", onScroll);
1599
+ };
1600
+ }
1601
+ function isAtScrollbackBottom(scrollTop, maxScrollTop) {
1602
+ return maxScrollTop <= 0 || scrollTop >= maxScrollTop - 1;
1603
+ }
1604
+ function clamp(value, min, max) {
1605
+ return Math.min(max, Math.max(min, value));
1606
+ }
1607
+ function isScrollbackShortcut(event) {
1608
+ return event.shiftKey && (event.key === "PageUp" || event.key === "PageDown");
1609
+ }
1610
+ function shouldPreserveVisiblePromptOnInput(event) {
1611
+ if (!(event instanceof KeyboardEvent)) return false;
1612
+ if (event.altKey || event.ctrlKey || event.metaKey) return false;
1613
+ return event.key.length === 1 || event.key === "Backspace" || event.key === "Delete" || isCompositionKeyDown(event);
1614
+ }
1615
+ function shouldFollowLiveOutputOnInput(event) {
1616
+ if (!(event instanceof KeyboardEvent)) return false;
1617
+ return !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && event.key === "Enter";
1618
+ }
1619
+ function isLiveInputSurfaceVisible(shell, terminalRoot, windowObject) {
1620
+ const anchor = resolveLiveInputSurfaceAnchor(terminalRoot);
1621
+ if (!(anchor instanceof HTMLElement)) return false;
1622
+ const rect = anchor.getBoundingClientRect();
1623
+ if (!hasUsableClientRect(rect)) return false;
1624
+ const topBoundary = getViewportTopBoundary(shell);
1625
+ const bottomBoundary = getViewportBottomBoundary(windowObject);
1626
+ return rect.bottom > topBoundary + 1 && rect.top < bottomBoundary - 1;
1627
+ }
1628
+ function resolveLiveInputSurfaceAnchor(terminalRoot) {
1629
+ const visibleCursors = Array.from(terminalRoot.querySelectorAll(".term-grid .term-cursor")).filter((cursor) => cursor instanceof HTMLElement && !cursor.hidden && cursor.getAttribute("aria-hidden") !== "true");
1630
+ if (visibleCursors.length > 0) return visibleCursors[visibleCursors.length - 1];
1631
+ const rows = Array.from(terminalRoot.querySelectorAll(".term-grid .term-row"));
1632
+ let lastNonEmptyRow = null;
1633
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
1634
+ const row = rows[index];
1635
+ if (!(row instanceof HTMLElement)) continue;
1636
+ const text = row.textContent?.trimEnd() ?? "";
1637
+ if (text.length === 0) continue;
1638
+ if (looksLikePromptLine(text)) return row;
1639
+ lastNonEmptyRow = lastNonEmptyRow ?? row;
1640
+ }
1641
+ return lastNonEmptyRow;
1642
+ }
1643
+ function getViewportTopBoundary(shell) {
1644
+ const topBar = shell.querySelector("[data-terminal-viewport-offset]");
1645
+ if (topBar instanceof HTMLElement) {
1646
+ const rect = topBar.getBoundingClientRect();
1647
+ if (hasUsableClientRect(rect)) return rect.bottom;
1648
+ }
1649
+ return 0;
1650
+ }
1651
+ function getViewportBottomBoundary(windowObject) {
1652
+ const candidates = [
1653
+ windowObject?.innerHeight,
1654
+ windowObject?.document?.documentElement?.clientHeight,
1655
+ windowObject?.document?.body?.clientHeight
1656
+ ];
1657
+ for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) return candidate;
1658
+ return Number.POSITIVE_INFINITY;
1659
+ }
1660
+ function firstFinitePositiveNumber(...candidates) {
1661
+ for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) return candidate;
1662
+ return null;
1663
+ }
1664
+ function hasUsableClientRect(rect) {
1665
+ if (!rect) return false;
1666
+ 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);
1667
+ }
1668
+ function shouldSnapScrollbackOnKeyDown(event) {
1669
+ if (event.defaultPrevented || isScrollbackShortcut(event) || isBrowserSafeShortcut(event) || isContainedTerminalNavigationKey(event)) return false;
1670
+ return ![
1671
+ "Alt",
1672
+ "CapsLock",
1673
+ "Control",
1674
+ "Fn",
1675
+ "Meta",
1676
+ "NumLock",
1677
+ "ScrollLock",
1678
+ "Shift"
1679
+ ].includes(event.key);
1680
+ }
1681
+ function decodeHelloReplay(value) {
1682
+ if (typeof value !== "string" || value.length === 0) return new Uint8Array(0);
1683
+ try {
1684
+ const normalizedValue = value.replace(/-/g, "+").replace(/_/g, "/");
1685
+ const missingPadding = normalizedValue.length % 4;
1686
+ const decoded = atob(missingPadding === 0 ? normalizedValue : `${normalizedValue}${"=".repeat(4 - missingPadding)}`);
1687
+ const bytes = new Uint8Array(decoded.length);
1688
+ for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
1689
+ return bytes;
1690
+ } catch {
1691
+ return new Uint8Array(0);
1692
+ }
1693
+ }
1694
+ function formatExitStatus(message) {
1695
+ if (typeof message.signal === "string" && message.signal) return `Session ended (${message.signal})`;
1696
+ if (typeof message.code === "number") return `Session ended (code ${message.code})`;
1697
+ return "Session ended";
1698
+ }
1699
+ function resolveExitPresentation(message) {
1700
+ if (isShutdownSignal(message.signal)) return {
1701
+ sessionState: "shutdown",
1702
+ statusText: `Session shut down (${message.signal})`
1703
+ };
1704
+ return {
1705
+ sessionState: "ended",
1706
+ statusText: formatExitStatus(message)
1707
+ };
1708
+ }
1709
+ function isShutdownSignal(signal) {
1710
+ return signal === "SIGINT" || signal === "SIGTERM";
1711
+ }
1712
+ function getRequiredElement(doc, selector) {
1713
+ const element = doc.querySelector(selector);
1714
+ if (!(element instanceof HTMLElement)) throw new Error(`Missing required element: ${selector}`);
1715
+ return element;
1716
+ }
1717
+ function getOptionalElement(doc, selector) {
1718
+ const element = doc.querySelector(selector);
1719
+ return element instanceof HTMLElement ? element : null;
1720
+ }
1721
+ function hasElementOverride(elements, key) {
1722
+ return Object.prototype.hasOwnProperty.call(elements, key);
1723
+ }
1724
+ function createTerminalThemeController(doc, shell, terminalRoot, theme) {
1725
+ const explicitTheme = normalizeThemeName(theme);
1726
+ let observingTheme = false;
1727
+ const clearProtocolOverrides = () => {
1728
+ clearTerminalThemeProtocolOverrides(doc.documentElement);
1729
+ clearTerminalThemeProtocolOverrides(shell);
1730
+ clearTerminalThemeProtocolOverrides(terminalRoot);
1731
+ };
1732
+ const MutationObserverCtor = doc.defaultView?.MutationObserver ?? globalThis.MutationObserver;
1733
+ const mutationObserver = typeof MutationObserverCtor === "function" ? new MutationObserverCtor(() => {
1734
+ const externalTheme = normalizeThemeName(doc.documentElement.dataset.theme);
1735
+ if (shell.dataset.theme === externalTheme && terminalRoot.dataset.theme === externalTheme) return;
1736
+ clearProtocolOverrides();
1737
+ syncInternalTheme(externalTheme);
1738
+ }) : null;
1739
+ const observeTheme = () => {
1740
+ if (observingTheme) return;
1741
+ mutationObserver?.observe(doc.documentElement, {
1742
+ attributeFilter: ["data-theme"],
1743
+ attributes: true
1744
+ });
1745
+ observingTheme = true;
1746
+ };
1747
+ const pauseThemeObservation = () => {
1748
+ if (!observingTheme) return;
1749
+ mutationObserver?.disconnect();
1750
+ observingTheme = false;
1751
+ };
1752
+ const syncTheme = (nextTheme) => {
1753
+ const configuredTheme = normalizeThemeName(nextTheme);
1754
+ if (!configuredTheme) {
1755
+ if (doc.documentElement.dataset.theme !== void 0) delete doc.documentElement.dataset.theme;
1756
+ doc.documentElement.style.removeProperty("color-scheme");
1757
+ delete shell.dataset.theme;
1758
+ delete terminalRoot.dataset.theme;
1759
+ return;
1760
+ }
1761
+ if (configuredTheme === "light" || configuredTheme === "dark") doc.documentElement.style.setProperty("color-scheme", configuredTheme);
1762
+ else doc.documentElement.style.removeProperty("color-scheme");
1763
+ if (doc.documentElement.dataset.theme !== configuredTheme) doc.documentElement.dataset.theme = configuredTheme;
1764
+ if (shell.dataset.theme !== configuredTheme) shell.dataset.theme = configuredTheme;
1765
+ if (terminalRoot.dataset.theme !== configuredTheme) terminalRoot.dataset.theme = configuredTheme;
1766
+ };
1767
+ const syncInternalTheme = (nextTheme) => {
1768
+ pauseThemeObservation();
1769
+ try {
1770
+ syncTheme(nextTheme);
1771
+ mutationObserver?.takeRecords();
1772
+ } finally {
1773
+ observeTheme();
1774
+ }
1775
+ };
1776
+ syncTheme(explicitTheme ?? doc.documentElement.dataset.theme);
1777
+ observeTheme();
1778
+ return {
1779
+ destroy() {
1780
+ mutationObserver?.disconnect();
1781
+ },
1782
+ getTheme() {
1783
+ return doc.documentElement.dataset.theme;
1784
+ },
1785
+ syncProtocolUpdate(update) {
1786
+ let themeChanged = false;
1787
+ if (update.theme && normalizeThemeName(update.theme) !== this.getTheme()) {
1788
+ clearProtocolOverrides();
1789
+ syncInternalTheme(update.theme);
1790
+ themeChanged = true;
1791
+ }
1792
+ applyTerminalThemeProtocolUpdate(doc.documentElement, update);
1793
+ applyTerminalThemeProtocolUpdate(shell, update);
1794
+ applyTerminalThemeProtocolUpdate(terminalRoot, update);
1795
+ return themeChanged || hasTerminalThemeProtocolUpdate(update);
1796
+ },
1797
+ setTheme(nextTheme) {
1798
+ const previousTheme = this.getTheme();
1799
+ const normalizedTheme = normalizeThemeName(nextTheme);
1800
+ clearProtocolOverrides();
1801
+ syncInternalTheme(nextTheme);
1802
+ return normalizedTheme !== previousTheme;
1803
+ }
1804
+ };
1805
+ }
1806
+ function clearTerminalThemeProtocolOverrides(element) {
1807
+ for (const variableName of TERMINAL_THEME_PROTOCOL_VARIABLES) element.style.removeProperty(variableName);
1808
+ }
1809
+ function applyTerminalThemeProtocolUpdate(element, update) {
1810
+ const { background, cursor, foreground } = update.colors;
1811
+ if (background) element.style.setProperty("--theme-term-bg", background);
1812
+ if (cursor) element.style.setProperty("--theme-term-cursor", cursor);
1813
+ if (foreground) element.style.setProperty("--theme-term-fg", foreground);
1814
+ for (const { color, index } of update.palette) element.style.setProperty(`--theme-term-color-${index}`, color);
1815
+ }
1816
+ function hasTerminalThemeProtocolUpdate(update) {
1817
+ return Object.keys(update.colors).length > 0 || update.palette.length > 0 || Boolean(update.theme);
1818
+ }
1819
+ function normalizeThemeName(theme) {
1820
+ return normalizeTheme(theme);
1821
+ }
1822
+ function normalizeConnectionState(value) {
1823
+ switch (value) {
1824
+ case "open":
1825
+ case "reconnecting":
1826
+ case "superseded":
1827
+ case "closed":
1828
+ case "error": return value;
1829
+ default: return "connecting";
1830
+ }
1831
+ }
1832
+ function normalizeOutputState(value) {
1833
+ return value === "ready" ? "ready" : "pending";
1834
+ }
1835
+ function normalizeSessionPresentationState(value) {
1836
+ switch (value) {
1837
+ case "live":
1838
+ case "reconnecting":
1839
+ case "superseded":
1840
+ case "shutdown":
1841
+ case "ended":
1842
+ case "error": return value;
1843
+ default: return "starting";
1844
+ }
1845
+ }
1846
+ function cloneTerminalStatusSnapshot(status) {
1847
+ return {
1848
+ ...status,
1849
+ loading: { ...status.loading }
1850
+ };
1851
+ }
1852
+ function areTerminalStatusSnapshotsEqual(left, right) {
1853
+ 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
+ }
1855
+ function toWebSocketUrl(sessionUrl) {
1856
+ const url = new URL(sessionUrl.href);
1857
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1858
+ url.pathname = "/ws";
1859
+ return url.toString();
1860
+ }
1861
+ function resolveAittySessionUrl(windowObject, src) {
1862
+ if (src instanceof URL) return new URL(src.href);
1863
+ if (typeof src === "string" && src.trim()) return new URL(src, windowObject.location.href);
1864
+ return new URL(windowObject.location.href);
1865
+ }
1866
+ function resolveShellRuntimeKind(doc, shell, sessionUrl) {
1867
+ const configuredRuntime = shell.dataset.runtime;
1868
+ if (configuredRuntime === "droid" || configuredRuntime === "pty") return configuredRuntime;
1869
+ if (configuredRuntime !== "session") return "pty";
1870
+ const windowObject = doc.defaultView ?? window;
1871
+ const fetchRuntimeKind = typeof windowObject.fetch === "function" ? windowObject.fetch.bind(windowObject) : null;
1872
+ if (!fetchRuntimeKind) return "pty";
1873
+ return fetchRuntimeKindFromSession(sessionUrl ?? resolveAittySessionUrl(windowObject), fetchRuntimeKind);
1874
+ }
1875
+ async function fetchRuntimeKindFromSession(sessionUrl, fetchRuntimeKind) {
1876
+ try {
1877
+ const url = new URL(sessionUrl.href);
1878
+ url.pathname = "/session-info";
1879
+ url.hash = "";
1880
+ const response = await fetchRuntimeKind(url.toString());
1881
+ if (!response.ok) return "pty";
1882
+ return (await response.json()).runtimeKind === "droid" ? "droid" : "pty";
1883
+ } catch {
1884
+ return "pty";
1885
+ }
1886
+ }
1887
+ function syncDimensions(element, cols, rows) {
1888
+ element.dataset.cols = String(cols);
1889
+ element.dataset.rows = String(rows);
1890
+ }
1891
+ function syncTranscriptMetadata(element, transcriptLineCount, archivedLineCount) {
1892
+ element.dataset.transcriptLines = String(transcriptLineCount);
1893
+ element.dataset.archivedLines = String(archivedLineCount);
1894
+ element.classList.toggle("has-scrollback", archivedLineCount > 0 || element.querySelector(".term-scrollback-row") !== null);
1895
+ }
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
+ function trimReconciledPreservedTranscriptLines(preservedLines, liveLines) {
1908
+ if (preservedLines.length === 0 || liveLines.length === 0) return preservedLines;
1909
+ const normalizedLiveLines = normalizePreservedTranscriptLines(liveLines);
1910
+ for (let start = 0; start < preservedLines.length; start += 1) {
1911
+ const candidate = preservedLines.slice(start);
1912
+ if (candidate.length <= normalizedLiveLines.length && candidate.every((line, index) => line === normalizedLiveLines[index])) return preservedLines.slice(0, start);
1913
+ }
1914
+ return preservedLines;
1915
+ }
1916
+ function normalizePreservedTranscriptLines(lines) {
1917
+ return lines.map((line) => line.trimEnd()).filter((line, index, array) => line.length > 0 || index > 0 && index < array.length - 1);
1918
+ }
1919
+ function looksLikePromptLine(text) {
1920
+ return /^\s*(?:[$>#]|[^\s]+[@:][^\s]+[$#])\s?/.test(text);
1921
+ }
1922
+ function copyToArrayBuffer(bytes) {
1923
+ const copy = new Uint8Array(bytes.byteLength);
1924
+ copy.set(bytes);
1925
+ return copy.buffer;
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;
1936
+ }
1937
+ if (typeof document !== "undefined") {
1938
+ defineAittyTerminalElement();
1939
+ if (shouldAutoMount(document)) mountTerminalApp().catch((error) => {
1940
+ const shell = document.querySelector("[data-shell]");
1941
+ const status = document.querySelector("[data-terminal-status]");
1942
+ const loading = document.querySelector("[data-terminal-loading]");
1943
+ if (shell instanceof HTMLElement) {
1944
+ shell.dataset.connection = "error";
1945
+ shell.dataset.sessionState = "error";
1946
+ }
1947
+ if (status instanceof HTMLElement) status.textContent = "Failed to start terminal";
1948
+ if (loading instanceof HTMLElement) loading.hidden = true;
1949
+ console.error(error);
1950
+ });
1951
+ }
1952
+ function shouldAutoMount(doc) {
1953
+ const shell = doc.querySelector("[data-shell]");
1954
+ return shell instanceof HTMLElement && !shell.hasAttribute("data-aitty-manual");
1955
+ }
1956
+ //#endregion
1957
+ export { createBufferedTerminalWriter, defineAittyTerminalElement, mountAitty, mountTerminalApp };