@vibecook/ghosttea-react 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/TerminalSurface.d.ts +28 -0
- package/dist/TerminalSurface.d.ts.map +1 -0
- package/dist/TerminalSurface.js +614 -0
- package/dist/TerminalSurface.js.map +1 -0
- package/dist/bindings/action-route.d.ts +155 -0
- package/dist/bindings/action-route.d.ts.map +1 -0
- package/dist/bindings/action-route.js +196 -0
- package/dist/bindings/action-route.js.map +1 -0
- package/dist/bindings/fixtures/extensions.json +10 -0
- package/dist/bindings/fixtures/keybinds-linux-default.json +505 -0
- package/dist/bindings/fixtures/keybinds-macos-default.json +660 -0
- package/dist/bindings/ghostty-actions.d.ts +247 -0
- package/dist/bindings/ghostty-actions.d.ts.map +1 -0
- package/dist/bindings/ghostty-actions.js +431 -0
- package/dist/bindings/ghostty-actions.js.map +1 -0
- package/dist/bindings/ghostty-bindings.d.ts +88 -0
- package/dist/bindings/ghostty-bindings.d.ts.map +1 -0
- package/dist/bindings/ghostty-bindings.js +122 -0
- package/dist/bindings/ghostty-bindings.js.map +1 -0
- package/dist/bindings/ghostty-triggers.d.ts +47 -0
- package/dist/bindings/ghostty-triggers.d.ts.map +1 -0
- package/dist/bindings/ghostty-triggers.js +378 -0
- package/dist/bindings/ghostty-triggers.js.map +1 -0
- package/dist/bindings/index.d.ts +5 -0
- package/dist/bindings/index.d.ts.map +1 -0
- package/dist/bindings/index.js +5 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/catalog-budget.d.ts +22 -0
- package/dist/catalog-budget.d.ts.map +1 -0
- package/dist/catalog-budget.js +53 -0
- package/dist/catalog-budget.js.map +1 -0
- package/dist/cell-width.d.ts +3 -0
- package/dist/cell-width.d.ts.map +1 -0
- package/dist/cell-width.js +19 -0
- package/dist/cell-width.js.map +1 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +13 -0
- package/dist/context.js.map +1 -0
- package/dist/cursor-invalidation.d.ts +3 -0
- package/dist/cursor-invalidation.d.ts.map +1 -0
- package/dist/cursor-invalidation.js +4 -0
- package/dist/cursor-invalidation.js.map +1 -0
- package/dist/frame-resync.d.ts +16 -0
- package/dist/frame-resync.d.ts.map +1 -0
- package/dist/frame-resync.js +164 -0
- package/dist/frame-resync.js.map +1 -0
- package/dist/frame-sequence.d.ts +14 -0
- package/dist/frame-sequence.d.ts.map +1 -0
- package/dist/frame-sequence.js +16 -0
- package/dist/frame-sequence.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/keyboard-input.d.ts +18 -0
- package/dist/keyboard-input.d.ts.map +1 -0
- package/dist/keyboard-input.js +101 -0
- package/dist/keyboard-input.js.map +1 -0
- package/dist/performance.d.ts +66 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +21 -0
- package/dist/performance.js.map +1 -0
- package/dist/renderers/canvas-renderer.d.ts +12 -0
- package/dist/renderers/canvas-renderer.d.ts.map +1 -0
- package/dist/renderers/canvas-renderer.js +117 -0
- package/dist/renderers/canvas-renderer.js.map +1 -0
- package/dist/renderers/render-damage.d.ts +7 -0
- package/dist/renderers/render-damage.d.ts.map +1 -0
- package/dist/renderers/render-damage.js +16 -0
- package/dist/renderers/render-damage.js.map +1 -0
- package/dist/renderers/types.d.ts +65 -0
- package/dist/renderers/types.d.ts.map +1 -0
- package/dist/renderers/types.js +31 -0
- package/dist/renderers/types.js.map +1 -0
- package/dist/renderers/webgpu-renderer.d.ts +27 -0
- package/dist/renderers/webgpu-renderer.d.ts.map +1 -0
- package/dist/renderers/webgpu-renderer.js +1746 -0
- package/dist/renderers/webgpu-renderer.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1021 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scroll-input.d.ts +6 -0
- package/dist/scroll-input.d.ts.map +1 -0
- package/dist/scroll-input.js +14 -0
- package/dist/scroll-input.js.map +1 -0
- package/dist/selection-input.d.ts +16 -0
- package/dist/selection-input.d.ts.map +1 -0
- package/dist/selection-input.js +67 -0
- package/dist/selection-input.js.map +1 -0
- package/dist/selection-text.d.ts +4 -0
- package/dist/selection-text.d.ts.map +1 -0
- package/dist/selection-text.js +28 -0
- package/dist/selection-text.js.map +1 -0
- package/dist/styles.css +75 -0
- package/dist/terminal-bindings.d.ts +22 -0
- package/dist/terminal-bindings.d.ts.map +1 -0
- package/dist/terminal-bindings.js +22 -0
- package/dist/terminal-bindings.js.map +1 -0
- package/dist/terminal-render.worker.d.ts +2 -0
- package/dist/terminal-render.worker.d.ts.map +1 -0
- package/dist/terminal-render.worker.js +3165 -0
- package/dist/terminal-render.worker.js.map +7 -0
- package/dist/worker-messages.d.ts +111 -0
- package/dist/worker-messages.d.ts.map +1 -0
- package/dist/worker-messages.js +2 -0
- package/dist/worker-messages.js.map +1 -0
- package/dist/workspace/RemoteSessionPalette.d.ts +12 -0
- package/dist/workspace/RemoteSessionPalette.d.ts.map +1 -0
- package/dist/workspace/RemoteSessionPalette.js +135 -0
- package/dist/workspace/RemoteSessionPalette.js.map +1 -0
- package/dist/workspace/Workspace.d.ts +69 -0
- package/dist/workspace/Workspace.d.ts.map +1 -0
- package/dist/workspace/Workspace.js +536 -0
- package/dist/workspace/Workspace.js.map +1 -0
- package/dist/workspace/hotkeys.d.ts +27 -0
- package/dist/workspace/hotkeys.d.ts.map +1 -0
- package/dist/workspace/hotkeys.js +29 -0
- package/dist/workspace/hotkeys.js.map +1 -0
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +8 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/pane-layout.d.ts +35 -0
- package/dist/workspace/pane-layout.d.ts.map +1 -0
- package/dist/workspace/pane-layout.js +139 -0
- package/dist/workspace/pane-layout.js.map +1 -0
- package/dist/workspace/pending-cache.d.ts +5 -0
- package/dist/workspace/pending-cache.d.ts.map +1 -0
- package/dist/workspace/pending-cache.js +22 -0
- package/dist/workspace/pending-cache.js.map +1 -0
- package/dist/workspace/session-scope.d.ts +3 -0
- package/dist/workspace/session-scope.d.ts.map +1 -0
- package/dist/workspace/session-scope.js +6 -0
- package/dist/workspace/session-scope.js.map +1 -0
- package/dist/workspace/themes.d.ts +18 -0
- package/dist/workspace/themes.d.ts.map +1 -0
- package/dist/workspace/themes.js +19 -0
- package/dist/workspace/themes.js.map +1 -0
- package/dist/workspace/workspace-model.d.ts +59 -0
- package/dist/workspace/workspace-model.d.ts.map +1 -0
- package/dist/workspace/workspace-model.js +257 -0
- package/dist/workspace/workspace-model.js.map +1 -0
- package/dist/workspace/workspace-tabs.d.ts +41 -0
- package/dist/workspace/workspace-tabs.d.ts.map +1 -0
- package/dist/workspace/workspace-tabs.js +133 -0
- package/dist/workspace/workspace-tabs.js.map +1 -0
- package/dist/workspace.css +453 -0
- package/package.json +75 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import { ControlClient } from "@vibecook/ghosttea";
|
|
2
|
+
import { PROTOCOL_MAJOR, PROTOCOL_MINOR, } from "@vibecook/ghosttea-protocol";
|
|
3
|
+
import { FRAME_MAGIC, FrameFlag } from "@vibecook/ghosttea-frame";
|
|
4
|
+
import { FrameResyncController } from "./frame-resync.js";
|
|
5
|
+
function sameSessionActivity(left, right) {
|
|
6
|
+
return (left.kind === right.kind &&
|
|
7
|
+
left.source === right.source &&
|
|
8
|
+
left.confidence === right.confidence &&
|
|
9
|
+
left.rootProcessGroupId === right.rootProcessGroupId &&
|
|
10
|
+
left.foregroundProcessGroupId === right.foregroundProcessGroupId &&
|
|
11
|
+
left.observedAtMs === right.observedAtMs);
|
|
12
|
+
}
|
|
13
|
+
const FRAME_SUBSCRIPTION_ACK_TIMEOUT_MS = 10_000;
|
|
14
|
+
const FRAME_SUBSCRIPTION_ACK_PROTOCOL_MINOR = 7;
|
|
15
|
+
const FRAME_BRIDGE_CAPABILITY_VERSION = 1;
|
|
16
|
+
const DEFAULT_FRAME_SUBSCRIPTION_GRACE_MS = 1_000;
|
|
17
|
+
export function waitForGhostteaRendererPorts(timeoutMs = 10_000) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timeout = window.setTimeout(() => {
|
|
20
|
+
window.removeEventListener("message", listener);
|
|
21
|
+
reject(new Error("Electron did not transfer the terminal control and frame ports"));
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
const listener = (event) => {
|
|
24
|
+
if (event.data?.type !== "ghosttea:ports" || event.ports.length !== 2)
|
|
25
|
+
return;
|
|
26
|
+
window.clearTimeout(timeout);
|
|
27
|
+
window.removeEventListener("message", listener);
|
|
28
|
+
resolve({ control: event.ports[0], frames: event.ports[1] });
|
|
29
|
+
};
|
|
30
|
+
window.addEventListener("message", listener);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export class GhostteaTerminalRuntime extends EventTarget {
|
|
34
|
+
#worker;
|
|
35
|
+
#ports;
|
|
36
|
+
#platform;
|
|
37
|
+
#clientBuild;
|
|
38
|
+
#sessionOwnerId;
|
|
39
|
+
#frameSubscriptionGraceMs;
|
|
40
|
+
#control;
|
|
41
|
+
#frames;
|
|
42
|
+
#ready;
|
|
43
|
+
#sessionByHandle = new Map();
|
|
44
|
+
#handleBySessionId = new Map();
|
|
45
|
+
#subscribedSessionHandles = new Set();
|
|
46
|
+
#pinnedSessionHandles = new Set();
|
|
47
|
+
#sessionMountReferences = new Map();
|
|
48
|
+
#sessionReleaseTimers = new Map();
|
|
49
|
+
#frameSubscriptionVersion = 1;
|
|
50
|
+
#frameSubscriptionQueuedVersion = 0;
|
|
51
|
+
#frameSubscriptionRequestId = 1;
|
|
52
|
+
#frameSubscriptionReady = Promise.resolve();
|
|
53
|
+
#frameSubscriptionRequests = new Map();
|
|
54
|
+
#frameBridgeCapabilityProbeRequestId;
|
|
55
|
+
#frameSubscriptionAcksSupported = false;
|
|
56
|
+
#frameFlowControlEnabled = false;
|
|
57
|
+
#mountedCanvases = new WeakMap();
|
|
58
|
+
#mountedEntries = new Set();
|
|
59
|
+
#mountGenerationBySurface = new Map();
|
|
60
|
+
#mouseTrackingByHandle = new Map();
|
|
61
|
+
#scrollbarByHandle = new Map();
|
|
62
|
+
#focusByView = new Map();
|
|
63
|
+
#views = new Map();
|
|
64
|
+
#rendererBackend = "starting";
|
|
65
|
+
#metadataTimers = new Map();
|
|
66
|
+
#resync;
|
|
67
|
+
#performanceRequestId = 1;
|
|
68
|
+
#performanceRequests = new Map();
|
|
69
|
+
#disposed = false;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
super();
|
|
72
|
+
this.#worker =
|
|
73
|
+
options.workerFactory?.() ??
|
|
74
|
+
new Worker(new URL("./terminal-render.worker.js", import.meta.url), { type: "module" });
|
|
75
|
+
this.#ports = Promise.resolve(options.ports);
|
|
76
|
+
this.#platform = options.platform;
|
|
77
|
+
this.#clientBuild = options.clientBuild ?? "ghosttea-react";
|
|
78
|
+
this.#sessionOwnerId = options.sessionOwnerId;
|
|
79
|
+
this.#frameSubscriptionGraceMs = Math.max(0, options.frameSubscriptionGraceMs ?? DEFAULT_FRAME_SUBSCRIPTION_GRACE_MS);
|
|
80
|
+
this.#resync = new FrameResyncController((sessionHandle) => this.#refreshSession(sessionHandle), {
|
|
81
|
+
onExhausted: (sessionHandle, error) => {
|
|
82
|
+
console.error(`[terminal-runtime] frame resynchronization exhausted for ${sessionHandle}`, error);
|
|
83
|
+
this.dispatchEvent(new CustomEvent("frame-resync-failed", { detail: { sessionHandle, error } }));
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
this.#worker.addEventListener("error", (event) => {
|
|
87
|
+
console.error(`[terminal-runtime] render worker failed to start: ${event.message || "unknown worker error"}`);
|
|
88
|
+
this.dispatchEvent(new CustomEvent("renderer-error", { detail: event }));
|
|
89
|
+
});
|
|
90
|
+
this.#worker.addEventListener("messageerror", (event) => {
|
|
91
|
+
console.error("[terminal-runtime] render worker rejected a message", event);
|
|
92
|
+
this.dispatchEvent(new CustomEvent("renderer-error", { detail: event }));
|
|
93
|
+
});
|
|
94
|
+
this.#worker.addEventListener("message", ({ data }) => {
|
|
95
|
+
if (data.type === "renderer-status") {
|
|
96
|
+
this.#rendererBackend = data.backend;
|
|
97
|
+
console.info(`[terminal-runtime] renderer backend: ${data.backend}${data.textEngine ? ` + ${data.textEngine} text` : ""}${data.recovered ? " (recovered)" : ""}`);
|
|
98
|
+
this.dispatchEvent(new CustomEvent("renderer-status", { detail: data }));
|
|
99
|
+
}
|
|
100
|
+
else if (data.type === "clipboard-write") {
|
|
101
|
+
this.#platform.writeClipboard(data.text);
|
|
102
|
+
}
|
|
103
|
+
else if (data.type === "scrollbar-state") {
|
|
104
|
+
this.#scrollbarByHandle.set(data.sessionHandle, data.scrollbar);
|
|
105
|
+
this.dispatchEvent(new CustomEvent("scrollbar-state", {
|
|
106
|
+
detail: { sessionHandle: data.sessionHandle, scrollbar: data.scrollbar },
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
else if (data.type === "frame-resync-needed") {
|
|
110
|
+
this.#resync.request(data.sessionHandle);
|
|
111
|
+
}
|
|
112
|
+
else if (data.type === "frame-resync-complete") {
|
|
113
|
+
this.#resync.complete(data.sessionHandle);
|
|
114
|
+
}
|
|
115
|
+
else if (data.type === "catalog-pressure") {
|
|
116
|
+
console.warn(`[terminal-runtime] native text catalog budget exceeded for ${data.sessionHandle}; using bounded fallback text`);
|
|
117
|
+
this.dispatchEvent(new CustomEvent("catalog-pressure", { detail: data }));
|
|
118
|
+
}
|
|
119
|
+
else if (data.type === "frame-credit" && this.#frameFlowControlEnabled) {
|
|
120
|
+
this.#frames?.postMessage({ type: "frame-credit", bytes: data.bytes });
|
|
121
|
+
}
|
|
122
|
+
else if (data.type === "performance-started") {
|
|
123
|
+
this.#resolvePerformanceRequest(data.requestId, undefined);
|
|
124
|
+
}
|
|
125
|
+
else if (data.type === "performance-result") {
|
|
126
|
+
this.#resolvePerformanceRequest(data.requestId, data.snapshot);
|
|
127
|
+
}
|
|
128
|
+
else if (data.type === "renderer-reload-required") {
|
|
129
|
+
console.error(`[terminal-runtime] renderer requested reload: ${String(data.reason ?? "unknown")}`);
|
|
130
|
+
this.#platform.setForceCanvasFallback(true);
|
|
131
|
+
this.#platform.reload();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.#postWorker({
|
|
135
|
+
type: "renderer-config",
|
|
136
|
+
forceCanvasFallback: this.#platform.forceCanvasFallback(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
#postWorker(message, transfer = []) {
|
|
140
|
+
if (this.#disposed)
|
|
141
|
+
return;
|
|
142
|
+
this.#worker.postMessage(message, transfer);
|
|
143
|
+
}
|
|
144
|
+
get rendererBackend() {
|
|
145
|
+
return this.#rendererBackend;
|
|
146
|
+
}
|
|
147
|
+
#resolvePerformanceRequest(requestId, value) {
|
|
148
|
+
const pending = this.#performanceRequests.get(requestId);
|
|
149
|
+
if (!pending)
|
|
150
|
+
return;
|
|
151
|
+
window.clearTimeout(pending.timer);
|
|
152
|
+
this.#performanceRequests.delete(requestId);
|
|
153
|
+
pending.resolve(value);
|
|
154
|
+
}
|
|
155
|
+
#performanceRequest(send, timeoutMs) {
|
|
156
|
+
if (this.#disposed)
|
|
157
|
+
return Promise.reject(new Error("Terminal runtime is disposed"));
|
|
158
|
+
const requestId = this.#performanceRequestId++;
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const timer = window.setTimeout(() => {
|
|
161
|
+
this.#performanceRequests.delete(requestId);
|
|
162
|
+
reject(new Error(`Terminal render performance request ${requestId} timed out`));
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
this.#performanceRequests.set(requestId, { resolve, reject, timer });
|
|
165
|
+
this.#postWorker(send(requestId));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async startPerformanceMeasurement() {
|
|
169
|
+
await this.#performanceRequest((requestId) => ({ type: "performance-start", requestId }), 10_000);
|
|
170
|
+
}
|
|
171
|
+
async finishPerformanceMeasurement(options = {}) {
|
|
172
|
+
const quietMs = Math.max(0, options.quietMs ?? 250);
|
|
173
|
+
const timeoutMs = Math.max(quietMs + 1_000, options.timeoutMs ?? 15_000);
|
|
174
|
+
const result = await this.#performanceRequest((requestId) => ({ type: "performance-finish", requestId, quietMs, timeoutMs }), timeoutMs + 5_000);
|
|
175
|
+
if (!result)
|
|
176
|
+
throw new Error("Terminal render worker returned no performance snapshot");
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
connect() {
|
|
180
|
+
if (this.#disposed)
|
|
181
|
+
return Promise.reject(new Error("Terminal runtime is disposed"));
|
|
182
|
+
this.#ready ??= this.#connect();
|
|
183
|
+
return this.#ready;
|
|
184
|
+
}
|
|
185
|
+
async #connect() {
|
|
186
|
+
const ports = await this.#ports;
|
|
187
|
+
if (this.#disposed) {
|
|
188
|
+
ports.control.close();
|
|
189
|
+
ports.frames.close();
|
|
190
|
+
throw new Error("Terminal runtime was disposed before its ports arrived");
|
|
191
|
+
}
|
|
192
|
+
console.info("[terminal-runtime] received control and frame ports");
|
|
193
|
+
this.#control = new ControlClient(ports.control);
|
|
194
|
+
this.#control.addEventListener("session-exited", (event) => {
|
|
195
|
+
const detail = event.detail;
|
|
196
|
+
const handle = this.#handleBySessionId.get(detail.sessionId);
|
|
197
|
+
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
198
|
+
if (!handle || !session)
|
|
199
|
+
return;
|
|
200
|
+
this.#cancelMetadataRefresh(handle);
|
|
201
|
+
const exited = {
|
|
202
|
+
...session,
|
|
203
|
+
exited: true,
|
|
204
|
+
exitCode: detail.exitCode,
|
|
205
|
+
exitSignal: detail.exitSignal,
|
|
206
|
+
requestedTermination: detail.requestedTermination,
|
|
207
|
+
exitOutcome: detail.exitOutcome,
|
|
208
|
+
};
|
|
209
|
+
this.#sessionByHandle.set(handle, exited);
|
|
210
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: exited }));
|
|
211
|
+
this.dispatchEvent(new CustomEvent("session-exited", { detail }));
|
|
212
|
+
});
|
|
213
|
+
this.#control.addEventListener("session-activity-changed", (event) => {
|
|
214
|
+
const detail = event.detail;
|
|
215
|
+
const handle = this.#handleBySessionId.get(detail.sessionId);
|
|
216
|
+
const session = handle ? this.#sessionByHandle.get(handle) : undefined;
|
|
217
|
+
if (!handle || !session || session.exited)
|
|
218
|
+
return;
|
|
219
|
+
const updated = { ...session, activity: detail.activity };
|
|
220
|
+
this.#sessionByHandle.set(handle, updated);
|
|
221
|
+
this.dispatchEvent(new CustomEvent("session-activity", { detail }));
|
|
222
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: updated }));
|
|
223
|
+
});
|
|
224
|
+
this.#control.addEventListener("control-changed", (event) => {
|
|
225
|
+
const detail = event.detail;
|
|
226
|
+
for (const [viewId, view] of this.#views) {
|
|
227
|
+
if (view.sessionId !== detail.sessionId)
|
|
228
|
+
continue;
|
|
229
|
+
if (viewId !== detail.controllerViewId) {
|
|
230
|
+
view.controlEpoch = undefined;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
view.controlEpoch = detail.controlEpoch;
|
|
234
|
+
if (view.desiredCols !== undefined &&
|
|
235
|
+
view.desiredRows !== undefined &&
|
|
236
|
+
(view.desiredCols !== detail.cols || view.desiredRows !== detail.rows)) {
|
|
237
|
+
this.#sendResize(viewId, view, view.desiredCols, view.desiredRows);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
this.#frames = ports.frames;
|
|
242
|
+
this.#frames.onmessage = ({ data }) => {
|
|
243
|
+
if (!(data instanceof ArrayBuffer)) {
|
|
244
|
+
this.#handleFrameChannelControl(data);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (data.byteLength < 4)
|
|
248
|
+
return;
|
|
249
|
+
const view = new DataView(data);
|
|
250
|
+
if (view.getUint32(0, true) !== FRAME_MAGIC) {
|
|
251
|
+
try {
|
|
252
|
+
this.#handleFrameChannelControl(JSON.parse(new TextDecoder().decode(data)));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
console.warn("[terminal-runtime] frame channel received an invalid control packet");
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (data.byteLength < 16) {
|
|
260
|
+
this.#returnFrameCredit(data.byteLength);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const sessionHandle = view.getBigUint64(8, true).toString();
|
|
264
|
+
// A frame can already be queued in the bridge when its session is
|
|
265
|
+
// terminated. Do not recreate worker state for a session we deliberately
|
|
266
|
+
// dropped, and do not render sessions owned only by automation clients.
|
|
267
|
+
if (!this.#sessionByHandle.has(sessionHandle) || !this.#subscribedSessionHandles.has(sessionHandle)) {
|
|
268
|
+
this.#returnFrameCredit(data.byteLength);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const tracking = (view.getUint16(6, true) & FrameFlag.MouseTracking) !== 0;
|
|
272
|
+
if (this.#mouseTrackingByHandle.get(sessionHandle) !== tracking) {
|
|
273
|
+
this.#mouseTrackingByHandle.set(sessionHandle, tracking);
|
|
274
|
+
this.dispatchEvent(new CustomEvent("terminal-modes", { detail: { sessionHandle, mouseTracking: tracking } }));
|
|
275
|
+
}
|
|
276
|
+
this.#scheduleMetadataRefresh(sessionHandle);
|
|
277
|
+
this.#postWorker({ type: "frame", packet: data }, [data]);
|
|
278
|
+
};
|
|
279
|
+
this.#frames.start();
|
|
280
|
+
const hello = await this.#control.request({
|
|
281
|
+
type: "hello",
|
|
282
|
+
protocolMajor: PROTOCOL_MAJOR,
|
|
283
|
+
protocolMinor: PROTOCOL_MINOR,
|
|
284
|
+
clientBuild: this.#clientBuild,
|
|
285
|
+
});
|
|
286
|
+
if (hello.type !== "hello" || hello.protocolMajor !== PROTOCOL_MAJOR)
|
|
287
|
+
throw new Error("terminald protocol mismatch");
|
|
288
|
+
this.#frameSubscriptionAcksSupported = hello.protocolMinor >= FRAME_SUBSCRIPTION_ACK_PROTOCOL_MINOR;
|
|
289
|
+
await this.#queueFrameSubscriptionSync();
|
|
290
|
+
console.info("[terminal-runtime] authenticated terminald protocol");
|
|
291
|
+
}
|
|
292
|
+
#handleFrameChannelControl(message) {
|
|
293
|
+
if (!message || typeof message !== "object")
|
|
294
|
+
return false;
|
|
295
|
+
const data = message;
|
|
296
|
+
if (data.type === "subscription-ack" && Number.isSafeInteger(data.requestId) && Number(data.requestId) >= 0) {
|
|
297
|
+
const pending = this.#frameSubscriptionRequests.get(Number(data.requestId));
|
|
298
|
+
if (!pending)
|
|
299
|
+
return true;
|
|
300
|
+
window.clearTimeout(pending.timer);
|
|
301
|
+
this.#frameSubscriptionRequests.delete(Number(data.requestId));
|
|
302
|
+
pending.resolve();
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (data.type === "bridge-capabilities" &&
|
|
306
|
+
data.requestId === this.#frameBridgeCapabilityProbeRequestId &&
|
|
307
|
+
data.protocolVersion === FRAME_BRIDGE_CAPABILITY_VERSION &&
|
|
308
|
+
data.frameCredits === true) {
|
|
309
|
+
this.#frameFlowControlEnabled = true;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (data.type !== "frame-gap")
|
|
313
|
+
return false;
|
|
314
|
+
const reportedHandles = Array.isArray(data.sessionHandles)
|
|
315
|
+
? data.sessionHandles.filter((handle) => typeof handle === "string" &&
|
|
316
|
+
this.#subscribedSessionHandles.has(handle) &&
|
|
317
|
+
this.#sessionByHandle.has(handle))
|
|
318
|
+
: [...this.#subscribedSessionHandles].filter((handle) => this.#sessionByHandle.has(handle));
|
|
319
|
+
const sessionHandles = [...new Set(reportedHandles)];
|
|
320
|
+
sessionHandles.sort((left, right) => Number((this.#sessionMountReferences.get(right) ?? 0) > 0) -
|
|
321
|
+
Number((this.#sessionMountReferences.get(left) ?? 0) > 0));
|
|
322
|
+
if (sessionHandles.length > 0)
|
|
323
|
+
this.#postWorker({ type: "frame-gap", sessionHandles });
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
#returnFrameCredit(bytes) {
|
|
327
|
+
if (this.#frameFlowControlEnabled)
|
|
328
|
+
this.#frames?.postMessage({ type: "frame-credit", bytes });
|
|
329
|
+
}
|
|
330
|
+
#scheduleMetadataRefresh(sessionHandle) {
|
|
331
|
+
const scheduledSession = this.#sessionByHandle.get(sessionHandle);
|
|
332
|
+
if (!scheduledSession || scheduledSession.exited)
|
|
333
|
+
return;
|
|
334
|
+
if (this.#metadataTimers.has(sessionHandle))
|
|
335
|
+
return;
|
|
336
|
+
const timer = window.setTimeout(() => {
|
|
337
|
+
this.#metadataTimers.delete(sessionHandle);
|
|
338
|
+
const session = this.#sessionByHandle.get(sessionHandle);
|
|
339
|
+
if (!session || session.exited || !this.#control)
|
|
340
|
+
return;
|
|
341
|
+
void this.#control
|
|
342
|
+
.request({ type: "get-session", sessionId: session.id })
|
|
343
|
+
.then((response) => {
|
|
344
|
+
if (response.type !== "session")
|
|
345
|
+
return;
|
|
346
|
+
const previous = this.#sessionByHandle.get(sessionHandle);
|
|
347
|
+
if (!previous || previous.id !== response.session.id || previous.exited)
|
|
348
|
+
return;
|
|
349
|
+
this.#sessionByHandle.set(sessionHandle, response.session);
|
|
350
|
+
if (previous.title !== response.session.title ||
|
|
351
|
+
previous.cwd !== response.session.cwd ||
|
|
352
|
+
previous.exited !== response.session.exited ||
|
|
353
|
+
!sameSessionActivity(previous.activity, response.session.activity)) {
|
|
354
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: response.session }));
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch((error) => {
|
|
358
|
+
const current = this.#sessionByHandle.get(sessionHandle);
|
|
359
|
+
if (!current || current.exited || this.#disposed)
|
|
360
|
+
return;
|
|
361
|
+
console.warn("[terminal-runtime] session metadata refresh failed", error);
|
|
362
|
+
});
|
|
363
|
+
}, 200);
|
|
364
|
+
this.#metadataTimers.set(sessionHandle, timer);
|
|
365
|
+
}
|
|
366
|
+
#cancelMetadataRefresh(sessionHandle) {
|
|
367
|
+
const timer = this.#metadataTimers.get(sessionHandle);
|
|
368
|
+
if (timer !== undefined)
|
|
369
|
+
window.clearTimeout(timer);
|
|
370
|
+
this.#metadataTimers.delete(sessionHandle);
|
|
371
|
+
}
|
|
372
|
+
sessionMetadata(sessionHandle) {
|
|
373
|
+
return this.#sessionByHandle.get(sessionHandle);
|
|
374
|
+
}
|
|
375
|
+
registerSession(session) {
|
|
376
|
+
this.#sessionByHandle.set(session.handle, session);
|
|
377
|
+
this.#handleBySessionId.set(session.id, session.handle);
|
|
378
|
+
}
|
|
379
|
+
#queueFrameSubscriptionSync() {
|
|
380
|
+
const frames = this.#frames;
|
|
381
|
+
if (!frames)
|
|
382
|
+
return Promise.resolve();
|
|
383
|
+
if (this.#frameSubscriptionQueuedVersion >= this.#frameSubscriptionVersion) {
|
|
384
|
+
return this.#frameSubscriptionReady;
|
|
385
|
+
}
|
|
386
|
+
const queuedVersion = this.#frameSubscriptionVersion;
|
|
387
|
+
this.#frameSubscriptionQueuedVersion = queuedVersion;
|
|
388
|
+
const requestId = this.#frameSubscriptionRequestId++;
|
|
389
|
+
const sessionHandles = [...this.#subscribedSessionHandles];
|
|
390
|
+
const probesBridge = this.#frameBridgeCapabilityProbeRequestId === undefined;
|
|
391
|
+
if (probesBridge)
|
|
392
|
+
this.#frameBridgeCapabilityProbeRequestId = requestId;
|
|
393
|
+
const subscription = {
|
|
394
|
+
type: "subscribe",
|
|
395
|
+
requestId,
|
|
396
|
+
sessionHandles,
|
|
397
|
+
...(probesBridge ? { bridgeCapabilities: FRAME_BRIDGE_CAPABILITY_VERSION } : {}),
|
|
398
|
+
...(this.#frameFlowControlEnabled ? { frameCredits: true } : {}),
|
|
399
|
+
};
|
|
400
|
+
const previous = this.#frameSubscriptionReady;
|
|
401
|
+
const operation = previous
|
|
402
|
+
.catch(() => undefined)
|
|
403
|
+
.then(() => this.#sendFrameSubscription(frames, subscription, probesBridge));
|
|
404
|
+
this.#frameSubscriptionReady = operation;
|
|
405
|
+
void operation.catch(() => {
|
|
406
|
+
if (this.#frameSubscriptionReady === operation) {
|
|
407
|
+
this.#frameSubscriptionQueuedVersion = Math.min(this.#frameSubscriptionQueuedVersion, queuedVersion - 1);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return operation;
|
|
411
|
+
}
|
|
412
|
+
#sendFrameSubscription(frames, subscription, probesBridge) {
|
|
413
|
+
if (!this.#frameSubscriptionAcksSupported) {
|
|
414
|
+
try {
|
|
415
|
+
frames.postMessage(subscription);
|
|
416
|
+
return Promise.resolve();
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (probesBridge && !this.#frameFlowControlEnabled)
|
|
420
|
+
this.#frameBridgeCapabilityProbeRequestId = undefined;
|
|
421
|
+
return Promise.reject(error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const timer = window.setTimeout(() => {
|
|
426
|
+
if (!this.#frameSubscriptionRequests.delete(subscription.requestId))
|
|
427
|
+
return;
|
|
428
|
+
reject(new Error(`Frame subscription ${subscription.requestId} was not acknowledged`));
|
|
429
|
+
}, FRAME_SUBSCRIPTION_ACK_TIMEOUT_MS);
|
|
430
|
+
this.#frameSubscriptionRequests.set(subscription.requestId, { resolve, reject, timer });
|
|
431
|
+
try {
|
|
432
|
+
frames.postMessage(subscription);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
window.clearTimeout(timer);
|
|
436
|
+
this.#frameSubscriptionRequests.delete(subscription.requestId);
|
|
437
|
+
if (probesBridge && !this.#frameFlowControlEnabled)
|
|
438
|
+
this.#frameBridgeCapabilityProbeRequestId = undefined;
|
|
439
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
#syncFrameSubscriptionsInBackground() {
|
|
444
|
+
void this.connect()
|
|
445
|
+
.then(() => this.#queueFrameSubscriptionSync())
|
|
446
|
+
.catch((error) => {
|
|
447
|
+
if (this.#disposed)
|
|
448
|
+
return;
|
|
449
|
+
console.error("[terminal-runtime] failed to update frame subscriptions", error);
|
|
450
|
+
this.dispatchEvent(new CustomEvent("frame-subscription-error", { detail: { error } }));
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
#retainFrameSubscription(sessionHandle) {
|
|
454
|
+
const releaseTimer = this.#sessionReleaseTimers.get(sessionHandle);
|
|
455
|
+
if (releaseTimer !== undefined) {
|
|
456
|
+
window.clearTimeout(releaseTimer);
|
|
457
|
+
this.#sessionReleaseTimers.delete(sessionHandle);
|
|
458
|
+
}
|
|
459
|
+
this.#sessionMountReferences.set(sessionHandle, (this.#sessionMountReferences.get(sessionHandle) ?? 0) + 1);
|
|
460
|
+
const added = !this.#subscribedSessionHandles.has(sessionHandle);
|
|
461
|
+
if (added) {
|
|
462
|
+
this.#subscribedSessionHandles.add(sessionHandle);
|
|
463
|
+
this.#frameSubscriptionVersion += 1;
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
added,
|
|
467
|
+
ready: this.connect().then(() => this.#queueFrameSubscriptionSync()),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
#expectFullSnapshot(sessionHandle) {
|
|
471
|
+
if (this.#disposed ||
|
|
472
|
+
!this.#subscribedSessionHandles.has(sessionHandle) ||
|
|
473
|
+
!this.#sessionByHandle.has(sessionHandle))
|
|
474
|
+
return;
|
|
475
|
+
this.#postWorker({ type: "expect-full", sessionHandle });
|
|
476
|
+
this.#resync.request(sessionHandle);
|
|
477
|
+
}
|
|
478
|
+
#scheduleFrameSubscriptionRelease(sessionHandle) {
|
|
479
|
+
if ((this.#sessionMountReferences.get(sessionHandle) ?? 0) > 0 ||
|
|
480
|
+
this.#pinnedSessionHandles.has(sessionHandle) ||
|
|
481
|
+
!this.#subscribedSessionHandles.has(sessionHandle) ||
|
|
482
|
+
this.#sessionReleaseTimers.has(sessionHandle))
|
|
483
|
+
return;
|
|
484
|
+
const timer = window.setTimeout(() => {
|
|
485
|
+
this.#sessionReleaseTimers.delete(sessionHandle);
|
|
486
|
+
if ((this.#sessionMountReferences.get(sessionHandle) ?? 0) > 0 || this.#pinnedSessionHandles.has(sessionHandle))
|
|
487
|
+
return;
|
|
488
|
+
if (this.#subscribedSessionHandles.delete(sessionHandle)) {
|
|
489
|
+
this.#frameSubscriptionVersion += 1;
|
|
490
|
+
this.#syncFrameSubscriptionsInBackground();
|
|
491
|
+
}
|
|
492
|
+
this.#resync.cancel(sessionHandle);
|
|
493
|
+
this.#postWorker({ type: "drop-session", sessionHandle });
|
|
494
|
+
}, this.#frameSubscriptionGraceMs);
|
|
495
|
+
this.#sessionReleaseTimers.set(sessionHandle, timer);
|
|
496
|
+
}
|
|
497
|
+
#releaseFrameSubscription(sessionHandle) {
|
|
498
|
+
const references = Math.max(0, (this.#sessionMountReferences.get(sessionHandle) ?? 0) - 1);
|
|
499
|
+
if (references === 0)
|
|
500
|
+
this.#sessionMountReferences.delete(sessionHandle);
|
|
501
|
+
else
|
|
502
|
+
this.#sessionMountReferences.set(sessionHandle, references);
|
|
503
|
+
this.#scheduleFrameSubscriptionRelease(sessionHandle);
|
|
504
|
+
}
|
|
505
|
+
setSessionPinned(sessionHandle, pinned) {
|
|
506
|
+
if (pinned) {
|
|
507
|
+
if (!this.#sessionByHandle.has(sessionHandle)) {
|
|
508
|
+
throw new Error(`Cannot pin unknown terminal session ${sessionHandle}`);
|
|
509
|
+
}
|
|
510
|
+
if (this.#pinnedSessionHandles.has(sessionHandle))
|
|
511
|
+
return;
|
|
512
|
+
this.#pinnedSessionHandles.add(sessionHandle);
|
|
513
|
+
const releaseTimer = this.#sessionReleaseTimers.get(sessionHandle);
|
|
514
|
+
if (releaseTimer !== undefined) {
|
|
515
|
+
window.clearTimeout(releaseTimer);
|
|
516
|
+
this.#sessionReleaseTimers.delete(sessionHandle);
|
|
517
|
+
}
|
|
518
|
+
if (!this.#subscribedSessionHandles.has(sessionHandle)) {
|
|
519
|
+
this.#subscribedSessionHandles.add(sessionHandle);
|
|
520
|
+
this.#frameSubscriptionVersion += 1;
|
|
521
|
+
void this.connect()
|
|
522
|
+
.then(() => this.#queueFrameSubscriptionSync())
|
|
523
|
+
.then(() => this.#expectFullSnapshot(sessionHandle))
|
|
524
|
+
.catch((error) => {
|
|
525
|
+
if (!this.#disposed)
|
|
526
|
+
console.error(`[terminal-runtime] failed to pin frame subscription ${sessionHandle}`, error);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
if (!this.#pinnedSessionHandles.delete(sessionHandle))
|
|
532
|
+
return;
|
|
533
|
+
this.#scheduleFrameSubscriptionRelease(sessionHandle);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async createSession(options) {
|
|
537
|
+
await this.connect();
|
|
538
|
+
const response = await this.#control.request({
|
|
539
|
+
type: "create-session",
|
|
540
|
+
options: { ...options, ...(this.#sessionOwnerId ? { ownerId: this.#sessionOwnerId } : {}) },
|
|
541
|
+
});
|
|
542
|
+
if (response.type !== "session-created")
|
|
543
|
+
throw new Error("terminald returned an unexpected response");
|
|
544
|
+
this.registerSession(response.session);
|
|
545
|
+
console.info(`[terminal-runtime] created session ${response.session.id}`);
|
|
546
|
+
return response.session;
|
|
547
|
+
}
|
|
548
|
+
async listSessions() {
|
|
549
|
+
await this.connect();
|
|
550
|
+
const response = await this.#control.request({ type: "list-sessions" });
|
|
551
|
+
if (response.type !== "sessions")
|
|
552
|
+
throw new Error("terminald returned an unexpected response");
|
|
553
|
+
for (const session of response.sessions) {
|
|
554
|
+
this.registerSession(session);
|
|
555
|
+
}
|
|
556
|
+
return response.sessions;
|
|
557
|
+
}
|
|
558
|
+
async listRemoteHosts() {
|
|
559
|
+
await this.connect();
|
|
560
|
+
const response = await this.#control.request({ type: "list-remote-hosts" });
|
|
561
|
+
if (response.type !== "remote-hosts")
|
|
562
|
+
throw new Error("terminald returned an unexpected response");
|
|
563
|
+
return response.hosts;
|
|
564
|
+
}
|
|
565
|
+
async listRemoteSessions(deviceId) {
|
|
566
|
+
await this.connect();
|
|
567
|
+
const response = await this.#control.request({ type: "list-remote-sessions", deviceId }, 35_000);
|
|
568
|
+
if (response.type !== "remote-sessions" || response.deviceId !== deviceId)
|
|
569
|
+
throw new Error("terminald returned an unexpected response");
|
|
570
|
+
return response.sessions;
|
|
571
|
+
}
|
|
572
|
+
async openRemoteSession(deviceId, remoteSessionId, cols, rows) {
|
|
573
|
+
await this.connect();
|
|
574
|
+
const response = await this.#control.request({
|
|
575
|
+
type: "open-remote-session",
|
|
576
|
+
deviceId,
|
|
577
|
+
remoteSessionId,
|
|
578
|
+
cols,
|
|
579
|
+
rows,
|
|
580
|
+
...(this.#sessionOwnerId ? { ownerId: this.#sessionOwnerId } : {}),
|
|
581
|
+
});
|
|
582
|
+
if (response.type !== "session-created")
|
|
583
|
+
throw new Error("terminald could not open the remote session");
|
|
584
|
+
this.registerSession(response.session);
|
|
585
|
+
return response.session;
|
|
586
|
+
}
|
|
587
|
+
async #refreshSession(sessionHandle) {
|
|
588
|
+
const session = this.#sessionByHandle.get(sessionHandle);
|
|
589
|
+
if (!session)
|
|
590
|
+
return;
|
|
591
|
+
if (!this.#control)
|
|
592
|
+
throw new Error(`Session ${sessionHandle} is not ready for frame resynchronization`);
|
|
593
|
+
const response = await this.#control.request({ type: "refresh-session", sessionId: session.id });
|
|
594
|
+
if (response.type !== "ok")
|
|
595
|
+
throw new Error("terminald rejected frame resynchronization");
|
|
596
|
+
}
|
|
597
|
+
mount(sessionId, sessionHandle, viewId, canvas) {
|
|
598
|
+
if (this.#disposed)
|
|
599
|
+
throw new Error("Cannot mount a disposed terminal runtime");
|
|
600
|
+
const mounted = this.#mountedCanvases.get(canvas);
|
|
601
|
+
if (mounted) {
|
|
602
|
+
if (!mounted.active)
|
|
603
|
+
throw new Error("A released terminal canvas cannot be remounted");
|
|
604
|
+
if (mounted.sessionHandle !== sessionHandle) {
|
|
605
|
+
throw new Error("A terminal canvas cannot be reassigned to another session");
|
|
606
|
+
}
|
|
607
|
+
mounted.references += 1;
|
|
608
|
+
if (mounted.disposeTimer !== undefined) {
|
|
609
|
+
window.clearTimeout(mounted.disposeTimer);
|
|
610
|
+
mounted.disposeTimer = undefined;
|
|
611
|
+
}
|
|
612
|
+
return this.#createMountLease(mounted);
|
|
613
|
+
}
|
|
614
|
+
const offscreen = canvas.transferControlToOffscreen();
|
|
615
|
+
const subscription = this.#retainFrameSubscription(sessionHandle);
|
|
616
|
+
const generation = (this.#mountGenerationBySurface.get(viewId) ?? 0) + 1;
|
|
617
|
+
this.#mountGenerationBySurface.set(viewId, generation);
|
|
618
|
+
this.#postWorker({ type: "mount", surfaceId: viewId, sessionHandle, canvas: offscreen }, [offscreen]);
|
|
619
|
+
const entry = {
|
|
620
|
+
canvas,
|
|
621
|
+
sessionHandle,
|
|
622
|
+
sessionId,
|
|
623
|
+
viewId,
|
|
624
|
+
generation,
|
|
625
|
+
references: 1,
|
|
626
|
+
disposeTimer: undefined,
|
|
627
|
+
active: true,
|
|
628
|
+
};
|
|
629
|
+
this.#mountedCanvases.set(canvas, entry);
|
|
630
|
+
this.#mountedEntries.add(entry);
|
|
631
|
+
const view = {
|
|
632
|
+
sessionId,
|
|
633
|
+
sessionHandle,
|
|
634
|
+
inputSequence: 0,
|
|
635
|
+
resizeSequence: 0,
|
|
636
|
+
controlEpoch: undefined,
|
|
637
|
+
desiredCols: undefined,
|
|
638
|
+
desiredRows: undefined,
|
|
639
|
+
pendingInput: [],
|
|
640
|
+
};
|
|
641
|
+
this.#views.set(viewId, view);
|
|
642
|
+
void subscription.ready
|
|
643
|
+
.then(() => {
|
|
644
|
+
const current = this.#views.get(viewId);
|
|
645
|
+
if (current !== view || !entry.active)
|
|
646
|
+
return undefined;
|
|
647
|
+
if (subscription.added)
|
|
648
|
+
this.#expectFullSnapshot(sessionHandle);
|
|
649
|
+
return this.#control?.request({ type: "attach-session", sessionId, viewId }, 60_000);
|
|
650
|
+
})
|
|
651
|
+
.then((response) => {
|
|
652
|
+
if (!response)
|
|
653
|
+
return;
|
|
654
|
+
if (response.type !== "view-attached" || response.viewId !== viewId) {
|
|
655
|
+
throw new Error("terminald returned an invalid view attachment");
|
|
656
|
+
}
|
|
657
|
+
const current = this.#views.get(viewId);
|
|
658
|
+
if (current !== view)
|
|
659
|
+
return;
|
|
660
|
+
current.attachmentEpoch = response.attachmentEpoch;
|
|
661
|
+
current.readWrite = response.readWrite;
|
|
662
|
+
this.dispatchEvent(new CustomEvent("view-attached", {
|
|
663
|
+
detail: { sessionId, sessionHandle, viewId, readWrite: response.readWrite },
|
|
664
|
+
}));
|
|
665
|
+
const previous = this.#sessionByHandle.get(sessionHandle);
|
|
666
|
+
if (previous && previous.readWrite !== response.readWrite) {
|
|
667
|
+
const updated = { ...previous, readWrite: response.readWrite };
|
|
668
|
+
this.#sessionByHandle.set(sessionHandle, updated);
|
|
669
|
+
this.dispatchEvent(new CustomEvent("session-metadata", { detail: updated }));
|
|
670
|
+
}
|
|
671
|
+
const pending = current.pendingInput.splice(0);
|
|
672
|
+
if (!response.readWrite)
|
|
673
|
+
return;
|
|
674
|
+
for (const operation of pending) {
|
|
675
|
+
current.inputSequence += 1;
|
|
676
|
+
operation(response.attachmentEpoch, current.inputSequence);
|
|
677
|
+
}
|
|
678
|
+
})
|
|
679
|
+
.catch((error) => console.error(`[terminal-runtime] failed to attach view ${viewId}`, error));
|
|
680
|
+
return this.#createMountLease(entry);
|
|
681
|
+
}
|
|
682
|
+
#createMountLease(mounted) {
|
|
683
|
+
let disposed = false;
|
|
684
|
+
return {
|
|
685
|
+
resize: (width, height, dpr) => this.#postWorker({ type: "resize", surfaceId: mounted.viewId, width, height, dpr }),
|
|
686
|
+
dispose: () => {
|
|
687
|
+
if (disposed)
|
|
688
|
+
return;
|
|
689
|
+
disposed = true;
|
|
690
|
+
if (!mounted.active)
|
|
691
|
+
return;
|
|
692
|
+
mounted.references -= 1;
|
|
693
|
+
if (mounted.references !== 0)
|
|
694
|
+
return;
|
|
695
|
+
mounted.disposeTimer = window.setTimeout(() => {
|
|
696
|
+
mounted.disposeTimer = undefined;
|
|
697
|
+
if (mounted.references !== 0 || !mounted.active)
|
|
698
|
+
return;
|
|
699
|
+
mounted.active = false;
|
|
700
|
+
const ownsWorkerSurface = this.#mountGenerationBySurface.get(mounted.viewId) === mounted.generation;
|
|
701
|
+
if (ownsWorkerSurface) {
|
|
702
|
+
this.#postWorker({ type: "unmount", surfaceId: mounted.viewId });
|
|
703
|
+
this.#mountGenerationBySurface.delete(mounted.viewId);
|
|
704
|
+
this.#control?.notify({ type: "detach-session", sessionId: mounted.sessionId, viewId: mounted.viewId });
|
|
705
|
+
this.#views.delete(mounted.viewId);
|
|
706
|
+
this.#focusByView.delete(mounted.viewId);
|
|
707
|
+
}
|
|
708
|
+
this.#mountedCanvases.delete(mounted.canvas);
|
|
709
|
+
this.#mountedEntries.delete(mounted);
|
|
710
|
+
this.#releaseFrameSubscription(mounted.sessionHandle);
|
|
711
|
+
}, 0);
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
#sendViewInput(viewId, operation) {
|
|
716
|
+
const view = this.#views.get(viewId);
|
|
717
|
+
if (!view || view.readWrite === false)
|
|
718
|
+
return;
|
|
719
|
+
if (view.attachmentEpoch === undefined) {
|
|
720
|
+
if (view.pendingInput.length < 256)
|
|
721
|
+
view.pendingInput.push(operation);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
view.inputSequence += 1;
|
|
725
|
+
operation(view.attachmentEpoch, view.inputSequence);
|
|
726
|
+
}
|
|
727
|
+
sendText(sessionId, viewId, text) {
|
|
728
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "send-text", sessionId, viewId, attachmentEpoch, inputSequence, text }));
|
|
729
|
+
const handle = this.#handleBySessionId.get(sessionId);
|
|
730
|
+
if (handle)
|
|
731
|
+
this.#postWorker({ type: "cursor-activity", sessionHandle: handle });
|
|
732
|
+
}
|
|
733
|
+
paste(sessionId, viewId, text) {
|
|
734
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "paste", sessionId, viewId, attachmentEpoch, inputSequence, text }));
|
|
735
|
+
const handle = this.#handleBySessionId.get(sessionId);
|
|
736
|
+
if (handle)
|
|
737
|
+
this.#postWorker({ type: "cursor-activity", sessionHandle: handle });
|
|
738
|
+
}
|
|
739
|
+
sendKey(sessionId, viewId, event) {
|
|
740
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "send-key", sessionId, viewId, attachmentEpoch, inputSequence, event }));
|
|
741
|
+
const handle = this.#handleBySessionId.get(sessionId);
|
|
742
|
+
if (handle)
|
|
743
|
+
this.#postWorker({ type: "cursor-activity", sessionHandle: handle });
|
|
744
|
+
}
|
|
745
|
+
sendMouse(sessionId, viewId, event) {
|
|
746
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "send-mouse", sessionId, viewId, attachmentEpoch, inputSequence, event }));
|
|
747
|
+
}
|
|
748
|
+
scroll(sessionId, viewId, rows) {
|
|
749
|
+
if (rows === 0)
|
|
750
|
+
return;
|
|
751
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "scroll", sessionId, viewId, attachmentEpoch, inputSequence, rows }));
|
|
752
|
+
}
|
|
753
|
+
scrollTo(sessionId, viewId, row) {
|
|
754
|
+
if (!Number.isSafeInteger(row) || row < 0)
|
|
755
|
+
return;
|
|
756
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "scroll-to", sessionId, viewId, attachmentEpoch, inputSequence, row }));
|
|
757
|
+
}
|
|
758
|
+
scrollbar(sessionHandle) {
|
|
759
|
+
return this.#scrollbarByHandle.get(sessionHandle);
|
|
760
|
+
}
|
|
761
|
+
isMouseTracking(sessionHandle) {
|
|
762
|
+
return this.#mouseTrackingByHandle.get(sessionHandle) ?? false;
|
|
763
|
+
}
|
|
764
|
+
setTheme(sessionHandle, theme, surfaceId) {
|
|
765
|
+
this.#postWorker({ type: "theme", sessionHandle, ...(surfaceId ? { surfaceId } : {}), theme });
|
|
766
|
+
const session = this.#sessionByHandle.get(sessionHandle);
|
|
767
|
+
if (!session)
|
|
768
|
+
return;
|
|
769
|
+
const rgb = (color) => [
|
|
770
|
+
Math.round(color[0] * 255),
|
|
771
|
+
Math.round(color[1] * 255),
|
|
772
|
+
Math.round(color[2] * 255),
|
|
773
|
+
];
|
|
774
|
+
this.#control?.notify({
|
|
775
|
+
type: "set-colors",
|
|
776
|
+
sessionId: session.id,
|
|
777
|
+
foreground: rgb(theme.foreground),
|
|
778
|
+
background: rgb(theme.background),
|
|
779
|
+
cursor: rgb(theme.cursor),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
setSelection(sessionHandle, selection, surfaceId) {
|
|
783
|
+
this.#postWorker({ type: "selection", sessionHandle, ...(surfaceId ? { surfaceId } : {}), selection });
|
|
784
|
+
}
|
|
785
|
+
setVisible(sessionHandle, visible, surfaceId) {
|
|
786
|
+
this.#postWorker({ type: "visibility", sessionHandle, ...(surfaceId ? { surfaceId } : {}), visible });
|
|
787
|
+
}
|
|
788
|
+
forceFullRedraw(sessionHandle) {
|
|
789
|
+
this.#postWorker({ type: "force-full-redraw", sessionHandle });
|
|
790
|
+
}
|
|
791
|
+
forceRowRedraw(sessionHandle, row) {
|
|
792
|
+
this.#postWorker({ type: "force-row-redraw", sessionHandle, row });
|
|
793
|
+
}
|
|
794
|
+
setPartialRenderingEnabled(enabled) {
|
|
795
|
+
this.#postWorker({ type: "partial-rendering", enabled });
|
|
796
|
+
}
|
|
797
|
+
claimResizeControl(sessionHandle, viewId, cols, rows) {
|
|
798
|
+
const view = this.#views.get(viewId);
|
|
799
|
+
if (view) {
|
|
800
|
+
view.desiredCols = cols;
|
|
801
|
+
view.desiredRows = rows;
|
|
802
|
+
}
|
|
803
|
+
const session = this.#sessionByHandle.get(sessionHandle);
|
|
804
|
+
if (!session)
|
|
805
|
+
return;
|
|
806
|
+
this.#sendViewInput(viewId, (attachmentEpoch) => {
|
|
807
|
+
this.#control?.notify({
|
|
808
|
+
type: "focus-and-resize",
|
|
809
|
+
sessionId: session.id,
|
|
810
|
+
viewId,
|
|
811
|
+
attachmentEpoch,
|
|
812
|
+
cols,
|
|
813
|
+
rows,
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
setFocused(sessionHandle, viewId, focused, cols, rows) {
|
|
818
|
+
const view = this.#views.get(viewId);
|
|
819
|
+
if (view) {
|
|
820
|
+
view.desiredCols = cols;
|
|
821
|
+
view.desiredRows = rows;
|
|
822
|
+
}
|
|
823
|
+
if (this.#focusByView.get(viewId) === focused)
|
|
824
|
+
return;
|
|
825
|
+
this.#focusByView.set(viewId, focused);
|
|
826
|
+
this.#postWorker({ type: "focus", surfaceId: viewId, sessionHandle, focused });
|
|
827
|
+
const session = this.#sessionByHandle.get(sessionHandle);
|
|
828
|
+
if (!session)
|
|
829
|
+
return;
|
|
830
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => {
|
|
831
|
+
this.#control?.notify({
|
|
832
|
+
type: "focus",
|
|
833
|
+
sessionId: session.id,
|
|
834
|
+
viewId,
|
|
835
|
+
attachmentEpoch,
|
|
836
|
+
inputSequence,
|
|
837
|
+
focused,
|
|
838
|
+
});
|
|
839
|
+
if (focused) {
|
|
840
|
+
this.#control?.notify({
|
|
841
|
+
type: "focus-and-resize",
|
|
842
|
+
sessionId: session.id,
|
|
843
|
+
viewId,
|
|
844
|
+
attachmentEpoch,
|
|
845
|
+
cols,
|
|
846
|
+
rows,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
async copySelection(sessionId, viewId, selection, selectAll = false) {
|
|
852
|
+
await this.connect();
|
|
853
|
+
const view = this.#views.get(viewId);
|
|
854
|
+
if (!view || view.sessionId !== sessionId || view.attachmentEpoch === undefined)
|
|
855
|
+
return "";
|
|
856
|
+
const response = await this.#control.request({
|
|
857
|
+
type: "selection-text",
|
|
858
|
+
sessionId,
|
|
859
|
+
viewId,
|
|
860
|
+
attachmentEpoch: view.attachmentEpoch,
|
|
861
|
+
startColumn: selection.anchor.column,
|
|
862
|
+
startRow: selection.anchor.row,
|
|
863
|
+
endColumn: selection.focus.column,
|
|
864
|
+
endRow: selection.focus.row,
|
|
865
|
+
selectAll,
|
|
866
|
+
});
|
|
867
|
+
if (response.type !== "selection-text")
|
|
868
|
+
throw new Error("terminald returned an unexpected selection response");
|
|
869
|
+
if (response.text)
|
|
870
|
+
this.#platform.writeClipboard(response.text);
|
|
871
|
+
return response.text;
|
|
872
|
+
}
|
|
873
|
+
interrupt(sessionId, viewId) {
|
|
874
|
+
this.#sendViewInput(viewId, (attachmentEpoch, inputSequence) => this.#control?.notify({ type: "interrupt", sessionId, viewId, attachmentEpoch, inputSequence }));
|
|
875
|
+
const handle = this.#handleBySessionId.get(sessionId);
|
|
876
|
+
if (handle)
|
|
877
|
+
this.#postWorker({ type: "cursor-activity", sessionHandle: handle });
|
|
878
|
+
}
|
|
879
|
+
#removeRegisteredSession(sessionId, detachViews) {
|
|
880
|
+
const handle = this.#handleBySessionId.get(sessionId);
|
|
881
|
+
if (!handle)
|
|
882
|
+
return;
|
|
883
|
+
const releaseTimer = this.#sessionReleaseTimers.get(handle);
|
|
884
|
+
if (releaseTimer !== undefined)
|
|
885
|
+
window.clearTimeout(releaseTimer);
|
|
886
|
+
this.#sessionReleaseTimers.delete(handle);
|
|
887
|
+
this.#pinnedSessionHandles.delete(handle);
|
|
888
|
+
this.#sessionMountReferences.delete(handle);
|
|
889
|
+
const subscriptionChanged = this.#subscribedSessionHandles.delete(handle);
|
|
890
|
+
this.#cancelMetadataRefresh(handle);
|
|
891
|
+
this.#mouseTrackingByHandle.delete(handle);
|
|
892
|
+
this.#scrollbarByHandle.delete(handle);
|
|
893
|
+
for (const mounted of [...this.#mountedEntries]) {
|
|
894
|
+
if (mounted.sessionHandle !== handle)
|
|
895
|
+
continue;
|
|
896
|
+
mounted.active = false;
|
|
897
|
+
if (mounted.disposeTimer !== undefined)
|
|
898
|
+
window.clearTimeout(mounted.disposeTimer);
|
|
899
|
+
mounted.disposeTimer = undefined;
|
|
900
|
+
const ownsWorkerSurface = this.#mountGenerationBySurface.get(mounted.viewId) === mounted.generation;
|
|
901
|
+
if (ownsWorkerSurface) {
|
|
902
|
+
this.#postWorker({ type: "unmount", surfaceId: mounted.viewId });
|
|
903
|
+
this.#mountGenerationBySurface.delete(mounted.viewId);
|
|
904
|
+
if (detachViews)
|
|
905
|
+
this.#control?.notify({ type: "detach-session", sessionId: mounted.sessionId, viewId: mounted.viewId });
|
|
906
|
+
this.#views.delete(mounted.viewId);
|
|
907
|
+
this.#focusByView.delete(mounted.viewId);
|
|
908
|
+
}
|
|
909
|
+
this.#mountedCanvases.delete(mounted.canvas);
|
|
910
|
+
this.#mountedEntries.delete(mounted);
|
|
911
|
+
}
|
|
912
|
+
for (const [viewId, view] of this.#views) {
|
|
913
|
+
if (view.sessionId === sessionId) {
|
|
914
|
+
view.pendingInput.length = 0;
|
|
915
|
+
if (detachViews)
|
|
916
|
+
this.#control?.notify({ type: "detach-session", sessionId, viewId });
|
|
917
|
+
this.#views.delete(viewId);
|
|
918
|
+
this.#focusByView.delete(viewId);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
this.#sessionByHandle.delete(handle);
|
|
922
|
+
this.#handleBySessionId.delete(sessionId);
|
|
923
|
+
if (subscriptionChanged) {
|
|
924
|
+
this.#frameSubscriptionVersion += 1;
|
|
925
|
+
this.#syncFrameSubscriptionsInBackground();
|
|
926
|
+
}
|
|
927
|
+
this.#resync.cancel(handle);
|
|
928
|
+
this.#postWorker({ type: "drop-session", sessionHandle: handle });
|
|
929
|
+
}
|
|
930
|
+
unregisterSession(sessionId) {
|
|
931
|
+
this.#removeRegisteredSession(sessionId, true);
|
|
932
|
+
}
|
|
933
|
+
terminate(sessionId, source = "user") {
|
|
934
|
+
this.#control?.notify({ type: "terminate", sessionId, source });
|
|
935
|
+
this.#removeRegisteredSession(sessionId, false);
|
|
936
|
+
}
|
|
937
|
+
resize(sessionId, viewId, cols, rows) {
|
|
938
|
+
const view = this.#views.get(viewId);
|
|
939
|
+
if (!view || view.sessionId !== sessionId)
|
|
940
|
+
return;
|
|
941
|
+
view.desiredCols = cols;
|
|
942
|
+
view.desiredRows = rows;
|
|
943
|
+
if (view.attachmentEpoch === undefined || view.controlEpoch === undefined)
|
|
944
|
+
return;
|
|
945
|
+
this.#sendResize(viewId, view, cols, rows);
|
|
946
|
+
}
|
|
947
|
+
dispose() {
|
|
948
|
+
if (this.#disposed)
|
|
949
|
+
return;
|
|
950
|
+
this.#disposed = true;
|
|
951
|
+
for (const [viewId, view] of this.#views) {
|
|
952
|
+
view.pendingInput.length = 0;
|
|
953
|
+
this.#control?.notify({ type: "detach-session", sessionId: view.sessionId, viewId });
|
|
954
|
+
}
|
|
955
|
+
for (const mounted of this.#mountedEntries) {
|
|
956
|
+
if (mounted.disposeTimer !== undefined)
|
|
957
|
+
window.clearTimeout(mounted.disposeTimer);
|
|
958
|
+
mounted.active = false;
|
|
959
|
+
this.#mountedCanvases.delete(mounted.canvas);
|
|
960
|
+
}
|
|
961
|
+
this.#mountedEntries.clear();
|
|
962
|
+
for (const timer of this.#sessionReleaseTimers.values())
|
|
963
|
+
window.clearTimeout(timer);
|
|
964
|
+
this.#sessionReleaseTimers.clear();
|
|
965
|
+
for (const pending of this.#frameSubscriptionRequests.values()) {
|
|
966
|
+
window.clearTimeout(pending.timer);
|
|
967
|
+
pending.reject(new Error("Terminal runtime was disposed during a frame subscription"));
|
|
968
|
+
}
|
|
969
|
+
this.#frameSubscriptionRequests.clear();
|
|
970
|
+
for (const timer of this.#metadataTimers.values())
|
|
971
|
+
window.clearTimeout(timer);
|
|
972
|
+
this.#metadataTimers.clear();
|
|
973
|
+
this.#resync.dispose();
|
|
974
|
+
for (const request of this.#performanceRequests.values()) {
|
|
975
|
+
window.clearTimeout(request.timer);
|
|
976
|
+
request.reject(new Error("Terminal runtime was disposed during a performance request"));
|
|
977
|
+
}
|
|
978
|
+
this.#performanceRequests.clear();
|
|
979
|
+
this.#views.clear();
|
|
980
|
+
this.#focusByView.clear();
|
|
981
|
+
this.#mountGenerationBySurface.clear();
|
|
982
|
+
this.#mouseTrackingByHandle.clear();
|
|
983
|
+
this.#scrollbarByHandle.clear();
|
|
984
|
+
this.#subscribedSessionHandles.clear();
|
|
985
|
+
this.#pinnedSessionHandles.clear();
|
|
986
|
+
this.#sessionMountReferences.clear();
|
|
987
|
+
this.#sessionByHandle.clear();
|
|
988
|
+
this.#handleBySessionId.clear();
|
|
989
|
+
if (this.#frames) {
|
|
990
|
+
this.#frames.onmessage = null;
|
|
991
|
+
this.#frames.close();
|
|
992
|
+
this.#frames = undefined;
|
|
993
|
+
}
|
|
994
|
+
this.#control?.dispose();
|
|
995
|
+
this.#control = undefined;
|
|
996
|
+
this.#worker.terminate();
|
|
997
|
+
void this.#ports.then((ports) => {
|
|
998
|
+
ports.control.close();
|
|
999
|
+
ports.frames.close();
|
|
1000
|
+
}, () => undefined);
|
|
1001
|
+
}
|
|
1002
|
+
#sendResize(viewId, view, cols, rows) {
|
|
1003
|
+
if (view.attachmentEpoch === undefined || view.controlEpoch === undefined)
|
|
1004
|
+
return;
|
|
1005
|
+
view.resizeSequence += 1;
|
|
1006
|
+
this.#control?.notify({
|
|
1007
|
+
type: "resize",
|
|
1008
|
+
sessionId: view.sessionId,
|
|
1009
|
+
viewId,
|
|
1010
|
+
attachmentEpoch: view.attachmentEpoch,
|
|
1011
|
+
controlEpoch: view.controlEpoch,
|
|
1012
|
+
resizeSequence: view.resizeSequence,
|
|
1013
|
+
cols,
|
|
1014
|
+
rows,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
export function createGhostteaTerminalRuntime(options) {
|
|
1019
|
+
return new GhostteaTerminalRuntime(options);
|
|
1020
|
+
}
|
|
1021
|
+
//# sourceMappingURL=runtime.js.map
|