@floegence/floeterm-terminal-web 0.4.32 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +86 -56
  2. package/dist/core/TerminalCore.d.ts +27 -1
  3. package/dist/core/TerminalCore.d.ts.map +1 -1
  4. package/dist/core/TerminalCore.js +490 -41
  5. package/dist/core/TerminalCore.js.map +1 -1
  6. package/dist/core/TerminalRenderScheduler.d.ts +4 -0
  7. package/dist/core/TerminalRenderScheduler.d.ts.map +1 -1
  8. package/dist/core/TerminalRenderScheduler.js +35 -2
  9. package/dist/core/TerminalRenderScheduler.js.map +1 -1
  10. package/dist/fabric/BeamtermFabricRenderer.d.ts +59 -0
  11. package/dist/fabric/BeamtermFabricRenderer.d.ts.map +1 -0
  12. package/dist/fabric/BeamtermFabricRenderer.js +448 -0
  13. package/dist/fabric/BeamtermFabricRenderer.js.map +1 -0
  14. package/dist/fabric/TerminalFabricCoordinator.d.ts +40 -0
  15. package/dist/fabric/TerminalFabricCoordinator.d.ts.map +1 -0
  16. package/dist/fabric/TerminalFabricCoordinator.js +140 -0
  17. package/dist/fabric/TerminalFabricCoordinator.js.map +1 -0
  18. package/dist/fabric/TerminalLiveFabric.d.ts +84 -0
  19. package/dist/fabric/TerminalLiveFabric.d.ts.map +1 -0
  20. package/dist/fabric/TerminalLiveFabric.js +211 -0
  21. package/dist/fabric/TerminalLiveFabric.js.map +1 -0
  22. package/dist/fabric/types.d.ts +112 -0
  23. package/dist/fabric/types.d.ts.map +1 -0
  24. package/dist/fabric/types.js +2 -0
  25. package/dist/fabric/types.js.map +1 -0
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal/TerminalInitializationScheduler.d.ts +22 -0
  31. package/dist/internal/TerminalInitializationScheduler.d.ts.map +1 -0
  32. package/dist/internal/TerminalInitializationScheduler.js +79 -0
  33. package/dist/internal/TerminalInitializationScheduler.js.map +1 -0
  34. package/dist/internal/scheduleUiTurn.d.ts +5 -0
  35. package/dist/internal/scheduleUiTurn.d.ts.map +1 -0
  36. package/dist/internal/scheduleUiTurn.js +37 -0
  37. package/dist/internal/scheduleUiTurn.js.map +1 -0
  38. package/dist/manager/TerminalInstanceController.d.ts +71 -0
  39. package/dist/manager/TerminalInstanceController.d.ts.map +1 -0
  40. package/dist/manager/TerminalInstanceController.js +670 -0
  41. package/dist/manager/TerminalInstanceController.js.map +1 -0
  42. package/dist/types.d.ts +48 -6
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +3 -12
  46. package/dist/hooks/useTerminalInstance.d.ts +0 -3
  47. package/dist/hooks/useTerminalInstance.d.ts.map +0 -1
  48. package/dist/hooks/useTerminalInstance.js +0 -544
  49. package/dist/hooks/useTerminalInstance.js.map +0 -1
@@ -0,0 +1,670 @@
1
+ import { TerminalCore } from '../core/TerminalCore';
2
+ import { SequenceBuffer } from '../internal/SequenceBuffer';
3
+ import { createTerminalError } from '../utils/errors';
4
+ import { getDefaultTerminalConfig, getThemeColors } from '../utils/config';
5
+ import { createConsoleLogger, noopLogger } from '../utils/logger';
6
+ import { concatChunks } from '../utils/history';
7
+ import { terminalInitializationScheduler } from '../internal/TerminalInitializationScheduler';
8
+ import { TerminalState, computeConnectionState, computeTerminalState, } from '../types';
9
+ var ConnectionState;
10
+ (function (ConnectionState) {
11
+ ConnectionState["IDLE"] = "idle";
12
+ ConnectionState["CONNECTING"] = "connecting";
13
+ ConnectionState["CONNECTED"] = "connected";
14
+ ConnectionState["DISCONNECTED"] = "disconnected";
15
+ ConnectionState["RETRYING"] = "retrying";
16
+ ConnectionState["FAILED"] = "failed";
17
+ ConnectionState["ABORTED"] = "aborted";
18
+ })(ConnectionState || (ConnectionState = {}));
19
+ const MAX_WRITE_BATCH_CHUNKS = 64;
20
+ const MAX_WRITE_BATCH_BYTES = 256 * 1024;
21
+ const createDefaultScheduler = () => ({
22
+ requestFrame: callback => {
23
+ if (typeof requestAnimationFrame === 'function') {
24
+ return requestAnimationFrame(callback);
25
+ }
26
+ return setTimeout(() => callback(Date.now()), 0);
27
+ },
28
+ cancelFrame: id => {
29
+ if (typeof cancelAnimationFrame === 'function') {
30
+ cancelAnimationFrame(id);
31
+ return;
32
+ }
33
+ clearTimeout(id);
34
+ },
35
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
36
+ clearTimer: id => clearTimeout(id),
37
+ });
38
+ export class FrameworkNeutralTerminalInstanceController {
39
+ constructor(options) {
40
+ this.listeners = new Set();
41
+ this.container = null;
42
+ this.terminalCore = null;
43
+ this.isInitializing = false;
44
+ this.terminalDataUnsubscribe = null;
45
+ this.sequenceBuffer = new SequenceBuffer();
46
+ this.replayCompleteReceived = false;
47
+ this.isReplayActive = false;
48
+ this.lastAppliedSequence = 0;
49
+ this.loadingState = 'idle';
50
+ this.loadingMessage = '';
51
+ this.connectionState = ConnectionState.IDLE;
52
+ this.connectionError = null;
53
+ this.retryCount = 0;
54
+ this.retryTimeout = null;
55
+ this.initializeTimeout = null;
56
+ this.initializationAbortController = null;
57
+ this.queueRetryTimeout = null;
58
+ this.state = {
59
+ state: TerminalState.IDLE,
60
+ error: undefined,
61
+ dimensions: { cols: 80, rows: 24 }
62
+ };
63
+ this.dimensions = { cols: 80, rows: 24 };
64
+ this.dataQueue = [];
65
+ this.isProcessing = false;
66
+ this.queueGeneration = 0;
67
+ this.flushRaf = null;
68
+ this.disposed = false;
69
+ this.actions = {
70
+ write: data => {
71
+ const chunk = {
72
+ data: new TextEncoder().encode(data),
73
+ sequence: 0,
74
+ timestampMs: Date.now()
75
+ };
76
+ this.addChunkToQueue(chunk);
77
+ },
78
+ clear: () => {
79
+ this.terminalCore?.clear();
80
+ this.dataQueue = [];
81
+ const sessionId = this.options.sessionId;
82
+ if (!sessionId) {
83
+ return;
84
+ }
85
+ this.options.transport.clear(sessionId)
86
+ .catch(error => this.logger.warn('[TerminalInstanceController] Clear history failed', { error }))
87
+ .finally(() => {
88
+ this.options.transport.sendInput(sessionId, '\r')
89
+ .catch(error => this.logger.warn('[TerminalInstanceController] Clear redraw failed', { error }));
90
+ });
91
+ },
92
+ findNext: (term, options) => this.terminalCore?.findNext(term, options) ?? false,
93
+ findPrevious: (term, options) => this.terminalCore?.findPrevious(term, options) ?? false,
94
+ clearSearch: () => this.terminalCore?.clearSearch(),
95
+ serialize: () => this.terminalCore?.serialize() ?? '',
96
+ getSelectionText: () => this.terminalCore?.getSelectionText() ?? '',
97
+ hasSelection: () => this.terminalCore?.hasSelection() ?? false,
98
+ copySelection: source => this.terminalCore?.copySelection(source) ?? Promise.resolve({
99
+ copied: false,
100
+ reason: 'empty_selection',
101
+ source: source ?? 'command'
102
+ }),
103
+ setConnected: connected => this.terminalCore?.setConnected(connected),
104
+ forceResize: () => this.terminalCore?.forceResize(),
105
+ setSearchResultsCallback: callback => this.terminalCore?.setSearchResultsCallback(callback),
106
+ focus: () => this.terminalCore?.focus(),
107
+ getTerminalInfo: () => this.terminalCore?.getTerminalInfo() ?? null,
108
+ sendInput: data => this.handleUserInput(data),
109
+ setAppearance: appearance => this.applyCoreAppearance(appearance),
110
+ setTheme: theme => this.applyCoreAppearance({ themeName: theme }),
111
+ setFontSize: size => this.applyCoreAppearance({ fontSize: size }),
112
+ setPresentationScale: scale => this.applyCoreAppearance({ presentationScale: scale }),
113
+ reinitialize: () => this.reinitialize(),
114
+ };
115
+ this.handleStateChange = (newState) => {
116
+ this.updateState({ state: newState });
117
+ };
118
+ this.handleResize = async (size) => {
119
+ this.dimensions = size;
120
+ this.updateState({ dimensions: size });
121
+ this.options.onResize?.(size.cols, size.rows);
122
+ const sessionId = this.options.sessionId;
123
+ if (!sessionId) {
124
+ return;
125
+ }
126
+ try {
127
+ await this.options.transport.resize(sessionId, size.cols, size.rows);
128
+ }
129
+ catch (error) {
130
+ this.logger.warn('[TerminalInstanceController] Resize request failed', { error });
131
+ }
132
+ };
133
+ this.handleError = (error) => {
134
+ this.updateState({ error, state: TerminalState.ERROR });
135
+ this.options.onError?.(error);
136
+ };
137
+ this.options = { ...options };
138
+ this.logger = options.logger ?? createConsoleLogger();
139
+ this.scheduler = options.scheduler ?? createDefaultScheduler();
140
+ this.subscribeToTerminalData();
141
+ }
142
+ get connection() {
143
+ return computeConnectionState(this.createConnectionState());
144
+ }
145
+ async mount(container) {
146
+ if (this.disposed) {
147
+ throw new Error('Cannot mount a disposed TerminalInstanceController');
148
+ }
149
+ this.container = container;
150
+ this.scheduleInitialize();
151
+ }
152
+ unmount() {
153
+ this.clearInitializeTimeout();
154
+ this.initializationAbortController?.abort();
155
+ this.initializationAbortController = null;
156
+ this.cleanupTerminal();
157
+ this.container = null;
158
+ }
159
+ dispose() {
160
+ if (this.disposed) {
161
+ return;
162
+ }
163
+ this.disposed = true;
164
+ this.clearInitializeTimeout();
165
+ this.initializationAbortController?.abort();
166
+ this.initializationAbortController = null;
167
+ this.clearRetryTimeout();
168
+ this.clearQueueRetryTimeout();
169
+ this.cancelDataQueueFlush();
170
+ this.terminalDataUnsubscribe?.();
171
+ this.terminalDataUnsubscribe = null;
172
+ this.cleanupTerminal();
173
+ this.updateState({ state: TerminalState.DISPOSED });
174
+ this.listeners.clear();
175
+ }
176
+ updateOptions(options) {
177
+ if (this.disposed) {
178
+ return;
179
+ }
180
+ const previousSessionId = this.options.sessionId;
181
+ const previousIsActive = this.options.isActive;
182
+ const previousTheme = this.options.themeName;
183
+ const previousFontSize = this.options.fontSize;
184
+ const previousFontFamily = this.options.config?.fontFamily;
185
+ const previousPresentationScale = this.options.presentationScale;
186
+ this.options = {
187
+ ...this.options,
188
+ ...options,
189
+ config: options.config !== undefined ? options.config : this.options.config,
190
+ };
191
+ if (options.sessionId !== undefined && options.sessionId !== previousSessionId) {
192
+ this.resetSessionState();
193
+ this.subscribeToTerminalData();
194
+ if (this.terminalCore) {
195
+ void this.reinitialize();
196
+ }
197
+ return;
198
+ }
199
+ if (options.isActive !== undefined && options.isActive !== previousIsActive) {
200
+ if (this.options.isActive) {
201
+ this.scheduleInitialize();
202
+ if (this.terminalCore && this.options.sessionId) {
203
+ void this.connectToSession();
204
+ }
205
+ }
206
+ else {
207
+ this.terminalCore?.setConnected(false);
208
+ }
209
+ }
210
+ const nextFontFamily = this.options.config?.fontFamily;
211
+ if (previousTheme !== this.options.themeName
212
+ || previousFontSize !== this.options.fontSize
213
+ || previousFontFamily !== nextFontFamily
214
+ || previousPresentationScale !== this.options.presentationScale) {
215
+ this.applyCoreAppearance({
216
+ themeName: this.options.themeName,
217
+ ...(typeof this.options.fontSize === 'number' ? { fontSize: this.options.fontSize } : {}),
218
+ ...(typeof nextFontFamily === 'string' ? { fontFamily: nextFontFamily } : {}),
219
+ presentationScale: this.options.presentationScale ?? 1
220
+ });
221
+ }
222
+ }
223
+ getSnapshot() {
224
+ return {
225
+ state: computeTerminalState(this.state),
226
+ connection: computeConnectionState(this.createConnectionState()),
227
+ loadingState: this.loadingState,
228
+ loadingMessage: this.loadingMessage,
229
+ };
230
+ }
231
+ subscribe(listener) {
232
+ this.listeners.add(listener);
233
+ listener(this.getSnapshot());
234
+ return () => {
235
+ this.listeners.delete(listener);
236
+ };
237
+ }
238
+ getCore() {
239
+ return this.terminalCore;
240
+ }
241
+ emit() {
242
+ if (this.disposed) {
243
+ return;
244
+ }
245
+ const snapshot = this.getSnapshot();
246
+ for (const listener of this.listeners) {
247
+ listener(snapshot);
248
+ }
249
+ }
250
+ updateState(updates) {
251
+ this.state = { ...this.state, ...updates };
252
+ this.emit();
253
+ }
254
+ setLoading(state, message) {
255
+ this.loadingState = state;
256
+ this.loadingMessage = message;
257
+ this.emit();
258
+ }
259
+ setConnectionState(state) {
260
+ this.connectionState = state;
261
+ this.terminalCore?.setConnected(state === ConnectionState.CONNECTED);
262
+ this.emit();
263
+ }
264
+ setConnectionError(error) {
265
+ this.connectionError = error;
266
+ this.emit();
267
+ }
268
+ createConnectionState() {
269
+ return {
270
+ state: this.connectionState,
271
+ error: this.connectionError,
272
+ retryCount: this.retryCount,
273
+ connect: () => void this.connectToSession(),
274
+ disconnect: () => {
275
+ this.clearRetryTimeout();
276
+ this.setConnectionState(ConnectionState.ABORTED);
277
+ },
278
+ retry: () => {
279
+ this.clearRetryTimeout();
280
+ this.retryCount = 0;
281
+ this.setConnectionError(null);
282
+ this.setConnectionState(ConnectionState.IDLE);
283
+ void this.connectToSession();
284
+ },
285
+ clearError: () => this.setConnectionError(null),
286
+ };
287
+ }
288
+ resetSessionState() {
289
+ this.queueGeneration += 1;
290
+ this.sequenceBuffer.reset(1);
291
+ this.replayCompleteReceived = false;
292
+ this.isReplayActive = false;
293
+ this.lastAppliedSequence = 0;
294
+ this.dataQueue = [];
295
+ this.isProcessing = false;
296
+ this.cancelDataQueueFlush();
297
+ this.clearQueueRetryTimeout();
298
+ this.setConnectionState(ConnectionState.IDLE);
299
+ this.setConnectionError(null);
300
+ this.retryCount = 0;
301
+ this.clearRetryTimeout();
302
+ }
303
+ handleUserInput(data) {
304
+ const sessionId = this.options.sessionId;
305
+ if (!sessionId) {
306
+ return;
307
+ }
308
+ this.options.transport.sendInput(sessionId, data).catch(error => {
309
+ this.logger.warn('[TerminalInstanceController] sendInput failed', { error });
310
+ });
311
+ }
312
+ scheduleFocus() {
313
+ if (!this.options.autoFocus) {
314
+ return;
315
+ }
316
+ this.scheduler.requestFrame(() => {
317
+ this.scheduler.requestFrame(() => {
318
+ this.terminalCore?.focus();
319
+ });
320
+ });
321
+ }
322
+ cleanupTerminal() {
323
+ this.initializationAbortController?.abort();
324
+ this.initializationAbortController = null;
325
+ this.isInitializing = false;
326
+ if (this.terminalCore) {
327
+ this.terminalCore.dispose();
328
+ this.terminalCore = null;
329
+ }
330
+ this.dataQueue = [];
331
+ this.isProcessing = false;
332
+ }
333
+ finishReplayIfIdle() {
334
+ if (!this.replayCompleteReceived || this.dataQueue.length > 0 || this.isProcessing) {
335
+ return;
336
+ }
337
+ if (this.isReplayActive) {
338
+ this.terminalCore?.endHistoryReplay?.();
339
+ this.isReplayActive = false;
340
+ }
341
+ this.setLoading('ready', '');
342
+ this.scheduleFocus();
343
+ }
344
+ scheduleDataQueueFlush() {
345
+ if (this.flushRaf !== null) {
346
+ return;
347
+ }
348
+ const generation = this.queueGeneration;
349
+ this.flushRaf = this.scheduler.requestFrame(() => {
350
+ this.flushRaf = null;
351
+ if (this.queueGeneration !== generation) {
352
+ return;
353
+ }
354
+ void this.processDataQueue();
355
+ });
356
+ }
357
+ cancelDataQueueFlush() {
358
+ if (this.flushRaf === null) {
359
+ return;
360
+ }
361
+ this.scheduler.cancelFrame(this.flushRaf);
362
+ this.flushRaf = null;
363
+ }
364
+ async processDataQueue() {
365
+ const generation = this.queueGeneration;
366
+ if (this.isProcessing || !this.terminalCore || this.dataQueue.length === 0) {
367
+ return;
368
+ }
369
+ const terminalState = this.terminalCore.getState();
370
+ if (terminalState !== TerminalState.READY && terminalState !== TerminalState.CONNECTED) {
371
+ this.clearQueueRetryTimeout();
372
+ this.queueRetryTimeout = this.scheduler.setTimer(() => {
373
+ this.queueRetryTimeout = null;
374
+ if (this.queueGeneration !== generation) {
375
+ return;
376
+ }
377
+ void this.processDataQueue();
378
+ }, 100);
379
+ return;
380
+ }
381
+ this.isProcessing = true;
382
+ try {
383
+ let batchLength = 0;
384
+ let batchBytes = 0;
385
+ for (const chunk of this.dataQueue) {
386
+ if (batchLength >= MAX_WRITE_BATCH_CHUNKS) {
387
+ break;
388
+ }
389
+ if (batchLength > 0 && batchBytes + chunk.data.byteLength > MAX_WRITE_BATCH_BYTES) {
390
+ break;
391
+ }
392
+ batchLength += 1;
393
+ batchBytes += chunk.data.byteLength;
394
+ }
395
+ const batch = this.dataQueue.splice(0, Math.max(1, batchLength));
396
+ if (this.queueGeneration !== generation) {
397
+ return;
398
+ }
399
+ const payload = batch.length === 1 ? batch[0].data : concatChunks(batch.map(chunk => chunk.data));
400
+ this.terminalCore.write(payload);
401
+ for (const chunk of batch) {
402
+ if (this.queueGeneration !== generation) {
403
+ return;
404
+ }
405
+ if (chunk.sequence > this.lastAppliedSequence) {
406
+ this.lastAppliedSequence = chunk.sequence;
407
+ }
408
+ }
409
+ }
410
+ finally {
411
+ this.isProcessing = false;
412
+ if (this.dataQueue.length > 0) {
413
+ this.scheduleDataQueueFlush();
414
+ }
415
+ else {
416
+ this.finishReplayIfIdle();
417
+ }
418
+ }
419
+ }
420
+ addChunkToQueue(chunk) {
421
+ if (chunk.sequence > 0 && chunk.sequence <= this.lastAppliedSequence) {
422
+ return;
423
+ }
424
+ const ready = this.sequenceBuffer.push(chunk);
425
+ if (ready.length === 0) {
426
+ return;
427
+ }
428
+ this.dataQueue.push(...ready);
429
+ if (!this.isProcessing) {
430
+ this.scheduleDataQueueFlush();
431
+ }
432
+ }
433
+ applyCoreAppearance(appearance) {
434
+ const core = this.terminalCore;
435
+ if (!core) {
436
+ return;
437
+ }
438
+ const theme = appearance.themeName ? getThemeColors(appearance.themeName) : undefined;
439
+ if (core.setAppearance) {
440
+ core.setAppearance({
441
+ ...(theme ? { theme } : {}),
442
+ ...(typeof appearance.fontSize === 'number' ? { fontSize: appearance.fontSize } : {}),
443
+ ...(typeof appearance.fontFamily === 'string' ? { fontFamily: appearance.fontFamily } : {}),
444
+ ...(typeof appearance.presentationScale === 'number' ? { presentationScale: appearance.presentationScale } : {})
445
+ });
446
+ return;
447
+ }
448
+ if (theme) {
449
+ core.setTheme(theme);
450
+ }
451
+ if (typeof appearance.fontSize === 'number') {
452
+ core.setFontSize(appearance.fontSize);
453
+ }
454
+ if (typeof appearance.fontFamily === 'string') {
455
+ core.setFontFamily?.(appearance.fontFamily);
456
+ }
457
+ if (typeof appearance.presentationScale === 'number') {
458
+ core.setPresentationScale(appearance.presentationScale);
459
+ }
460
+ }
461
+ scheduleInitialize() {
462
+ if (!this.options.isActive || !this.container || this.terminalCore || this.isInitializing || this.initializeTimeout) {
463
+ return;
464
+ }
465
+ this.initializeTimeout = this.scheduler.setTimer(() => {
466
+ this.initializeTimeout = null;
467
+ void this.initializeTerminal();
468
+ }, 100);
469
+ }
470
+ clearInitializeTimeout() {
471
+ if (!this.initializeTimeout) {
472
+ return;
473
+ }
474
+ this.scheduler.clearTimer(this.initializeTimeout);
475
+ this.initializeTimeout = null;
476
+ }
477
+ async initializeTerminal() {
478
+ if (!this.container || this.isInitializing || this.terminalCore || this.disposed) {
479
+ return;
480
+ }
481
+ this.isInitializing = true;
482
+ const abortController = new AbortController();
483
+ this.initializationAbortController = abortController;
484
+ this.setLoading('initializing_terminal', 'Initializing terminal...');
485
+ try {
486
+ const permit = await terminalInitializationScheduler.acquire(abortController.signal);
487
+ if (!permit || abortController.signal.aborted || this.disposed || !this.container) {
488
+ return;
489
+ }
490
+ try {
491
+ const configOverrides = {
492
+ ...(this.options.config ?? {}),
493
+ sessionId: this.options.sessionId,
494
+ ...(this.options.fontSize ? { fontSize: this.options.fontSize } : {}),
495
+ ...(this.options.presentationScale ? { presentationScale: this.options.presentationScale } : {}),
496
+ };
497
+ const config = getDefaultTerminalConfig(this.options.themeName ?? 'dark', configOverrides);
498
+ const CoreCtor = this.options.coreConstructor ?? TerminalCore;
499
+ this.terminalCore = new CoreCtor(this.container, config, {
500
+ onData: data => this.handleUserInput(data),
501
+ onResize: size => void this.handleResize(size),
502
+ onStateChange: this.handleStateChange,
503
+ onError: this.handleError
504
+ }, this.logger ?? noopLogger);
505
+ await this.terminalCore.initialize();
506
+ if (this.isReplayActive) {
507
+ this.terminalCore.startHistoryReplay(30000);
508
+ }
509
+ const sessionId = this.options.sessionId;
510
+ if (sessionId) {
511
+ const dimensions = this.terminalCore.getDimensions();
512
+ await this.options.transport.resize(sessionId, dimensions.cols, dimensions.rows);
513
+ }
514
+ void this.processDataQueue();
515
+ if (!this.isReplayActive || this.replayCompleteReceived) {
516
+ this.setLoading('ready', '');
517
+ }
518
+ else {
519
+ this.setLoading('processing_history', 'Restoring terminal...');
520
+ }
521
+ if (this.options.isActive && sessionId) {
522
+ await this.connectToSession();
523
+ }
524
+ }
525
+ finally {
526
+ permit.release();
527
+ }
528
+ }
529
+ catch (error) {
530
+ if (abortController.signal.aborted) {
531
+ return;
532
+ }
533
+ this.handleError(error instanceof Error ? error : new Error(String(error)));
534
+ this.setLoading('ready', '');
535
+ }
536
+ finally {
537
+ if (this.initializationAbortController === abortController) {
538
+ this.initializationAbortController = null;
539
+ this.isInitializing = false;
540
+ }
541
+ }
542
+ }
543
+ async connectToSession() {
544
+ const sessionId = this.options.sessionId;
545
+ if (!sessionId || this.disposed) {
546
+ return;
547
+ }
548
+ this.clearRetryTimeout();
549
+ this.setConnectionState(ConnectionState.CONNECTING);
550
+ this.setConnectionError(null);
551
+ this.setLoading('attaching', 'Attaching to session...');
552
+ try {
553
+ const dims = this.terminalCore?.getDimensions() ?? this.dimensions;
554
+ await this.options.transport.attach(sessionId, dims.cols, dims.rows);
555
+ this.setConnectionState(ConnectionState.CONNECTED);
556
+ this.retryCount = 0;
557
+ this.emit();
558
+ if (!this.isReplayActive || this.replayCompleteReceived) {
559
+ this.setLoading('ready', '');
560
+ }
561
+ else {
562
+ this.setLoading('processing_history', 'Restoring terminal...');
563
+ }
564
+ }
565
+ catch (error) {
566
+ const terminalError = createTerminalError('transport', error);
567
+ this.setConnectionState(ConnectionState.FAILED);
568
+ this.setConnectionError(terminalError);
569
+ this.setLoading('ready', '');
570
+ this.scheduleConnectionRetry();
571
+ }
572
+ }
573
+ scheduleConnectionRetry() {
574
+ if (this.connectionState !== ConnectionState.FAILED || this.disposed) {
575
+ return;
576
+ }
577
+ const delay = Math.min(5000, 1000 * Math.pow(2, this.retryCount));
578
+ this.clearRetryTimeout();
579
+ this.retryTimeout = this.scheduler.setTimer(() => {
580
+ this.retryTimeout = null;
581
+ this.retryCount += 1;
582
+ this.setConnectionState(ConnectionState.RETRYING);
583
+ void this.connectToSession();
584
+ }, delay);
585
+ }
586
+ clearRetryTimeout() {
587
+ if (!this.retryTimeout) {
588
+ return;
589
+ }
590
+ this.scheduler.clearTimer(this.retryTimeout);
591
+ this.retryTimeout = null;
592
+ }
593
+ clearQueueRetryTimeout() {
594
+ if (!this.queueRetryTimeout) {
595
+ return;
596
+ }
597
+ this.scheduler.clearTimer(this.queueRetryTimeout);
598
+ this.queueRetryTimeout = null;
599
+ }
600
+ subscribeToTerminalData() {
601
+ this.terminalDataUnsubscribe?.();
602
+ this.terminalDataUnsubscribe = null;
603
+ const sessionId = this.options.sessionId;
604
+ if (!sessionId) {
605
+ return;
606
+ }
607
+ this.isReplayActive = true;
608
+ this.replayCompleteReceived = false;
609
+ this.terminalCore?.startHistoryReplay(30000);
610
+ this.setLoading('processing_history', 'Restoring terminal...');
611
+ const lastSeq = this.terminalCore ? this.lastAppliedSequence : 0;
612
+ const unsubscribe = this.options.eventSource.onTerminalData(sessionId, (payload) => {
613
+ if (payload.type === 'error') {
614
+ const message = payload.error || 'Terminal stream failed';
615
+ const terminalError = createTerminalError('connection', new Error(message));
616
+ this.setConnectionState(ConnectionState.FAILED);
617
+ this.setConnectionError(terminalError);
618
+ this.setLoading('ready', '');
619
+ this.updateState({ error: new Error(message), state: TerminalState.ERROR });
620
+ this.options.onError?.(new Error(message));
621
+ return;
622
+ }
623
+ if (payload.type === 'replay-complete') {
624
+ this.replayCompleteReceived = true;
625
+ const ready = this.sequenceBuffer.flushPending();
626
+ if (ready.length > 0) {
627
+ this.dataQueue.push(...ready);
628
+ this.scheduleDataQueueFlush();
629
+ }
630
+ else if (typeof payload.sequence === 'number'
631
+ && payload.sequence > this.lastAppliedSequence
632
+ && this.dataQueue.length === 0
633
+ && !this.isProcessing) {
634
+ this.lastAppliedSequence = payload.sequence;
635
+ this.sequenceBuffer.reset(payload.sequence + 1);
636
+ }
637
+ this.finishReplayIfIdle();
638
+ return;
639
+ }
640
+ const chunk = {
641
+ data: payload.data,
642
+ sequence: payload.sequence ?? 0,
643
+ timestampMs: payload.timestampMs ?? Date.now()
644
+ };
645
+ this.addChunkToQueue(chunk);
646
+ }, { lastSeq });
647
+ this.terminalDataUnsubscribe = unsubscribe;
648
+ }
649
+ async reinitialize() {
650
+ this.terminalDataUnsubscribe?.();
651
+ this.terminalDataUnsubscribe = null;
652
+ this.queueGeneration += 1;
653
+ this.sequenceBuffer.reset(1);
654
+ this.replayCompleteReceived = false;
655
+ this.isReplayActive = true;
656
+ this.lastAppliedSequence = 0;
657
+ this.dataQueue = [];
658
+ this.isProcessing = false;
659
+ this.cancelDataQueueFlush();
660
+ this.clearQueueRetryTimeout();
661
+ this.setLoading('processing_history', 'Restoring terminal...');
662
+ this.cleanupTerminal();
663
+ this.subscribeToTerminalData();
664
+ await this.initializeTerminal();
665
+ }
666
+ }
667
+ export const createTerminalInstance = (options) => {
668
+ return new FrameworkNeutralTerminalInstanceController(options);
669
+ };
670
+ //# sourceMappingURL=TerminalInstanceController.js.map