@vibecook/ghosttea-react 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/TerminalSurface.d.ts.map +1 -1
- package/dist/TerminalSurface.js +31 -4
- package/dist/TerminalSurface.js.map +1 -1
- package/dist/appearance/AppearanceSettings.d.ts +3 -2
- package/dist/appearance/AppearanceSettings.d.ts.map +1 -1
- package/dist/appearance/AppearanceSettings.js +3 -2
- package/dist/appearance/AppearanceSettings.js.map +1 -1
- package/dist/appearance/BackdropBlurControl.d.ts +5 -0
- package/dist/appearance/BackdropBlurControl.d.ts.map +1 -0
- package/dist/appearance/BackdropBlurControl.js +42 -0
- package/dist/appearance/BackdropBlurControl.js.map +1 -0
- package/dist/appearance/types.d.ts +6 -0
- package/dist/appearance/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link-input.d.ts +18 -0
- package/dist/link-input.d.ts.map +1 -0
- package/dist/link-input.js +27 -0
- package/dist/link-input.js.map +1 -0
- package/dist/renderers/background-geometry.d.ts +16 -0
- package/dist/renderers/background-geometry.d.ts.map +1 -0
- package/dist/renderers/background-geometry.js +20 -0
- package/dist/renderers/background-geometry.js.map +1 -0
- package/dist/renderers/types.d.ts +2 -0
- package/dist/renderers/types.d.ts.map +1 -1
- package/dist/renderers/types.js.map +1 -1
- package/dist/renderers/webgpu-renderer.d.ts.map +1 -1
- package/dist/renderers/webgpu-renderer.js +10 -2
- package/dist/renderers/webgpu-renderer.js.map +1 -1
- package/dist/routed-control.d.ts +10 -0
- package/dist/routed-control.d.ts.map +1 -1
- package/dist/routed-control.js +14 -0
- package/dist/routed-control.js.map +1 -1
- package/dist/runtime.d.ts +30 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +244 -54
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +13 -0
- package/dist/terminal-render.worker.js +71 -9
- package/dist/terminal-render.worker.js.map +3 -3
- package/dist/use-terminal-links.d.ts +17 -0
- package/dist/use-terminal-links.d.ts.map +1 -0
- package/dist/use-terminal-links.js +101 -0
- package/dist/use-terminal-links.js.map +1 -0
- package/dist/worker-messages.d.ts +5 -0
- package/dist/worker-messages.d.ts.map +1 -1
- package/dist/workspace/Workspace.d.ts +6 -3
- package/dist/workspace/Workspace.d.ts.map +1 -1
- package/dist/workspace/Workspace.js +18 -9
- package/dist/workspace/Workspace.js.map +1 -1
- package/dist/workspace/index.d.ts +2 -2
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/pane-focus.d.ts +8 -0
- package/dist/workspace/pane-focus.d.ts.map +1 -0
- package/dist/workspace/pane-focus.js +24 -0
- package/dist/workspace/pane-focus.js.map +1 -0
- package/dist/workspace/pane-layout.d.ts +2 -1
- package/dist/workspace/pane-layout.d.ts.map +1 -1
- package/dist/workspace/pane-layout.js +29 -2
- package/dist/workspace/pane-layout.js.map +1 -1
- package/package.json +4 -4
package/dist/runtime.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { terminalLinkUrl } from "@vibecook/ghosttea-protocol";
|
|
1
2
|
import { ControlClient } from "@vibecook/ghosttea";
|
|
2
3
|
import { DEFAULT_ROUTED_PROTOCOL_LIMITS, PROTOCOL_MAJOR, PROTOCOL_MINOR, SESSION_SCROLLBACK_PROTOCOL_MINOR, STRUCTURED_ERROR_PROTOCOL_MINOR, isRoutedSessionAttachGrant, isRoutedTerminalOpenTicket, isValidScrollbackBytes, } from "@vibecook/ghosttea-protocol";
|
|
3
4
|
import { FRAME_MAGIC, FrameFlag } from "@vibecook/ghosttea-frame";
|
|
4
5
|
import { FrameResyncController } from "./frame-resync.js";
|
|
5
6
|
import { initialRoutedActivation, reduceRoutedActivation, } from "./routed-activation.js";
|
|
6
|
-
import { RoutedControlTransport } from "./routed-control.js";
|
|
7
|
+
import { RoutedControlTransport, } from "./routed-control.js";
|
|
7
8
|
const MAX_BROWSER_TIMEOUT_MS = 2_147_483_647;
|
|
8
9
|
function routedConnectionRefusalIsRecoverable(refusal) {
|
|
9
10
|
return refusal.retryable || refusal.code === "GRANT_GENERATION_ROLLBACK" || refusal.code === "GRANT_NONCE_REPLAYED";
|
|
@@ -101,10 +102,16 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
101
102
|
#configSnapshot;
|
|
102
103
|
#configProtocolSupported = false;
|
|
103
104
|
#metadataTimers = new Map();
|
|
105
|
+
#metadataRefreshes = new Map();
|
|
106
|
+
#metadataRefreshPending = new Set();
|
|
107
|
+
#sessionGenerationByHandle = new Map();
|
|
108
|
+
#appliedRoutedExitEvents = new Set();
|
|
109
|
+
#sessionGeneration = 0;
|
|
104
110
|
#resync;
|
|
105
111
|
#performanceRequestId = 1;
|
|
106
112
|
#performanceRequests = new Map();
|
|
107
113
|
#counterRequests = new Map();
|
|
114
|
+
#linksByHandle = new Map();
|
|
108
115
|
#disposed = false;
|
|
109
116
|
constructor(options) {
|
|
110
117
|
super();
|
|
@@ -119,6 +126,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
119
126
|
options.transport === "routed"
|
|
120
127
|
? new RoutedControlTransport({
|
|
121
128
|
...(options.websocketFactory === undefined ? {} : { socketFactory: options.websocketFactory }),
|
|
129
|
+
acceptExtensionMessages: options.host.onExtensionMessage !== undefined,
|
|
122
130
|
emit: (event) => this.#handleRoutedControlEvent(event),
|
|
123
131
|
})
|
|
124
132
|
: undefined;
|
|
@@ -149,6 +157,10 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
149
157
|
else if (data.type === "clipboard-write") {
|
|
150
158
|
this.#platform.writeClipboard(data.text);
|
|
151
159
|
}
|
|
160
|
+
else if (data.type === "link-targets") {
|
|
161
|
+
this.#linksByHandle.set(data.sessionHandle, data.links.filter((link) => terminalLinkUrl(link.uri) !== null));
|
|
162
|
+
this.dispatchEvent(new CustomEvent("link-targets", { detail: { sessionHandle: data.sessionHandle } }));
|
|
163
|
+
}
|
|
152
164
|
else if (data.type === "scrollbar-state") {
|
|
153
165
|
this.#scrollbarByHandle.set(data.sessionHandle, data.scrollbar);
|
|
154
166
|
this.dispatchEvent(new CustomEvent("scrollbar-state", {
|
|
@@ -162,6 +174,8 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
162
174
|
this.#resync.complete(data.sessionHandle);
|
|
163
175
|
}
|
|
164
176
|
else if (data.type === "frame-committed") {
|
|
177
|
+
if (this.#routedHost?.getSession)
|
|
178
|
+
this.#scheduleMetadataRefresh(data.sessionHandle);
|
|
165
179
|
this.#recordCommittedFrame(data.sessionHandle, data.fullSnapshot);
|
|
166
180
|
}
|
|
167
181
|
else if (data.type === "catalog-pressure") {
|
|
@@ -282,22 +296,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
282
296
|
this.#control = new ControlClient(ports.control);
|
|
283
297
|
this.#control.addEventListener("session-exited", (event) => {
|
|
284
298
|
const detail = event.detail;
|
|
285
|
-
|
|
286
|
-
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
287
|
-
if (!handle || !session)
|
|
288
|
-
return;
|
|
289
|
-
this.#cancelMetadataRefresh(handle);
|
|
290
|
-
const exited = {
|
|
291
|
-
...session,
|
|
292
|
-
exited: true,
|
|
293
|
-
exitCode: detail.exitCode,
|
|
294
|
-
exitSignal: detail.exitSignal,
|
|
295
|
-
requestedTermination: detail.requestedTermination,
|
|
296
|
-
exitOutcome: detail.exitOutcome,
|
|
297
|
-
};
|
|
298
|
-
this.#sessionByHandle.set(handle, exited);
|
|
299
|
-
this.dispatchEvent(new CustomEvent("session-metadata", { detail: exited }));
|
|
300
|
-
this.dispatchEvent(new CustomEvent("session-exited", { detail }));
|
|
299
|
+
this.#applySessionExited(detail);
|
|
301
300
|
});
|
|
302
301
|
this.#control.addEventListener("events-lost", () => {
|
|
303
302
|
// The daemon dropped events faster than this client drained them. Any
|
|
@@ -307,14 +306,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
307
306
|
});
|
|
308
307
|
this.#control.addEventListener("session-activity-changed", (event) => {
|
|
309
308
|
const detail = event.detail;
|
|
310
|
-
|
|
311
|
-
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
312
|
-
if (!handle || !session || session.exited)
|
|
313
|
-
return;
|
|
314
|
-
const updated = { ...session, activity: detail.activity };
|
|
315
|
-
this.#sessionByHandle.set(handle, updated);
|
|
316
|
-
this.dispatchEvent(new CustomEvent("session-activity", { detail }));
|
|
317
|
-
this.dispatchEvent(new CustomEvent("session-metadata", { detail: updated }));
|
|
309
|
+
this.#applySessionActivity(detail);
|
|
318
310
|
});
|
|
319
311
|
this.#control.addEventListener("control-changed", (event) => {
|
|
320
312
|
const detail = event.detail;
|
|
@@ -464,54 +456,196 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
464
456
|
if (this.#frameFlowControlEnabled)
|
|
465
457
|
this.#frames?.postMessage({ type: "frame-credit", bytes });
|
|
466
458
|
}
|
|
459
|
+
#bumpSessionGeneration(sessionHandle) {
|
|
460
|
+
this.#sessionGeneration += 1;
|
|
461
|
+
this.#sessionGenerationByHandle.set(sessionHandle, this.#sessionGeneration);
|
|
462
|
+
return this.#sessionGeneration;
|
|
463
|
+
}
|
|
464
|
+
#applySessionExited(detail) {
|
|
465
|
+
const handle = this.#handleBySessionId.get(detail.sessionId);
|
|
466
|
+
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
467
|
+
if (!handle || !session)
|
|
468
|
+
return;
|
|
469
|
+
this.#cancelMetadataRefresh(handle);
|
|
470
|
+
const exited = {
|
|
471
|
+
...session,
|
|
472
|
+
exited: true,
|
|
473
|
+
exitCode: detail.exitCode,
|
|
474
|
+
exitSignal: detail.exitSignal,
|
|
475
|
+
requestedTermination: detail.requestedTermination,
|
|
476
|
+
exitOutcome: detail.exitOutcome,
|
|
477
|
+
};
|
|
478
|
+
this.#sessionByHandle.set(handle, exited);
|
|
479
|
+
this.#bumpSessionGeneration(handle);
|
|
480
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: exited }));
|
|
481
|
+
this.dispatchEvent(new CustomEvent("session-exited", { detail }));
|
|
482
|
+
}
|
|
483
|
+
#applySessionActivity(detail) {
|
|
484
|
+
const handle = this.#handleBySessionId.get(detail.sessionId);
|
|
485
|
+
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
486
|
+
if (!handle || !session || session.exited)
|
|
487
|
+
return;
|
|
488
|
+
const updated = { ...session, activity: detail.activity };
|
|
489
|
+
this.#sessionByHandle.set(handle, updated);
|
|
490
|
+
this.#bumpSessionGeneration(handle);
|
|
491
|
+
this.dispatchEvent(new CustomEvent("session-activity", { detail }));
|
|
492
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: updated }));
|
|
493
|
+
}
|
|
494
|
+
#applyRoutedSessionSummary(sessionHandle, next, expectedGeneration) {
|
|
495
|
+
const previous = this.#sessionByHandle.get(sessionHandle);
|
|
496
|
+
if (!previous ||
|
|
497
|
+
previous.exited ||
|
|
498
|
+
previous.id !== next.id ||
|
|
499
|
+
next.handle !== sessionHandle ||
|
|
500
|
+
this.#handleBySessionId.get(next.id) !== sessionHandle ||
|
|
501
|
+
(expectedGeneration !== undefined && this.#sessionGenerationByHandle.get(sessionHandle) !== expectedGeneration)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
this.#sessionByHandle.set(sessionHandle, next);
|
|
505
|
+
this.#bumpSessionGeneration(sessionHandle);
|
|
506
|
+
if (previous.title !== next.title ||
|
|
507
|
+
previous.cwd !== next.cwd ||
|
|
508
|
+
previous.exited !== next.exited ||
|
|
509
|
+
!sameSessionActivity(previous.activity, next.activity)) {
|
|
510
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: next }));
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
467
514
|
#scheduleMetadataRefresh(sessionHandle) {
|
|
468
515
|
const scheduledSession = this.#sessionByHandle.get(sessionHandle);
|
|
469
516
|
if (!scheduledSession || scheduledSession.exited)
|
|
470
517
|
return;
|
|
518
|
+
if (this.#routedHost?.getSession && this.#metadataRefreshes.has(sessionHandle)) {
|
|
519
|
+
this.#metadataRefreshPending.add(sessionHandle);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
471
522
|
if (this.#metadataTimers.has(sessionHandle))
|
|
472
523
|
return;
|
|
473
524
|
const timer = window.setTimeout(() => {
|
|
474
525
|
this.#metadataTimers.delete(sessionHandle);
|
|
475
526
|
const session = this.#sessionByHandle.get(sessionHandle);
|
|
476
|
-
if (!session || session.exited
|
|
527
|
+
if (!session || session.exited)
|
|
477
528
|
return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
previous.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
529
|
+
if (this.#control) {
|
|
530
|
+
void this.#control
|
|
531
|
+
.request({ type: "get-session", sessionId: session.id })
|
|
532
|
+
.then((response) => {
|
|
533
|
+
if (response.type !== "session")
|
|
534
|
+
return;
|
|
535
|
+
const previous = this.#sessionByHandle.get(sessionHandle);
|
|
536
|
+
if (!previous || previous.id !== response.session.id || previous.exited)
|
|
537
|
+
return;
|
|
538
|
+
this.#sessionByHandle.set(sessionHandle, response.session);
|
|
539
|
+
if (previous.title !== response.session.title ||
|
|
540
|
+
previous.cwd !== response.session.cwd ||
|
|
541
|
+
previous.exited !== response.session.exited ||
|
|
542
|
+
!sameSessionActivity(previous.activity, response.session.activity)) {
|
|
543
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: response.session }));
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
.catch((error) => {
|
|
547
|
+
const current = this.#sessionByHandle.get(sessionHandle);
|
|
548
|
+
if (!current || current.exited || this.#disposed)
|
|
549
|
+
return;
|
|
550
|
+
console.warn("[terminal-runtime] session metadata refresh failed", error);
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (this.#routedHost?.getSession)
|
|
555
|
+
this.#startRoutedMetadataRefresh(sessionHandle, session);
|
|
500
556
|
}, 200);
|
|
501
557
|
this.#metadataTimers.set(sessionHandle, timer);
|
|
502
558
|
}
|
|
559
|
+
#startRoutedMetadataRefresh(sessionHandle, session) {
|
|
560
|
+
const host = this.#routedHost;
|
|
561
|
+
const getSession = host?.getSession;
|
|
562
|
+
if (!host || !getSession || this.#disposed)
|
|
563
|
+
return;
|
|
564
|
+
if (this.#metadataRefreshes.has(sessionHandle)) {
|
|
565
|
+
this.#metadataRefreshPending.add(sessionHandle);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const expectedGeneration = this.#sessionGenerationByHandle.get(sessionHandle);
|
|
569
|
+
const refresh = Promise.resolve()
|
|
570
|
+
.then(() => getSession.call(host, session.id))
|
|
571
|
+
.then((next) => {
|
|
572
|
+
if (next === null)
|
|
573
|
+
return;
|
|
574
|
+
this.#applyRoutedSessionSummary(sessionHandle, next, expectedGeneration);
|
|
575
|
+
})
|
|
576
|
+
.catch((error) => {
|
|
577
|
+
const current = this.#sessionByHandle.get(sessionHandle);
|
|
578
|
+
if (!current || current.exited || this.#disposed)
|
|
579
|
+
return;
|
|
580
|
+
console.warn("[terminal-runtime] session metadata refresh failed", error);
|
|
581
|
+
})
|
|
582
|
+
.finally(() => {
|
|
583
|
+
if (this.#metadataRefreshes.get(sessionHandle) !== refresh)
|
|
584
|
+
return;
|
|
585
|
+
this.#metadataRefreshes.delete(sessionHandle);
|
|
586
|
+
if (!this.#metadataRefreshPending.delete(sessionHandle) || this.#disposed)
|
|
587
|
+
return;
|
|
588
|
+
const current = this.#sessionByHandle.get(sessionHandle);
|
|
589
|
+
if (current && !current.exited)
|
|
590
|
+
this.#scheduleMetadataRefresh(sessionHandle);
|
|
591
|
+
});
|
|
592
|
+
this.#metadataRefreshes.set(sessionHandle, refresh);
|
|
593
|
+
}
|
|
503
594
|
#cancelMetadataRefresh(sessionHandle) {
|
|
504
595
|
const timer = this.#metadataTimers.get(sessionHandle);
|
|
505
596
|
if (timer !== undefined)
|
|
506
597
|
window.clearTimeout(timer);
|
|
507
598
|
this.#metadataTimers.delete(sessionHandle);
|
|
599
|
+
this.#metadataRefreshPending.delete(sessionHandle);
|
|
508
600
|
}
|
|
509
601
|
sessionMetadata(sessionHandle) {
|
|
510
602
|
return this.#sessionByHandle.get(sessionHandle);
|
|
511
603
|
}
|
|
512
604
|
registerSession(session) {
|
|
605
|
+
const previous = this.#sessionByHandle.get(session.handle);
|
|
606
|
+
if (!previous || previous.id !== session.id || !previous.exited || !session.exited) {
|
|
607
|
+
this.#appliedRoutedExitEvents.delete(session.handle);
|
|
608
|
+
}
|
|
513
609
|
this.#sessionByHandle.set(session.handle, session);
|
|
514
610
|
this.#handleBySessionId.set(session.id, session.handle);
|
|
611
|
+
this.#bumpSessionGeneration(session.handle);
|
|
612
|
+
}
|
|
613
|
+
applySessionEvent(event) {
|
|
614
|
+
if (this.#disposed)
|
|
615
|
+
return;
|
|
616
|
+
if (event.type === "updated") {
|
|
617
|
+
const handle = this.#handleBySessionId.get(event.session.id);
|
|
618
|
+
if (handle)
|
|
619
|
+
this.#applyRoutedSessionSummary(handle, event.session);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (event.type === "activity-changed") {
|
|
623
|
+
this.#applySessionActivity({
|
|
624
|
+
requestId: 0,
|
|
625
|
+
type: "session-activity-changed",
|
|
626
|
+
sessionId: event.sessionId,
|
|
627
|
+
activity: event.activity,
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (event.type === "exited") {
|
|
632
|
+
const handle = this.#handleBySessionId.get(event.sessionId);
|
|
633
|
+
const current = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
634
|
+
if (!handle || !current || this.#appliedRoutedExitEvents.has(handle))
|
|
635
|
+
return;
|
|
636
|
+
this.#appliedRoutedExitEvents.add(handle);
|
|
637
|
+
this.#applySessionExited({
|
|
638
|
+
requestId: 0,
|
|
639
|
+
type: "session-exited",
|
|
640
|
+
sessionId: event.sessionId,
|
|
641
|
+
exitCode: event.exitCode,
|
|
642
|
+
exitSignal: event.exitSignal,
|
|
643
|
+
requestedTermination: event.requestedTermination,
|
|
644
|
+
exitOutcome: event.exitOutcome,
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.unregisterSession(event.sessionId);
|
|
515
649
|
}
|
|
516
650
|
#queueFrameSubscriptionSync() {
|
|
517
651
|
const frames = this.#frames;
|
|
@@ -1186,6 +1320,18 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
1186
1320
|
#handleRoutedControlEvent(event) {
|
|
1187
1321
|
if (this.#disposed)
|
|
1188
1322
|
return;
|
|
1323
|
+
if (event.type === "extension-message") {
|
|
1324
|
+
const host = this.#routedHost;
|
|
1325
|
+
if (!host?.onExtensionMessage)
|
|
1326
|
+
return;
|
|
1327
|
+
try {
|
|
1328
|
+
host.onExtensionMessage(event.message, event.context);
|
|
1329
|
+
}
|
|
1330
|
+
catch (error) {
|
|
1331
|
+
console.error("[terminal-runtime] routed extension handler failed", error);
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1189
1335
|
if (event.type === "transport-ready") {
|
|
1190
1336
|
const deadline = event.accepted.protocolLimits.activationAttachDeadlineMs;
|
|
1191
1337
|
this.#routedAttachDeadlineByCell.set(event.cellBootId, deadline);
|
|
@@ -1664,6 +1810,29 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
1664
1810
|
this.#routedByActivation.delete(entry.state.activationId);
|
|
1665
1811
|
this.#routedGeometry.delete(sessionId);
|
|
1666
1812
|
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Detach one of our views and let the seat go with it.
|
|
1815
|
+
*
|
|
1816
|
+
* The daemon clears the resize controller when its holder detaches, but a
|
|
1817
|
+
* local session announces control only through the legacy `control-changed`
|
|
1818
|
+
* frame, which cannot say "no controller": the clear never reaches this
|
|
1819
|
+
* client, and a pane that remounts (a split re-parents its surface) would
|
|
1820
|
+
* see its own previous incarnation on the record forever and never claim.
|
|
1821
|
+
* Our own detach is the one clear this client can predict, so the record is
|
|
1822
|
+
* cleared here at the same revision and the session's remaining views get
|
|
1823
|
+
* their one look at the empty seat (§4.2.3).
|
|
1824
|
+
*/
|
|
1825
|
+
#detachView(sessionId, viewId) {
|
|
1826
|
+
this.#control?.notify({ type: "detach-session", sessionId, viewId });
|
|
1827
|
+
const control = this.#controlBySession.get(sessionId);
|
|
1828
|
+
if (!control?.controller || control.controller.viewId !== viewId)
|
|
1829
|
+
return;
|
|
1830
|
+
this.#controlBySession.set(sessionId, { controller: null, revision: control.revision });
|
|
1831
|
+
for (const other of this.#viewIdsForSession(sessionId)) {
|
|
1832
|
+
if (other !== viewId)
|
|
1833
|
+
this.#maybeReclaim(other);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1667
1836
|
#createMountLease(mounted) {
|
|
1668
1837
|
let disposed = false;
|
|
1669
1838
|
return {
|
|
@@ -1689,7 +1858,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
1689
1858
|
if (this.#routedHost)
|
|
1690
1859
|
this.#releaseRoutedView(mounted.sessionId, mounted.viewId);
|
|
1691
1860
|
else
|
|
1692
|
-
this.#
|
|
1861
|
+
this.#detachView(mounted.sessionId, mounted.viewId);
|
|
1693
1862
|
this.#views.delete(mounted.viewId);
|
|
1694
1863
|
this.#focusByView.delete(mounted.viewId);
|
|
1695
1864
|
}
|
|
@@ -2129,6 +2298,16 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2129
2298
|
setEffects(sessionHandle, effects, surfaceId) {
|
|
2130
2299
|
this.#postWorker({ type: "effects", sessionHandle, ...(surfaceId ? { surfaceId } : {}), effects });
|
|
2131
2300
|
}
|
|
2301
|
+
links(sessionHandle) {
|
|
2302
|
+
return this.#platform.openExternal
|
|
2303
|
+
? (this.#linksByHandle.get(sessionHandle) ?? []).filter((link) => link.explicit || this.#configSnapshot?.renderer.linkUrl !== false)
|
|
2304
|
+
: [];
|
|
2305
|
+
}
|
|
2306
|
+
async openLink(uri) {
|
|
2307
|
+
const url = terminalLinkUrl(uri);
|
|
2308
|
+
if (url)
|
|
2309
|
+
await this.#platform.openExternal?.(url);
|
|
2310
|
+
}
|
|
2132
2311
|
setSelection(sessionHandle, selection, surfaceId) {
|
|
2133
2312
|
this.#postWorker({ type: "selection", sessionHandle, ...(surfaceId ? { surfaceId } : {}), selection });
|
|
2134
2313
|
}
|
|
@@ -2229,9 +2408,12 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2229
2408
|
}
|
|
2230
2409
|
return;
|
|
2231
2410
|
}
|
|
2232
|
-
// The legacy protocol has no release verb
|
|
2233
|
-
//
|
|
2234
|
-
|
|
2411
|
+
// The legacy protocol has no release verb, so the daemon keeps this view
|
|
2412
|
+
// seated. Dropping the request is what closes every resize path; the epoch
|
|
2413
|
+
// stays, because it is still the seat's truth and a later explicit claim
|
|
2414
|
+
// resumes on it. Clearing it here would leave the view with the record in
|
|
2415
|
+
// its own name and no epoch: the funnel's one-claim-per-attachment guard
|
|
2416
|
+
// refuses that claim, and a pane hidden by a zoom never resizes again.
|
|
2235
2417
|
}
|
|
2236
2418
|
setViewInputPolicy(viewId, readWrite) {
|
|
2237
2419
|
const view = this.#views.get(viewId);
|
|
@@ -2340,6 +2522,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2340
2522
|
this.#sessionMountReferences.delete(handle);
|
|
2341
2523
|
const subscriptionChanged = this.#subscribedSessionHandles.delete(handle);
|
|
2342
2524
|
this.#cancelMetadataRefresh(handle);
|
|
2525
|
+
this.#linksByHandle.delete(handle);
|
|
2343
2526
|
this.#mouseTrackingByHandle.delete(handle);
|
|
2344
2527
|
this.#scrollbarByHandle.delete(handle);
|
|
2345
2528
|
for (const mounted of [...this.#mountedEntries]) {
|
|
@@ -2354,7 +2537,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2354
2537
|
this.#postWorker({ type: "unmount", surfaceId: mounted.viewId });
|
|
2355
2538
|
this.#mountGenerationBySurface.delete(mounted.viewId);
|
|
2356
2539
|
if (detachViews)
|
|
2357
|
-
this.#
|
|
2540
|
+
this.#detachView(mounted.sessionId, mounted.viewId);
|
|
2358
2541
|
this.#views.delete(mounted.viewId);
|
|
2359
2542
|
this.#focusByView.delete(mounted.viewId);
|
|
2360
2543
|
}
|
|
@@ -2365,12 +2548,14 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2365
2548
|
if (view.sessionId === sessionId) {
|
|
2366
2549
|
view.pendingInput.length = 0;
|
|
2367
2550
|
if (detachViews)
|
|
2368
|
-
this.#
|
|
2551
|
+
this.#detachView(sessionId, viewId);
|
|
2369
2552
|
this.#views.delete(viewId);
|
|
2370
2553
|
this.#focusByView.delete(viewId);
|
|
2371
2554
|
}
|
|
2372
2555
|
}
|
|
2373
2556
|
this.#sessionByHandle.delete(handle);
|
|
2557
|
+
this.#sessionGenerationByHandle.delete(handle);
|
|
2558
|
+
this.#appliedRoutedExitEvents.delete(handle);
|
|
2374
2559
|
this.#handleBySessionId.delete(sessionId);
|
|
2375
2560
|
this.#remoteSessions.delete(sessionId);
|
|
2376
2561
|
this.#controlBySession.delete(sessionId);
|
|
@@ -2452,6 +2637,10 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2452
2637
|
for (const timer of this.#metadataTimers.values())
|
|
2453
2638
|
window.clearTimeout(timer);
|
|
2454
2639
|
this.#metadataTimers.clear();
|
|
2640
|
+
this.#metadataRefreshPending.clear();
|
|
2641
|
+
this.#metadataRefreshes.clear();
|
|
2642
|
+
this.#sessionGenerationByHandle.clear();
|
|
2643
|
+
this.#appliedRoutedExitEvents.clear();
|
|
2455
2644
|
this.#resync.dispose();
|
|
2456
2645
|
for (const request of this.#performanceRequests.values()) {
|
|
2457
2646
|
window.clearTimeout(request.timer);
|
|
@@ -2469,6 +2658,7 @@ export class GhostteaTerminalRuntime extends EventTarget {
|
|
|
2469
2658
|
this.#recoveredSessions.clear();
|
|
2470
2659
|
this.#focusByView.clear();
|
|
2471
2660
|
this.#mountGenerationBySurface.clear();
|
|
2661
|
+
this.#linksByHandle.clear();
|
|
2472
2662
|
this.#mouseTrackingByHandle.clear();
|
|
2473
2663
|
this.#scrollbarByHandle.clear();
|
|
2474
2664
|
this.#subscribedSessionHandles.clear();
|