@tutti-os/workspace-terminal 0.0.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.
@@ -0,0 +1,1722 @@
1
+ import {
2
+ createBufferedTerminalInputQueue,
3
+ createTerminalAttachmentController,
4
+ createTerminalCloseDiagnostics,
5
+ createTerminalScreenStateCache,
6
+ createTerminalSessionRecoveryDiagnostics,
7
+ createTerminalSurfaceDiagnostics,
8
+ detectTerminalFileLinks,
9
+ isTerminalSessionEndedStatus,
10
+ resolveTerminalScrollbackDelta,
11
+ toTerminalLinkTarget,
12
+ truncateTerminalScrollback,
13
+ writeQueuedTerminalInput
14
+ } from "./chunk-56B5GYMU.js";
15
+
16
+ // src/core/sessionControllerRecovery.ts
17
+ function createTerminalSessionRecovery(input) {
18
+ let disposed = false;
19
+ let hydrationReady = false;
20
+ const hydrationReplayChunks = [];
21
+ const disposeData = input.transportRuntime.onData((event) => {
22
+ if (disposed || event.sessionId !== input.sessionId) {
23
+ return;
24
+ }
25
+ if (!hydrationReady) {
26
+ hydrationReplayChunks.push(event.data);
27
+ return;
28
+ }
29
+ input.store.appendRawOutput(event.data);
30
+ });
31
+ const disposeState = input.transportRuntime.onState((event) => {
32
+ if (disposed || event.sessionId !== input.sessionId) {
33
+ return;
34
+ }
35
+ if (typeof event.gapStartSeq === "number" && typeof event.gapEndSeq === "number") {
36
+ input.diagnostics.hydrationGap({
37
+ fromSeq: event.gapStartSeq,
38
+ toSeq: event.gapEndSeq
39
+ });
40
+ input.store.setSurfaceError(input.replayGapMessage);
41
+ return;
42
+ }
43
+ if (event.error) {
44
+ input.store.setSurfaceError(event.error);
45
+ }
46
+ if (event.status === "detached") {
47
+ input.onWriteReadyChange(false);
48
+ input.store.setInputReady(false);
49
+ input.onRecoverableDetach();
50
+ return;
51
+ }
52
+ if (event.status === "exited") {
53
+ input.onWriteReadyChange(false);
54
+ input.store.setInputReady(false);
55
+ }
56
+ });
57
+ const disposeExit = input.transportRuntime.onExit((event) => {
58
+ if (disposed || event.sessionId !== input.sessionId) {
59
+ return;
60
+ }
61
+ input.onWriteReadyChange(false);
62
+ input.store.setInputReady(false);
63
+ });
64
+ const disposeMetadata = input.transportRuntime.onMetadata(() => void 0);
65
+ return {
66
+ dispose() {
67
+ if (disposed) {
68
+ return;
69
+ }
70
+ disposed = true;
71
+ hydrationReady = false;
72
+ hydrationReplayChunks.length = 0;
73
+ disposeData();
74
+ disposeState();
75
+ disposeExit();
76
+ disposeMetadata();
77
+ },
78
+ async start() {
79
+ input.diagnostics.hydrationStart();
80
+ input.diagnostics.snapshotStart();
81
+ try {
82
+ const snapshot = await input.transportRuntime.snapshot();
83
+ if (disposed) {
84
+ return;
85
+ }
86
+ input.store.replaceRawOutput(snapshot.data);
87
+ const snapshotState = input.store.getState();
88
+ input.diagnostics.outputProjected({
89
+ contentEpoch: snapshotState.contentEpoch,
90
+ previewHead: previewTerminalOutput(snapshot.data, "head"),
91
+ previewTail: previewTerminalOutput(snapshot.data, "tail"),
92
+ rawBytes: snapshotState.rawOutput.length,
93
+ source: "snapshot",
94
+ writeBytes: snapshot.data.length
95
+ });
96
+ if (snapshot.truncated) {
97
+ input.store.setSurfaceError(input.snapshotTruncatedMessage);
98
+ } else {
99
+ input.store.setSurfaceError(null);
100
+ }
101
+ input.diagnostics.snapshotComplete({
102
+ toSeq: snapshot.toSeq ?? null,
103
+ truncated: snapshot.truncated ?? false
104
+ });
105
+ input.diagnostics.attachStart(snapshot.toSeq ?? null);
106
+ await input.transportRuntime.attach(snapshot.toSeq);
107
+ if (disposed) {
108
+ return;
109
+ }
110
+ hydrationReady = true;
111
+ const replayChunks = hydrationReplayChunks.splice(0);
112
+ replayChunks.forEach((chunk) => {
113
+ input.store.appendRawOutput(chunk);
114
+ });
115
+ if (replayChunks.length > 0) {
116
+ const replayState = input.store.getState();
117
+ input.diagnostics.outputProjected({
118
+ contentEpoch: replayState.contentEpoch,
119
+ previewHead: previewTerminalOutput(replayChunks.join(""), "head"),
120
+ previewTail: previewTerminalOutput(replayChunks.join(""), "tail"),
121
+ rawBytes: replayState.rawOutput.length,
122
+ source: "replay",
123
+ writeBytes: replayChunks.reduce(
124
+ (total, chunk) => total + chunk.length,
125
+ 0
126
+ )
127
+ });
128
+ }
129
+ input.onWriteReadyChange(true);
130
+ input.store.setInputReady(true);
131
+ await writeQueuedTerminalInput({
132
+ queue: input.inputQueue,
133
+ sessionId: input.sessionId,
134
+ transport: input.transport
135
+ });
136
+ if (disposed) {
137
+ return;
138
+ }
139
+ input.diagnostics.attachComplete();
140
+ input.diagnostics.hydrationComplete(replayChunks.length);
141
+ } catch (error) {
142
+ if (disposed) {
143
+ return;
144
+ }
145
+ input.diagnostics.attachError();
146
+ input.store.setSurfaceError(errorMessage(error));
147
+ }
148
+ }
149
+ };
150
+ }
151
+ function errorMessage(error) {
152
+ if (error instanceof Error) {
153
+ return error.message;
154
+ }
155
+ return String(error);
156
+ }
157
+ function previewTerminalOutput(value, side, maxChars = 48) {
158
+ const slice = side === "head" ? value.slice(0, maxChars) : value.slice(Math.max(0, value.length - maxChars));
159
+ return slice.replaceAll("\\", "\\\\").replaceAll("\r", "\\r").replaceAll("\n", "\\n");
160
+ }
161
+
162
+ // src/core/sessionControllerStore.ts
163
+ var initialState = {
164
+ contentEpoch: 0,
165
+ inputReady: false,
166
+ rawOutput: "",
167
+ surfaceError: null
168
+ };
169
+ function createTerminalSessionControllerStore(input) {
170
+ let state = initialState;
171
+ const listeners = /* @__PURE__ */ new Set();
172
+ return {
173
+ appendRawOutput(data) {
174
+ if (!data) {
175
+ return;
176
+ }
177
+ const nextOutput = truncateTerminalScrollback(
178
+ `${state.rawOutput}${data}`,
179
+ {
180
+ maxChars: input.maxScrollbackChars
181
+ }
182
+ );
183
+ if (nextOutput === state.rawOutput) {
184
+ return;
185
+ }
186
+ state = {
187
+ ...state,
188
+ rawOutput: nextOutput
189
+ };
190
+ emitState(listeners);
191
+ },
192
+ clearListeners() {
193
+ listeners.clear();
194
+ },
195
+ getState() {
196
+ return state;
197
+ },
198
+ replaceRawOutput(rawOutput) {
199
+ state = {
200
+ ...state,
201
+ contentEpoch: state.contentEpoch + 1,
202
+ rawOutput: truncateTerminalScrollback(rawOutput, {
203
+ maxChars: input.maxScrollbackChars
204
+ })
205
+ };
206
+ emitState(listeners);
207
+ },
208
+ setInputReady(inputReady) {
209
+ if (state.inputReady === inputReady) {
210
+ return;
211
+ }
212
+ state = {
213
+ ...state,
214
+ inputReady
215
+ };
216
+ emitState(listeners);
217
+ },
218
+ setSurfaceError(surfaceError) {
219
+ if (state.surfaceError === surfaceError) {
220
+ return;
221
+ }
222
+ state = {
223
+ ...state,
224
+ surfaceError
225
+ };
226
+ emitState(listeners);
227
+ },
228
+ subscribe(listener) {
229
+ listeners.add(listener);
230
+ return () => {
231
+ listeners.delete(listener);
232
+ };
233
+ }
234
+ };
235
+ }
236
+ function emitState(listeners) {
237
+ for (const listener of listeners) {
238
+ listener();
239
+ }
240
+ }
241
+
242
+ // src/core/sessionTransportRuntime.ts
243
+ var defaultDetachGraceMs = 200;
244
+ var runtimeRegistry = /* @__PURE__ */ new Map();
245
+ var runtimeClientSequence = 0;
246
+ function acquireTerminalSessionTransportRuntime(input) {
247
+ const entry = runtimeRegistry.get(input.sessionId) ?? createTerminalSessionTransportRuntimeEntry(input);
248
+ if (!runtimeRegistry.has(input.sessionId)) {
249
+ runtimeRegistry.set(input.sessionId, entry);
250
+ }
251
+ entry.refCount += 1;
252
+ if (entry.detachTimer !== null) {
253
+ clearTimeout(entry.detachTimer);
254
+ entry.detachTimer = null;
255
+ }
256
+ return {
257
+ attach(afterSeq) {
258
+ return entry.attach(afterSeq);
259
+ },
260
+ onData(listener) {
261
+ entry.dataListeners.add(listener);
262
+ flushBufferedEvents(entry.dataBuffer, listener);
263
+ return () => {
264
+ entry.dataListeners.delete(listener);
265
+ };
266
+ },
267
+ onExit(listener) {
268
+ entry.exitListeners.add(listener);
269
+ flushBufferedEvents(entry.exitBuffer, listener);
270
+ return () => {
271
+ entry.exitListeners.delete(listener);
272
+ };
273
+ },
274
+ onMetadata(listener) {
275
+ entry.metadataListeners.add(listener);
276
+ flushBufferedEvents(entry.metadataBuffer, listener);
277
+ return () => {
278
+ entry.metadataListeners.delete(listener);
279
+ };
280
+ },
281
+ onState(listener) {
282
+ entry.stateListeners.add(listener);
283
+ flushBufferedEvents(entry.stateBuffer, listener);
284
+ return () => {
285
+ entry.stateListeners.delete(listener);
286
+ };
287
+ },
288
+ release() {
289
+ entry.release();
290
+ },
291
+ snapshot() {
292
+ return entry.snapshot();
293
+ }
294
+ };
295
+ }
296
+ function createTerminalSessionTransportRuntimeEntry(input) {
297
+ const attachmentController = createTerminalAttachmentController({
298
+ clientId: createTerminalTransportClientId(),
299
+ sessionId: input.sessionId,
300
+ transport: input.transport
301
+ });
302
+ const entry = {
303
+ attach(afterSeq) {
304
+ return attachmentController.attach(afterSeq);
305
+ },
306
+ dataBuffer: [],
307
+ dataListeners: /* @__PURE__ */ new Set(),
308
+ detachGraceMs: input.detachGraceMs ?? defaultDetachGraceMs,
309
+ detachTimer: null,
310
+ exitBuffer: [],
311
+ exitListeners: /* @__PURE__ */ new Set(),
312
+ metadataBuffer: [],
313
+ metadataListeners: /* @__PURE__ */ new Set(),
314
+ refCount: 0,
315
+ release() {
316
+ entry.refCount = Math.max(0, entry.refCount - 1);
317
+ if (entry.refCount > 0 || entry.detachTimer !== null) {
318
+ return;
319
+ }
320
+ entry.detachTimer = setTimeout(() => {
321
+ entry.detachTimer = null;
322
+ if (entry.refCount > 0) {
323
+ return;
324
+ }
325
+ void attachmentController.detach().finally(() => {
326
+ entry.teardown();
327
+ runtimeRegistry.delete(entry.sessionId);
328
+ });
329
+ }, entry.detachGraceMs);
330
+ },
331
+ sessionId: input.sessionId,
332
+ snapshot() {
333
+ if (entry.snapshotPromise) {
334
+ return entry.snapshotPromise;
335
+ }
336
+ if (entry.snapshotCache) {
337
+ return Promise.resolve(entry.snapshotCache);
338
+ }
339
+ entry.snapshotPromise = input.transport.snapshot({ sessionId: input.sessionId }).then((snapshot) => {
340
+ entry.snapshotCache = snapshot;
341
+ return snapshot;
342
+ }).finally(() => {
343
+ entry.snapshotPromise = null;
344
+ });
345
+ return entry.snapshotPromise;
346
+ },
347
+ snapshotCache: null,
348
+ snapshotPromise: null,
349
+ stateBuffer: [],
350
+ stateListeners: /* @__PURE__ */ new Set(),
351
+ teardown() {
352
+ disposeData();
353
+ disposeState();
354
+ disposeExit();
355
+ disposeMetadata?.();
356
+ entry.dataBuffer.length = 0;
357
+ entry.exitBuffer.length = 0;
358
+ entry.metadataBuffer.length = 0;
359
+ entry.stateBuffer.length = 0;
360
+ entry.snapshotCache = null;
361
+ entry.snapshotPromise = null;
362
+ }
363
+ };
364
+ const disposeData = input.transport.onData((event) => {
365
+ if (event.sessionId !== input.sessionId) {
366
+ return;
367
+ }
368
+ entry.snapshotCache = null;
369
+ dispatchOrBuffer(entry.dataListeners, entry.dataBuffer, event);
370
+ });
371
+ const disposeState = input.transport.onState((event) => {
372
+ if (event.sessionId !== input.sessionId) {
373
+ return;
374
+ }
375
+ if (event.status === "detached" || event.status === "exited") {
376
+ attachmentController.markDetached();
377
+ }
378
+ if (typeof event.gapStartSeq === "number" || typeof event.gapEndSeq === "number") {
379
+ entry.snapshotCache = null;
380
+ }
381
+ dispatchOrBuffer(entry.stateListeners, entry.stateBuffer, event);
382
+ });
383
+ const disposeExit = input.transport.onExit((event) => {
384
+ if (event.sessionId !== input.sessionId) {
385
+ return;
386
+ }
387
+ dispatchOrBuffer(entry.exitListeners, entry.exitBuffer, event);
388
+ });
389
+ const disposeMetadata = input.transport.onMetadata?.((event) => {
390
+ if (event.sessionId !== input.sessionId) {
391
+ return;
392
+ }
393
+ dispatchOrBuffer(entry.metadataListeners, entry.metadataBuffer, event);
394
+ });
395
+ return entry;
396
+ }
397
+ function createTerminalTransportClientId() {
398
+ runtimeClientSequence += 1;
399
+ return `terminal-runtime-${runtimeClientSequence}`;
400
+ }
401
+ function dispatchOrBuffer(listeners, buffer, event) {
402
+ if (listeners.size === 0) {
403
+ buffer.push(event);
404
+ return;
405
+ }
406
+ for (const listener of listeners) {
407
+ listener(event);
408
+ }
409
+ }
410
+ function flushBufferedEvents(buffer, listener) {
411
+ if (buffer.length === 0) {
412
+ return;
413
+ }
414
+ const pending = buffer.splice(0);
415
+ for (const event of pending) {
416
+ listener(event);
417
+ }
418
+ }
419
+
420
+ // src/core/sessionController.ts
421
+ var defaultControllerRetainMs = 5e3;
422
+ var controllerRegistry = /* @__PURE__ */ new Map();
423
+ function acquireTerminalSessionController(input) {
424
+ const entry = controllerRegistry.get(input.sessionId) ?? createTerminalSessionControllerEntry({
425
+ feature: input.feature,
426
+ nodeId: input.nodeId,
427
+ retainMs: input.retainMs ?? defaultControllerRetainMs,
428
+ sessionId: input.sessionId
429
+ });
430
+ if (!controllerRegistry.has(input.sessionId)) {
431
+ controllerRegistry.set(input.sessionId, entry);
432
+ }
433
+ entry.context = {
434
+ feature: input.feature,
435
+ nodeId: input.nodeId,
436
+ sessionId: input.sessionId
437
+ };
438
+ return {
439
+ getState() {
440
+ return entry.store.getState();
441
+ },
442
+ retain() {
443
+ entry.refCount += 1;
444
+ if (entry.retainTimer !== null) {
445
+ clearTimeout(entry.retainTimer);
446
+ entry.retainTimer = null;
447
+ }
448
+ ensureRecoveryStarted(entry);
449
+ },
450
+ release() {
451
+ entry.release();
452
+ },
453
+ resize({ cols, rows }) {
454
+ return entry.context.feature.transport.resize({
455
+ cols,
456
+ rows,
457
+ sessionId: entry.context.sessionId
458
+ });
459
+ },
460
+ subscribe(listener) {
461
+ return entry.store.subscribe(listener);
462
+ },
463
+ write(data, encoding = "utf8") {
464
+ if (!entry.runtimeWriteReady) {
465
+ entry.inputQueue.enqueue(data, encoding);
466
+ ensureRecoveryStarted(entry);
467
+ return;
468
+ }
469
+ void writeTerminalInput(entry, data, encoding);
470
+ }
471
+ };
472
+ }
473
+ function createTerminalSessionControllerEntry(input) {
474
+ const transportRuntime = acquireTerminalSessionTransportRuntime({
475
+ sessionId: input.sessionId,
476
+ transport: input.feature.transport
477
+ });
478
+ const inputQueue = createBufferedTerminalInputQueue();
479
+ const store = createTerminalSessionControllerStore({
480
+ maxScrollbackChars: resolveMaxScrollbackChars(input.feature.limits)
481
+ });
482
+ const entry = {
483
+ context: {
484
+ feature: input.feature,
485
+ nodeId: input.nodeId,
486
+ sessionId: input.sessionId
487
+ },
488
+ disposed: false,
489
+ inputQueue,
490
+ recovery: null,
491
+ refCount: 0,
492
+ release: () => void 0,
493
+ retainTimer: null,
494
+ runtimeWriteReady: false,
495
+ startPromise: null,
496
+ store,
497
+ teardown: () => void 0,
498
+ transportRuntime
499
+ };
500
+ entry.release = () => {
501
+ entry.refCount = Math.max(0, entry.refCount - 1);
502
+ if (entry.refCount > 0 || entry.retainTimer !== null) {
503
+ return;
504
+ }
505
+ entry.retainTimer = setTimeout(() => {
506
+ entry.retainTimer = null;
507
+ if (entry.refCount > 0) {
508
+ return;
509
+ }
510
+ entry.teardown();
511
+ controllerRegistry.delete(entry.context.sessionId);
512
+ }, input.retainMs);
513
+ };
514
+ entry.teardown = () => {
515
+ if (entry.disposed) {
516
+ return;
517
+ }
518
+ entry.disposed = true;
519
+ inputQueue.reset();
520
+ entry.runtimeWriteReady = false;
521
+ entry.recovery.dispose();
522
+ transportRuntime.release();
523
+ store.clearListeners();
524
+ };
525
+ entry.recovery = createTerminalSessionRecovery({
526
+ diagnostics: createTerminalSessionRecoveryDiagnostics({
527
+ diagnostics: input.feature.diagnostics,
528
+ nodeId: input.nodeId,
529
+ sessionId: input.sessionId
530
+ }),
531
+ inputQueue,
532
+ onRecoverableDetach() {
533
+ if (entry.disposed || entry.refCount === 0) {
534
+ return;
535
+ }
536
+ ensureRecoveryStarted(entry);
537
+ },
538
+ onWriteReadyChange(ready) {
539
+ if (entry.disposed) {
540
+ return;
541
+ }
542
+ entry.runtimeWriteReady = ready;
543
+ },
544
+ replayGapMessage: input.feature.i18n.t("recovery.replayGap"),
545
+ sessionId: input.sessionId,
546
+ snapshotTruncatedMessage: input.feature.i18n.t(
547
+ "recovery.snapshotTruncated"
548
+ ),
549
+ store,
550
+ transport: input.feature.transport,
551
+ transportRuntime
552
+ });
553
+ return entry;
554
+ }
555
+ function ensureRecoveryStarted(entry) {
556
+ if (entry.disposed || entry.startPromise !== null || entry.runtimeWriteReady) {
557
+ return;
558
+ }
559
+ entry.startPromise = entry.recovery.start().finally(() => {
560
+ entry.startPromise = null;
561
+ });
562
+ }
563
+ function resolveMaxScrollbackChars(limits) {
564
+ return Math.max(limits.maxScrollbackLines * 160, 4e5);
565
+ }
566
+ function writeTerminalInput(entry, data, encoding) {
567
+ return entry.context.feature.transport.write({
568
+ data,
569
+ encoding,
570
+ provenance: "user",
571
+ sessionId: entry.context.sessionId
572
+ }).catch((error) => {
573
+ entry.runtimeWriteReady = false;
574
+ entry.store.setInputReady(false);
575
+ entry.inputQueue.enqueue(data, encoding);
576
+ ensureRecoveryStarted(entry);
577
+ entry.context.feature.diagnostics.log("write-error", {
578
+ nodeId: entry.context.nodeId,
579
+ sessionId: entry.context.sessionId
580
+ });
581
+ entry.store.setSurfaceError(errorMessage2(error));
582
+ });
583
+ }
584
+ function errorMessage2(error) {
585
+ if (error instanceof Error) {
586
+ return error.message;
587
+ }
588
+ return String(error);
589
+ }
590
+
591
+ // src/react/TerminalNode.tsx
592
+ import { useState as useState3 } from "react";
593
+ import { Badge, StatusDot } from "@tutti-os/ui-system";
594
+
595
+ // src/react/headerActions.ts
596
+ function hasTerminalHeaderDefaultActions(defaultActions) {
597
+ return defaultActions !== void 0 && defaultActions !== null;
598
+ }
599
+
600
+ // src/react/TerminalSurface.tsx
601
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2 } from "react";
602
+
603
+ // src/react/terminalSurfaceRuntime.ts
604
+ import { FitAddon } from "@xterm/addon-fit";
605
+ import { SearchAddon } from "@xterm/addon-search";
606
+ import { SerializeAddon } from "@xterm/addon-serialize";
607
+ import { WebLinksAddon } from "@xterm/addon-web-links";
608
+ import { Terminal } from "@xterm/xterm";
609
+
610
+ // src/react/terminalFileLinkProvider.ts
611
+ function createTerminalFileLinkProvider(input) {
612
+ return {
613
+ provideLinks(bufferLineNumber, callback) {
614
+ if (!input.feature.linkHandler) {
615
+ callback(void 0);
616
+ return;
617
+ }
618
+ const line = input.terminal.buffer.active.getLine(bufferLineNumber - 1);
619
+ if (!line) {
620
+ callback(void 0);
621
+ return;
622
+ }
623
+ const text = line.translateToString(true);
624
+ const links = detectTerminalFileLinks(text).map((link) => ({
625
+ activate: () => {
626
+ void input.feature.linkHandler?.open({
627
+ ...toTerminalLinkTarget(link),
628
+ cwd: input.getCwd()
629
+ });
630
+ },
631
+ range: {
632
+ end: {
633
+ x: link.index + link.text.length,
634
+ y: bufferLineNumber
635
+ },
636
+ start: {
637
+ x: link.index + 1,
638
+ y: bufferLineNumber
639
+ }
640
+ },
641
+ text: link.text
642
+ }));
643
+ callback(links.length > 0 ? links : void 0);
644
+ }
645
+ };
646
+ }
647
+
648
+ // src/react/terminalSurfaceOutputPlan.ts
649
+ function resolveTerminalSurfaceOutputPlan(input) {
650
+ if (input.nextContentEpoch === 0 && input.rawOutput.length === 0 && input.committedRawOutput.length > 0) {
651
+ return null;
652
+ }
653
+ if (input.nextContentEpoch !== input.contentEpoch) {
654
+ if (input.rawOutput === input.committedRawOutput) {
655
+ return {
656
+ nextCommittedRawOutput: input.committedRawOutput,
657
+ nextContentEpoch: input.nextContentEpoch,
658
+ reset: false,
659
+ write: null
660
+ };
661
+ }
662
+ return {
663
+ nextCommittedRawOutput: input.rawOutput,
664
+ nextContentEpoch: input.nextContentEpoch,
665
+ reset: true,
666
+ write: input.rawOutput || null
667
+ };
668
+ }
669
+ if (input.rawOutput === input.committedRawOutput) {
670
+ return null;
671
+ }
672
+ const delta = resolveTerminalScrollbackDelta(
673
+ input.committedRawOutput,
674
+ input.rawOutput
675
+ );
676
+ return {
677
+ nextCommittedRawOutput: input.rawOutput,
678
+ nextContentEpoch: input.nextContentEpoch,
679
+ reset: !delta,
680
+ write: delta || input.rawOutput || null
681
+ };
682
+ }
683
+
684
+ // src/react/terminalSurfaceResizePlan.ts
685
+ function resolveTerminalSurfaceResizePlan(input) {
686
+ if (input.lastSize?.cols === input.nextSize.cols && input.lastSize.rows === input.nextSize.rows) {
687
+ return null;
688
+ }
689
+ return input.nextSize;
690
+ }
691
+
692
+ // src/react/terminalSurfaceScreenCachePlan.ts
693
+ function resolveTerminalSurfaceScreenCachePlan(input) {
694
+ if (input.hasPendingWrites) {
695
+ return {
696
+ action: input.serialized ? "skip" : "remove"
697
+ };
698
+ }
699
+ if (!input.serialized) {
700
+ return { action: "skip" };
701
+ }
702
+ return {
703
+ action: "save",
704
+ rawSnapshot: input.rawSnapshot,
705
+ serialized: input.serialized
706
+ };
707
+ }
708
+
709
+ // src/react/terminalWriteBatch.ts
710
+ function drainTerminalWriteBatch(chunks, maxBytes) {
711
+ let remaining = maxBytes;
712
+ let nextWrite = "";
713
+ const remainingChunks = chunks.slice();
714
+ while (remaining > 0 && remainingChunks.length > 0) {
715
+ const chunk = remainingChunks.shift() ?? "";
716
+ if (chunk.length <= remaining) {
717
+ nextWrite += chunk;
718
+ remaining -= chunk.length;
719
+ continue;
720
+ }
721
+ nextWrite += chunk.slice(0, remaining);
722
+ remainingChunks.unshift(chunk.slice(remaining));
723
+ remaining = 0;
724
+ }
725
+ return {
726
+ nextWrite,
727
+ remainingChunks
728
+ };
729
+ }
730
+
731
+ // src/react/terminalSurfaceRuntime.ts
732
+ var terminalScreenStateCache = createTerminalScreenStateCache();
733
+ function createTerminalSurfaceRuntime(input) {
734
+ let contentEpoch = 0;
735
+ let committedRawOutput = "";
736
+ let initialLayoutFrame = null;
737
+ let initialLayoutTimeout = null;
738
+ let lastSize = null;
739
+ let pendingRevealFinalizeFrame = null;
740
+ let pendingRevealStartFrame = null;
741
+ let pendingRevealWrite = null;
742
+ let pendingWriteFrame = null;
743
+ let pendingWriteChunks = [];
744
+ let pendingRenderedWrite = null;
745
+ const terminal = new Terminal({
746
+ allowProposedApi: false,
747
+ convertEol: true,
748
+ cursorBlink: true,
749
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
750
+ fontSize: 13,
751
+ scrollback: input.feature.limits.maxScrollbackLines,
752
+ theme: resolveXtermTheme(input.theme)
753
+ });
754
+ const fitAddon = new FitAddon();
755
+ const searchAddon = new SearchAddon();
756
+ const serializeAddon = new SerializeAddon();
757
+ const webLinksAddon = new WebLinksAddon((_event, uri) => {
758
+ void input.feature.linkHandler?.open({ url: uri });
759
+ });
760
+ terminal.loadAddon(fitAddon);
761
+ terminal.loadAddon(searchAddon);
762
+ terminal.loadAddon(serializeAddon);
763
+ terminal.loadAddon(webLinksAddon);
764
+ const fileLinkDisposable = terminal.registerLinkProvider(
765
+ createTerminalFileLinkProvider({
766
+ feature: input.feature,
767
+ getCwd: input.getCwd,
768
+ terminal
769
+ })
770
+ );
771
+ terminal.open(input.container);
772
+ const dataSubscription = terminal.onData((data) => {
773
+ input.onUserInput(data);
774
+ });
775
+ const binarySubscription = terminal.onBinary((data) => {
776
+ input.onUserInput(data, "binary");
777
+ });
778
+ const writeParsedSubscription = typeof terminal.onWriteParsed === "function" ? terminal.onWriteParsed(() => {
779
+ if (!pendingRenderedWrite) {
780
+ refreshTerminalSurface();
781
+ return;
782
+ }
783
+ const renderedWrite = pendingRenderedWrite;
784
+ pendingRenderedWrite = null;
785
+ if (renderedWrite.mode === "cache" || renderedWrite.mode === "replace") {
786
+ scheduleHydratedReveal({
787
+ bytes: renderedWrite.bytes,
788
+ mode: renderedWrite.mode,
789
+ remainingChunkCount: renderedWrite.remainingChunkCount
790
+ });
791
+ return;
792
+ }
793
+ refreshTerminalSurface();
794
+ logSurfaceWrite(
795
+ renderedWrite.mode,
796
+ renderedWrite.bytes,
797
+ renderedWrite.remainingChunkCount
798
+ );
799
+ }) : { dispose: () => void 0 };
800
+ const resizeTerminal = () => {
801
+ try {
802
+ fitAddon.fit();
803
+ } catch {
804
+ return;
805
+ }
806
+ const plan = resolveTerminalSurfaceResizePlan({
807
+ lastSize,
808
+ nextSize: { cols: terminal.cols, rows: terminal.rows }
809
+ });
810
+ if (!plan) {
811
+ return;
812
+ }
813
+ lastSize = plan;
814
+ input.onResize(plan);
815
+ };
816
+ const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => resizeTerminal());
817
+ resizeObserver?.observe(input.container);
818
+ scheduleInitialLayoutSync();
819
+ const cached = terminalScreenStateCache.get(input.nodeId, input.sessionId);
820
+ if (cached) {
821
+ terminal.write(cached.serialized, () => {
822
+ queueRenderedWrite("cache", cached.serialized.length, 0);
823
+ });
824
+ committedRawOutput = cached.rawSnapshot;
825
+ }
826
+ return {
827
+ dispose() {
828
+ resizeObserver?.disconnect();
829
+ dataSubscription.dispose();
830
+ binarySubscription.dispose();
831
+ writeParsedSubscription.dispose();
832
+ fileLinkDisposable.dispose();
833
+ clearInitialLayoutSync();
834
+ clearPendingReveal();
835
+ const hasPendingWrites = pendingWriteFrame !== null || pendingWriteChunks.length > 0;
836
+ const cachePlan = resolveTerminalSurfaceScreenCachePlan({
837
+ hasPendingWrites,
838
+ rawSnapshot: committedRawOutput,
839
+ serialized: hasPendingWrites ? "" : serializeAddon.serialize()
840
+ });
841
+ if (cachePlan.action === "save") {
842
+ terminalScreenStateCache.set(input.nodeId, {
843
+ cols: terminal.cols,
844
+ rawSnapshot: cachePlan.rawSnapshot,
845
+ rows: terminal.rows,
846
+ serialized: cachePlan.serialized,
847
+ sessionId: input.sessionId
848
+ });
849
+ } else if (cachePlan.action === "remove") {
850
+ terminalScreenStateCache.remove(input.nodeId, input.sessionId);
851
+ }
852
+ clearPendingWrites();
853
+ terminal.dispose();
854
+ },
855
+ findNext(query, options) {
856
+ searchAddon.findNext(query, options);
857
+ },
858
+ findPrevious(query, options) {
859
+ searchAddon.findPrevious(query, options);
860
+ },
861
+ focus() {
862
+ terminal.focus();
863
+ resizeTerminal();
864
+ },
865
+ setTheme(theme) {
866
+ terminal.options.theme = resolveXtermTheme(theme);
867
+ },
868
+ syncOutput(rawOutput, nextContentEpoch) {
869
+ const plan = resolveTerminalSurfaceOutputPlan({
870
+ committedRawOutput,
871
+ contentEpoch,
872
+ nextContentEpoch,
873
+ rawOutput
874
+ });
875
+ input.diagnostics.outputSync({
876
+ contentEpoch: nextContentEpoch,
877
+ nextCommittedBytes: plan?.nextCommittedRawOutput.length ?? committedRawOutput.length,
878
+ plan: resolveOutputSyncPlan(plan),
879
+ previousCommittedBytes: committedRawOutput.length,
880
+ rawBytes: rawOutput.length,
881
+ writeBytes: plan?.write?.length ?? 0
882
+ });
883
+ if (!plan) {
884
+ return;
885
+ }
886
+ contentEpoch = plan.nextContentEpoch;
887
+ if (plan.reset && plan.write) {
888
+ replaceTerminalView(plan.write, { reset: true });
889
+ } else if (plan.reset) {
890
+ resetTerminalView();
891
+ }
892
+ if (plan.write && !plan.reset) {
893
+ writeToTerminal(plan.write);
894
+ }
895
+ committedRawOutput = plan.nextCommittedRawOutput;
896
+ }
897
+ };
898
+ function clearPendingWrites() {
899
+ pendingWriteChunks = [];
900
+ if (pendingWriteFrame === null) {
901
+ return;
902
+ }
903
+ if (typeof cancelAnimationFrame === "function") {
904
+ cancelAnimationFrame(pendingWriteFrame);
905
+ } else {
906
+ clearTimeout(pendingWriteFrame);
907
+ }
908
+ pendingWriteFrame = null;
909
+ }
910
+ function clearInitialLayoutSync() {
911
+ if (initialLayoutFrame !== null) {
912
+ if (typeof cancelAnimationFrame === "function") {
913
+ cancelAnimationFrame(initialLayoutFrame);
914
+ } else {
915
+ clearTimeout(initialLayoutFrame);
916
+ }
917
+ initialLayoutFrame = null;
918
+ }
919
+ if (initialLayoutTimeout !== null) {
920
+ clearTimeout(initialLayoutTimeout);
921
+ initialLayoutTimeout = null;
922
+ }
923
+ }
924
+ function clearPendingReveal() {
925
+ if (pendingRevealStartFrame !== null) {
926
+ if (typeof cancelAnimationFrame === "function") {
927
+ cancelAnimationFrame(pendingRevealStartFrame);
928
+ } else {
929
+ clearTimeout(pendingRevealStartFrame);
930
+ }
931
+ pendingRevealStartFrame = null;
932
+ }
933
+ if (pendingRevealFinalizeFrame !== null) {
934
+ if (typeof cancelAnimationFrame === "function") {
935
+ cancelAnimationFrame(pendingRevealFinalizeFrame);
936
+ } else {
937
+ clearTimeout(pendingRevealFinalizeFrame);
938
+ }
939
+ pendingRevealFinalizeFrame = null;
940
+ }
941
+ pendingRevealWrite = null;
942
+ }
943
+ function flushPendingWrites() {
944
+ pendingWriteFrame = null;
945
+ const batch = drainTerminalWriteBatch(
946
+ pendingWriteChunks,
947
+ input.feature.limits.maxWriteBatchBytes
948
+ );
949
+ pendingWriteChunks = batch.remainingChunks;
950
+ if (!batch.nextWrite) {
951
+ return;
952
+ }
953
+ terminal.write(batch.nextWrite, () => {
954
+ queueRenderedWrite(
955
+ "append",
956
+ batch.nextWrite.length,
957
+ pendingWriteChunks.length
958
+ );
959
+ if (pendingWriteChunks.length > 0) {
960
+ requestWriteFlush();
961
+ }
962
+ });
963
+ }
964
+ function requestWriteFlush() {
965
+ if (pendingWriteFrame !== null) {
966
+ return;
967
+ }
968
+ if (typeof requestAnimationFrame === "function") {
969
+ pendingWriteFrame = requestAnimationFrame(flushPendingWrites);
970
+ return;
971
+ }
972
+ pendingWriteFrame = window.setTimeout(flushPendingWrites, 0);
973
+ }
974
+ function resetTerminalView() {
975
+ clearPendingReveal();
976
+ clearPendingWrites();
977
+ terminal.reset();
978
+ refreshTerminalSurface();
979
+ logSurfaceWrite("reset", 0, 0);
980
+ }
981
+ function replaceTerminalView(data, options) {
982
+ const transformed = input.feature.outputTransform?.({
983
+ data,
984
+ sessionId: input.sessionId
985
+ }) ?? data;
986
+ if (transformed == null || transformed === "") {
987
+ return;
988
+ }
989
+ clearPendingReveal();
990
+ clearPendingWrites();
991
+ if (options?.reset) {
992
+ terminal.reset();
993
+ }
994
+ terminal.write(transformed, () => {
995
+ queueRenderedWrite("replace", transformed.length, 0);
996
+ });
997
+ }
998
+ function refreshTerminalSurface() {
999
+ if (terminal.rows <= 0) {
1000
+ return;
1001
+ }
1002
+ terminal.refresh(0, terminal.rows - 1);
1003
+ }
1004
+ function scheduleInitialLayoutSync() {
1005
+ resizeTerminal();
1006
+ if (typeof requestAnimationFrame === "function") {
1007
+ initialLayoutFrame = requestAnimationFrame(() => {
1008
+ initialLayoutFrame = null;
1009
+ resizeTerminal();
1010
+ refreshTerminalSurface();
1011
+ });
1012
+ } else {
1013
+ initialLayoutFrame = window.setTimeout(() => {
1014
+ initialLayoutFrame = null;
1015
+ resizeTerminal();
1016
+ refreshTerminalSurface();
1017
+ }, 0);
1018
+ }
1019
+ initialLayoutTimeout = window.setTimeout(() => {
1020
+ initialLayoutTimeout = null;
1021
+ resizeTerminal();
1022
+ refreshTerminalSurface();
1023
+ }, 32);
1024
+ }
1025
+ function scheduleTerminalWrite(data) {
1026
+ pendingWriteChunks.push(data);
1027
+ requestWriteFlush();
1028
+ }
1029
+ function writeToTerminal(data) {
1030
+ const transformed = input.feature.outputTransform?.({
1031
+ data,
1032
+ sessionId: input.sessionId
1033
+ }) ?? data;
1034
+ if (transformed == null || transformed === "") {
1035
+ return;
1036
+ }
1037
+ scheduleTerminalWrite(transformed);
1038
+ }
1039
+ function queueRenderedWrite(mode, bytes, remainingChunkCount) {
1040
+ if (pendingRenderedWrite && pendingRenderedWrite.mode === mode) {
1041
+ pendingRenderedWrite = {
1042
+ bytes: pendingRenderedWrite.bytes + bytes,
1043
+ mode,
1044
+ remainingChunkCount
1045
+ };
1046
+ return;
1047
+ }
1048
+ pendingRenderedWrite = {
1049
+ bytes,
1050
+ mode,
1051
+ remainingChunkCount
1052
+ };
1053
+ }
1054
+ function scheduleHydratedReveal(write) {
1055
+ if (pendingRevealWrite) {
1056
+ pendingRevealWrite = {
1057
+ bytes: pendingRevealWrite.bytes + write.bytes,
1058
+ mode: write.mode,
1059
+ remainingChunkCount: write.remainingChunkCount
1060
+ };
1061
+ } else {
1062
+ pendingRevealWrite = write;
1063
+ }
1064
+ if (pendingRevealStartFrame !== null || pendingRevealFinalizeFrame !== null) {
1065
+ return;
1066
+ }
1067
+ pendingRevealStartFrame = requestTerminalFrame(() => {
1068
+ pendingRevealStartFrame = null;
1069
+ resizeTerminal();
1070
+ pendingRevealFinalizeFrame = requestTerminalFrame(() => {
1071
+ pendingRevealFinalizeFrame = null;
1072
+ refreshTerminalSurface();
1073
+ const revealWrite = pendingRevealWrite;
1074
+ pendingRevealWrite = null;
1075
+ if (!revealWrite) {
1076
+ return;
1077
+ }
1078
+ logSurfaceWrite(
1079
+ revealWrite.mode,
1080
+ revealWrite.bytes,
1081
+ revealWrite.remainingChunkCount
1082
+ );
1083
+ });
1084
+ });
1085
+ }
1086
+ function logSurfaceWrite(mode, bytes, remainingChunkCount) {
1087
+ const buffer = terminal.buffer.active;
1088
+ const xtermElement = input.container.querySelector(".xterm") instanceof HTMLElement ? input.container.querySelector(".xterm") : null;
1089
+ const screenElement = input.container.querySelector(".xterm-screen") instanceof HTMLElement ? input.container.querySelector(".xterm-screen") : null;
1090
+ const helperElement = input.container.querySelector(".xterm-helpers") instanceof HTMLElement ? input.container.querySelector(".xterm-helpers") : null;
1091
+ const rowContainerElement = input.container.querySelector(".xterm-rows") instanceof HTMLElement ? input.container.querySelector(".xterm-rows") : null;
1092
+ const firstCanvas = screenElement?.querySelector("canvas") instanceof HTMLCanvasElement ? screenElement.querySelector("canvas") : null;
1093
+ const firstLine = buffer.getLine(0)?.translateToString(true) ?? "";
1094
+ const cursorLine = buffer.getLine(buffer.cursorY)?.translateToString(true) ?? "";
1095
+ const xtermStyle = xtermElement ? window.getComputedStyle(xtermElement) : null;
1096
+ const screenStyle = screenElement ? window.getComputedStyle(screenElement) : null;
1097
+ const renderDimensions = resolveXtermRenderDimensions(terminal);
1098
+ input.diagnostics.outputWritten({
1099
+ bufferCursorX: buffer.cursorX,
1100
+ bufferCursorY: buffer.cursorY,
1101
+ bufferLength: buffer.length,
1102
+ bytes,
1103
+ canvasClientHeight: firstCanvas?.clientHeight ?? 0,
1104
+ canvasClientWidth: firstCanvas?.clientWidth ?? 0,
1105
+ canvasCount: screenElement?.querySelectorAll("canvas").length ?? 0,
1106
+ cols: terminal.cols,
1107
+ containerClientHeight: input.container.clientHeight,
1108
+ containerClientWidth: input.container.clientWidth,
1109
+ cursorLinePreview: previewTerminalLine(cursorLine),
1110
+ firstLinePreview: previewTerminalLine(firstLine),
1111
+ helperChildCount: helperElement?.childElementCount ?? 0,
1112
+ mode,
1113
+ remainingChunkCount,
1114
+ renderDimensionsReady: renderDimensions !== void 0,
1115
+ rowContainerChildCount: rowContainerElement?.childElementCount ?? 0,
1116
+ rowContainerTextPreview: previewTerminalLine(
1117
+ rowContainerElement?.textContent ?? ""
1118
+ ),
1119
+ rows: terminal.rows,
1120
+ screenChildCount: screenElement?.childElementCount ?? 0,
1121
+ screenClientHeight: screenElement?.clientHeight ?? 0,
1122
+ screenClientWidth: screenElement?.clientWidth ?? 0,
1123
+ screenDisplay: screenStyle?.display ?? "",
1124
+ screenOpacity: screenStyle?.opacity ?? "",
1125
+ screenVisibility: screenStyle?.visibility ?? "",
1126
+ serializedBytes: serializeAddon.serialize().length,
1127
+ xtermClientHeight: xtermElement?.clientHeight ?? 0,
1128
+ xtermClientWidth: xtermElement?.clientWidth ?? 0,
1129
+ xtermDisplay: xtermStyle?.display ?? "",
1130
+ xtermOpacity: xtermStyle?.opacity ?? "",
1131
+ xtermVisibility: xtermStyle?.visibility ?? ""
1132
+ });
1133
+ }
1134
+ }
1135
+ function requestTerminalFrame(callback) {
1136
+ if (typeof requestAnimationFrame === "function") {
1137
+ return requestAnimationFrame(callback);
1138
+ }
1139
+ return window.setTimeout(callback, 0);
1140
+ }
1141
+ function resolveOutputSyncPlan(plan) {
1142
+ if (!plan) {
1143
+ return "skip";
1144
+ }
1145
+ if (plan.reset && plan.write) {
1146
+ return "replace";
1147
+ }
1148
+ if (plan.reset) {
1149
+ return "reset";
1150
+ }
1151
+ return "append";
1152
+ }
1153
+ function resolveXtermRenderDimensions(terminal) {
1154
+ const core = Reflect.get(terminal, "_core");
1155
+ if (core == null || typeof core !== "object") {
1156
+ return void 0;
1157
+ }
1158
+ const renderService = Reflect.get(core, "_renderService");
1159
+ if (renderService == null || typeof renderService !== "object") {
1160
+ return void 0;
1161
+ }
1162
+ return Reflect.get(renderService, "dimensions");
1163
+ }
1164
+ function previewTerminalLine(value, maxChars = 80) {
1165
+ return value.slice(0, maxChars).replaceAll("\\", "\\\\").replaceAll("\r", "\\r").replaceAll("\n", "\\n");
1166
+ }
1167
+ function resolveXtermTheme(theme) {
1168
+ return {
1169
+ background: theme.background,
1170
+ cursor: theme.cursor,
1171
+ foreground: theme.foreground,
1172
+ selectionBackground: theme.selectionBackground
1173
+ };
1174
+ }
1175
+
1176
+ // src/react/terminalFindShortcut.ts
1177
+ function isTerminalFindShortcut(event) {
1178
+ if (event.altKey) {
1179
+ return false;
1180
+ }
1181
+ if (!(event.metaKey || event.ctrlKey)) {
1182
+ return false;
1183
+ }
1184
+ return event.key.toLowerCase() === "f";
1185
+ }
1186
+
1187
+ // src/react/useTerminalFindController.ts
1188
+ import { useState } from "react";
1189
+ function useTerminalFindController(input) {
1190
+ const [open, setOpen] = useState(false);
1191
+ const [caseSensitive, setCaseSensitive] = useState(false);
1192
+ const [query, setQuery] = useState("");
1193
+ const [regex, setRegex] = useState(false);
1194
+ const resolveFindOptions = () => ({
1195
+ caseSensitive,
1196
+ regex
1197
+ });
1198
+ return {
1199
+ actions: {
1200
+ close() {
1201
+ setOpen(false);
1202
+ },
1203
+ findNext() {
1204
+ input.getRuntime()?.findNext(query, resolveFindOptions());
1205
+ },
1206
+ findPrevious() {
1207
+ input.getRuntime()?.findPrevious(query, resolveFindOptions());
1208
+ },
1209
+ open() {
1210
+ setOpen(true);
1211
+ },
1212
+ onQueryChange(nextQuery) {
1213
+ setQuery(nextQuery);
1214
+ if (!nextQuery) {
1215
+ return;
1216
+ }
1217
+ input.getRuntime()?.findNext(nextQuery, {
1218
+ ...resolveFindOptions(),
1219
+ incremental: true
1220
+ });
1221
+ },
1222
+ onSubmit() {
1223
+ input.getRuntime()?.findNext(query, resolveFindOptions());
1224
+ },
1225
+ toggleCaseSensitive() {
1226
+ setCaseSensitive((enabled) => !enabled);
1227
+ },
1228
+ toggleOpen() {
1229
+ setOpen((enabled) => !enabled);
1230
+ },
1231
+ toggleRegex() {
1232
+ setRegex((enabled) => !enabled);
1233
+ }
1234
+ },
1235
+ state: {
1236
+ caseSensitive,
1237
+ open,
1238
+ query,
1239
+ regex
1240
+ }
1241
+ };
1242
+ }
1243
+
1244
+ // src/react/useTerminalSessionController.ts
1245
+ import { useEffect, useMemo } from "react";
1246
+ import { useExternalStoreSnapshot } from "@tutti-os/ui-react-hooks";
1247
+ function useTerminalSessionController(input) {
1248
+ const controller = useMemo(
1249
+ () => acquireTerminalSessionController({
1250
+ feature: input.feature,
1251
+ nodeId: input.nodeId,
1252
+ sessionId: input.sessionId
1253
+ }),
1254
+ [input.nodeId, input.sessionId]
1255
+ );
1256
+ useEffect(() => {
1257
+ if (input.retainLease === false) {
1258
+ return;
1259
+ }
1260
+ controller.retain();
1261
+ return () => {
1262
+ controller.release();
1263
+ };
1264
+ }, [controller, input.retainLease]);
1265
+ const state = useExternalStoreSnapshot({
1266
+ getSnapshot() {
1267
+ return controller.getState();
1268
+ },
1269
+ subscribe(listener) {
1270
+ return controller.subscribe(listener);
1271
+ }
1272
+ });
1273
+ return {
1274
+ controller,
1275
+ state
1276
+ };
1277
+ }
1278
+
1279
+ // src/react/TerminalSurface.tsx
1280
+ import { jsx, jsxs } from "react/jsx-runtime";
1281
+ function TerminalSurface({
1282
+ controllerLeaseRetainedExternally = false,
1283
+ externalState,
1284
+ feature,
1285
+ nodeId,
1286
+ sessionId,
1287
+ status
1288
+ }) {
1289
+ const containerRef = useRef(null);
1290
+ const currentCwdRef = useRef(externalState?.cwd ?? null);
1291
+ const featureRef = useRef(feature);
1292
+ const findInputRef = useRef(null);
1293
+ const runtimeRef = useRef(null);
1294
+ const [interactionError, setInteractionError] = useState2(null);
1295
+ const { controller, state } = useTerminalSessionController({
1296
+ feature,
1297
+ nodeId,
1298
+ retainLease: !controllerLeaseRetainedExternally,
1299
+ sessionId
1300
+ });
1301
+ const find = useTerminalFindController({
1302
+ getRuntime: () => runtimeRef.current
1303
+ });
1304
+ const theme = useMemo2(
1305
+ () => feature.resolveTheme({
1306
+ runtimeKind: externalState?.runtimeKind ?? "local",
1307
+ sessionId,
1308
+ status
1309
+ }),
1310
+ [externalState?.runtimeKind, feature, sessionId, status]
1311
+ );
1312
+ const surfaceDiagnostics = useMemo2(
1313
+ () => createTerminalSurfaceDiagnostics({
1314
+ diagnostics: feature.diagnostics,
1315
+ nodeId,
1316
+ sessionId
1317
+ }),
1318
+ [feature.diagnostics, nodeId, sessionId]
1319
+ );
1320
+ const surfaceError = interactionError ?? state.surfaceError;
1321
+ useEffect2(() => {
1322
+ currentCwdRef.current = externalState?.cwd ?? null;
1323
+ }, [externalState?.cwd]);
1324
+ useEffect2(() => {
1325
+ featureRef.current = feature;
1326
+ }, [feature]);
1327
+ useEffect2(() => {
1328
+ if (!find.state.open) {
1329
+ return;
1330
+ }
1331
+ const input = findInputRef.current;
1332
+ if (!input) {
1333
+ return;
1334
+ }
1335
+ input.focus();
1336
+ input.select();
1337
+ }, [find.state.open]);
1338
+ useEffect2(() => {
1339
+ const container = containerRef.current;
1340
+ if (!container) {
1341
+ return;
1342
+ }
1343
+ surfaceDiagnostics.mount();
1344
+ setInteractionError(null);
1345
+ const runtime = createTerminalSurfaceRuntime({
1346
+ container,
1347
+ diagnostics: surfaceDiagnostics,
1348
+ feature: featureRef.current,
1349
+ getCwd: () => currentCwdRef.current,
1350
+ nodeId,
1351
+ onResize: ({ cols, rows }) => {
1352
+ surfaceDiagnostics.resize({ cols, rows });
1353
+ void controller.resize({ cols, rows });
1354
+ },
1355
+ onUserInput: (data, encoding) => {
1356
+ controller.write(data, encoding);
1357
+ },
1358
+ sessionId,
1359
+ theme
1360
+ });
1361
+ runtimeRef.current = runtime;
1362
+ runtime.syncOutput(state.rawOutput, state.contentEpoch);
1363
+ runtime.focus();
1364
+ return () => {
1365
+ surfaceDiagnostics.dispose();
1366
+ runtime.dispose();
1367
+ if (runtimeRef.current === runtime) {
1368
+ runtimeRef.current = null;
1369
+ }
1370
+ };
1371
+ }, [controller, nodeId, sessionId, surfaceDiagnostics]);
1372
+ useEffect2(() => {
1373
+ runtimeRef.current?.setTheme(theme);
1374
+ }, [theme]);
1375
+ useEffect2(() => {
1376
+ runtimeRef.current?.syncOutput(state.rawOutput, state.contentEpoch);
1377
+ }, [state.contentEpoch, state.rawOutput]);
1378
+ const writeInput = (data) => {
1379
+ controller.write(data);
1380
+ };
1381
+ return /* @__PURE__ */ jsxs(
1382
+ "div",
1383
+ {
1384
+ className: "workspace-terminal__surface-shell",
1385
+ onKeyDownCapture: (event) => {
1386
+ if (!isTerminalFindShortcut(event)) {
1387
+ if (event.key === "Escape" && find.state.open) {
1388
+ find.actions.close();
1389
+ runtimeRef.current?.focus();
1390
+ }
1391
+ return;
1392
+ }
1393
+ event.preventDefault();
1394
+ event.stopPropagation();
1395
+ find.actions.open();
1396
+ },
1397
+ onDragOver: (event) => {
1398
+ if (feature.dropInput) {
1399
+ event.preventDefault();
1400
+ }
1401
+ },
1402
+ onDrop: (event) => {
1403
+ if (!feature.dropInput) {
1404
+ return;
1405
+ }
1406
+ event.preventDefault();
1407
+ void Promise.resolve(
1408
+ feature.dropInput({
1409
+ cwd: externalState?.cwd ?? null,
1410
+ dataTransfer: event.dataTransfer,
1411
+ sessionId
1412
+ })
1413
+ ).then((input) => {
1414
+ if (input) {
1415
+ writeInput(input);
1416
+ }
1417
+ }).catch((error) => setInteractionError(errorMessage3(error)));
1418
+ },
1419
+ children: [
1420
+ find.state.open ? /* @__PURE__ */ jsxs(
1421
+ "form",
1422
+ {
1423
+ className: "workspace-terminal__find",
1424
+ onSubmit: (event) => {
1425
+ event.preventDefault();
1426
+ find.actions.onSubmit();
1427
+ },
1428
+ children: [
1429
+ /* @__PURE__ */ jsx(
1430
+ "input",
1431
+ {
1432
+ "aria-label": feature.i18n.t("findPlaceholder"),
1433
+ ref: findInputRef,
1434
+ onChange: (event) => {
1435
+ find.actions.onQueryChange(event.currentTarget.value);
1436
+ },
1437
+ placeholder: feature.i18n.t("findPlaceholder"),
1438
+ type: "search",
1439
+ value: find.state.query
1440
+ }
1441
+ ),
1442
+ /* @__PURE__ */ jsx(
1443
+ "button",
1444
+ {
1445
+ className: "workspace-terminal__find-toggle",
1446
+ "aria-label": feature.i18n.t("actions.caseSensitive"),
1447
+ "aria-pressed": find.state.caseSensitive,
1448
+ onClick: () => find.actions.toggleCaseSensitive(),
1449
+ type: "button",
1450
+ children: "Aa"
1451
+ }
1452
+ ),
1453
+ /* @__PURE__ */ jsx(
1454
+ "button",
1455
+ {
1456
+ className: "workspace-terminal__find-toggle",
1457
+ "aria-label": feature.i18n.t("actions.regex"),
1458
+ "aria-pressed": find.state.regex,
1459
+ onClick: () => find.actions.toggleRegex(),
1460
+ type: "button",
1461
+ children: ".*"
1462
+ }
1463
+ ),
1464
+ /* @__PURE__ */ jsx(
1465
+ "button",
1466
+ {
1467
+ className: "workspace-terminal__find-nav workspace-terminal__find-nav--previous",
1468
+ "aria-label": feature.i18n.t("actions.previous"),
1469
+ onClick: () => find.actions.findPrevious(),
1470
+ type: "button"
1471
+ }
1472
+ ),
1473
+ /* @__PURE__ */ jsx(
1474
+ "button",
1475
+ {
1476
+ className: "workspace-terminal__find-nav workspace-terminal__find-nav--next",
1477
+ "aria-label": feature.i18n.t("actions.next"),
1478
+ onClick: () => find.actions.findNext(),
1479
+ type: "submit"
1480
+ }
1481
+ )
1482
+ ]
1483
+ }
1484
+ ) : null,
1485
+ /* @__PURE__ */ jsx(
1486
+ "div",
1487
+ {
1488
+ className: "workspace-terminal__xterm",
1489
+ "data-terminal-xterm": "",
1490
+ ref: containerRef
1491
+ }
1492
+ ),
1493
+ surfaceError ? /* @__PURE__ */ jsx("div", { className: "workspace-terminal__surface-error", role: "status", children: surfaceError }) : null
1494
+ ]
1495
+ }
1496
+ );
1497
+ }
1498
+ function errorMessage3(error) {
1499
+ if (error instanceof Error) {
1500
+ return error.message;
1501
+ }
1502
+ return String(error);
1503
+ }
1504
+
1505
+ // src/react/TerminalNode.tsx
1506
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1507
+ function TerminalNode({
1508
+ children,
1509
+ controllerLeaseRetainedExternally = false,
1510
+ externalState = null,
1511
+ feature,
1512
+ headerAccessory,
1513
+ nodeId,
1514
+ onFocusRequest,
1515
+ sessionId,
1516
+ showHeader = true
1517
+ }) {
1518
+ const resolvedSessionId = sessionId ?? externalState?.sessionId ?? null;
1519
+ const status = externalState?.status ?? "created";
1520
+ return /* @__PURE__ */ jsxs2(
1521
+ "section",
1522
+ {
1523
+ className: "workspace-terminal",
1524
+ "data-terminal-node": nodeId,
1525
+ "data-terminal-session-id": resolvedSessionId ?? void 0,
1526
+ onClick: onFocusRequest,
1527
+ children: [
1528
+ showHeader ? /* @__PURE__ */ jsx2(
1529
+ TerminalNodeHeader,
1530
+ {
1531
+ externalState,
1532
+ feature,
1533
+ headerAccessory,
1534
+ sessionId: resolvedSessionId
1535
+ }
1536
+ ) : null,
1537
+ /* @__PURE__ */ jsx2("div", { className: "workspace-terminal__body", children: children ?? (resolvedSessionId ? /* @__PURE__ */ jsx2(
1538
+ TerminalSurface,
1539
+ {
1540
+ controllerLeaseRetainedExternally,
1541
+ externalState,
1542
+ feature,
1543
+ nodeId,
1544
+ sessionId: resolvedSessionId,
1545
+ status
1546
+ }
1547
+ ) : /* @__PURE__ */ jsx2("div", { className: "workspace-terminal__placeholder", children: feature.i18n.t("emptySession") })) })
1548
+ ]
1549
+ }
1550
+ );
1551
+ }
1552
+ function TerminalNodeHeader({
1553
+ className,
1554
+ defaultActions,
1555
+ externalState = null,
1556
+ feature,
1557
+ headerAccessory,
1558
+ onCloseRequest,
1559
+ sessionId,
1560
+ ...headerProps
1561
+ }) {
1562
+ const status = externalState?.status ?? "created";
1563
+ const statusLabel = resolveTerminalStatusLabel(feature, status);
1564
+ const resolvedSessionId = sessionId ?? externalState?.sessionId ?? null;
1565
+ const hasDefaultActions = hasTerminalHeaderDefaultActions(defaultActions);
1566
+ const [pendingCloseGuard, setPendingCloseGuard] = useState3(null);
1567
+ const [closeError, setCloseError] = useState3(null);
1568
+ const closeDiagnostics = resolvedSessionId ? createTerminalCloseDiagnostics({
1569
+ diagnostics: feature.diagnostics,
1570
+ sessionId: resolvedSessionId
1571
+ }) : null;
1572
+ const closeTerminal = async (skipConfirmation) => {
1573
+ if (!resolvedSessionId || isTerminalSessionEndedStatus(externalState?.status ?? "created")) {
1574
+ onCloseRequest?.();
1575
+ return;
1576
+ }
1577
+ setCloseError(null);
1578
+ try {
1579
+ if (!skipConfirmation) {
1580
+ closeDiagnostics?.requested();
1581
+ const guard = await feature.closeGuard.check({
1582
+ sessionId: resolvedSessionId
1583
+ });
1584
+ if (guard.requiresConfirmation) {
1585
+ setPendingCloseGuard(guard);
1586
+ return;
1587
+ }
1588
+ }
1589
+ await feature.launchService.terminate({ sessionId: resolvedSessionId });
1590
+ closeDiagnostics?.confirmed();
1591
+ setPendingCloseGuard(null);
1592
+ onCloseRequest?.();
1593
+ } catch (error) {
1594
+ setCloseError(errorMessage4(error));
1595
+ }
1596
+ };
1597
+ return /* @__PURE__ */ jsxs2(
1598
+ "header",
1599
+ {
1600
+ ...headerProps,
1601
+ className: joinClassName("workspace-terminal__header", className),
1602
+ children: [
1603
+ /* @__PURE__ */ jsxs2("div", { className: "workspace-terminal__header-main", children: [
1604
+ /* @__PURE__ */ jsx2("span", { className: "workspace-terminal__title", children: externalState?.title?.trim() || feature.i18n.t("title") }),
1605
+ /* @__PURE__ */ jsxs2(Badge, { className: "workspace-terminal__status-tag", variant: "default", children: [
1606
+ /* @__PURE__ */ jsx2(
1607
+ StatusDot,
1608
+ {
1609
+ pulse: status === "running" || status === "starting",
1610
+ size: "xs",
1611
+ tone: resolveTerminalStatusTone(status)
1612
+ }
1613
+ ),
1614
+ statusLabel
1615
+ ] })
1616
+ ] }),
1617
+ headerAccessory?.({
1618
+ externalState,
1619
+ sessionId: resolvedSessionId
1620
+ }),
1621
+ /* @__PURE__ */ jsxs2(
1622
+ "div",
1623
+ {
1624
+ className: "workspace-terminal__actions",
1625
+ onDoubleClick: (event) => {
1626
+ event.stopPropagation();
1627
+ },
1628
+ onPointerDown: (event) => {
1629
+ event.stopPropagation();
1630
+ },
1631
+ children: [
1632
+ defaultActions,
1633
+ onCloseRequest && !hasDefaultActions ? /* @__PURE__ */ jsx2(
1634
+ "button",
1635
+ {
1636
+ "aria-label": feature.i18n.t("actions.close"),
1637
+ className: "workspace-terminal__icon-button",
1638
+ onClick: () => {
1639
+ void closeTerminal(false);
1640
+ },
1641
+ onPointerDown: (event) => {
1642
+ event.stopPropagation();
1643
+ },
1644
+ type: "button",
1645
+ children: "x"
1646
+ }
1647
+ ) : null
1648
+ ]
1649
+ }
1650
+ ),
1651
+ /* @__PURE__ */ jsx2(
1652
+ TerminalCloseGuardDialog,
1653
+ {
1654
+ feature,
1655
+ leaderCommand: pendingCloseGuard?.leaderCommand,
1656
+ onCancel: () => setPendingCloseGuard(null),
1657
+ onConfirm: () => {
1658
+ void closeTerminal(true);
1659
+ },
1660
+ open: Boolean(pendingCloseGuard)
1661
+ }
1662
+ ),
1663
+ closeError ? /* @__PURE__ */ jsx2("div", { className: "workspace-terminal__close-error", role: "status", children: closeError }) : null
1664
+ ]
1665
+ }
1666
+ );
1667
+ }
1668
+ function TerminalCloseGuardDialog({
1669
+ feature,
1670
+ leaderCommand,
1671
+ onCancel,
1672
+ onConfirm,
1673
+ open
1674
+ }) {
1675
+ if (!open) {
1676
+ return null;
1677
+ }
1678
+ return /* @__PURE__ */ jsxs2("div", { className: "workspace-terminal__close-guard", role: "alertdialog", children: [
1679
+ /* @__PURE__ */ jsx2("h2", { children: feature.i18n.t("closeGuard.title") }),
1680
+ /* @__PURE__ */ jsx2("p", { children: feature.i18n.t("closeGuard.description") }),
1681
+ leaderCommand ? /* @__PURE__ */ jsx2("code", { children: leaderCommand }) : null,
1682
+ /* @__PURE__ */ jsxs2("div", { className: "workspace-terminal__close-guard-actions", children: [
1683
+ /* @__PURE__ */ jsx2("button", { onClick: () => onCancel(), type: "button", children: feature.i18n.t("closeGuard.cancel") }),
1684
+ /* @__PURE__ */ jsx2("button", { onClick: () => onConfirm(), type: "button", children: feature.i18n.t("closeGuard.confirm") })
1685
+ ] })
1686
+ ] });
1687
+ }
1688
+ function resolveTerminalStatusLabel(feature, status) {
1689
+ return feature.i18n.t(`status.${status}`);
1690
+ }
1691
+ function resolveTerminalStatusTone(status) {
1692
+ switch (status) {
1693
+ case "running":
1694
+ case "starting":
1695
+ return "blue";
1696
+ case "exited":
1697
+ return "green";
1698
+ case "failed":
1699
+ return "red";
1700
+ case "detached":
1701
+ return "amber";
1702
+ case "created":
1703
+ return "neutral";
1704
+ }
1705
+ }
1706
+ function errorMessage4(error) {
1707
+ if (error instanceof Error) {
1708
+ return error.message;
1709
+ }
1710
+ return String(error);
1711
+ }
1712
+ function joinClassName(...values) {
1713
+ return values.filter(Boolean).join(" ");
1714
+ }
1715
+
1716
+ export {
1717
+ acquireTerminalSessionController,
1718
+ TerminalNode,
1719
+ TerminalNodeHeader,
1720
+ TerminalCloseGuardDialog
1721
+ };
1722
+ //# sourceMappingURL=chunk-FEJT6FJ4.js.map