tmux-ide 2.9.12 → 2.9.13
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/bin/cli.js +3438 -2062
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/pane-stream.ts +17 -0
- package/packages/contracts/src/terminal-delivery.ts +2 -1
- package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
- package/packages/contracts/src/terminal-graphics.ts +5 -0
- package/packages/contracts/src/terminal-surfaces.ts +137 -0
- package/packages/core/src/terminal-delivery.ts +8 -2
- package/packages/core/src/terminal-replica.ts +33 -0
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +2 -0
- package/packages/daemon/dist/command-center/resources/saved-machine-watch.js +117 -0
- package/packages/daemon/dist/command-center/server.js +13 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +9 -0
- package/packages/daemon/dist/lib/daemon-embed.js +32 -1
- package/packages/daemon/dist/lib/native-surface-context.js +96 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +9 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/server/native-surface-provider-upgrade.js +88 -0
- package/packages/daemon/dist/terminal/graphics/compress-chunk.js +40 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-demand.js +99 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-provider-channel.js +162 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-provider.js +275 -0
- package/packages/daemon/dist/terminal/graphics/native-surface-viewer.js +146 -0
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +3 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +32 -9
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +196 -3
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +9 -0
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +4 -0
- package/packages/daemon/src/command-center/resources/saved-machine-watch.ts +132 -0
- package/packages/daemon/src/command-center/server.ts +18 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +14 -0
- package/packages/daemon/src/lib/daemon-embed.ts +43 -1
- package/packages/daemon/src/lib/native-surface-context.ts +123 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +9 -0
- package/packages/daemon/src/server/native-surface-provider-upgrade.ts +103 -0
- package/packages/daemon/src/terminal/graphics/compress-chunk.ts +49 -0
- package/packages/daemon/src/terminal/graphics/native-surface-demand.ts +88 -0
- package/packages/daemon/src/terminal/graphics/native-surface-provider-channel.ts +161 -0
- package/packages/daemon/src/terminal/graphics/native-surface-provider.ts +308 -0
- package/packages/daemon/src/terminal/graphics/native-surface-viewer.ts +154 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +5 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +31 -10
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +229 -2
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +11 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TERMINAL_SURFACE_MAX_CONTROL_BYTES, TERMINAL_SURFACE_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
|
|
2
|
+
import { NativeSurfaceViewer } from "../graphics/native-surface-viewer.js";
|
|
3
|
+
import { compressGraphicsChunk } from "../graphics/compress-chunk.js";
|
|
4
|
+
import { TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION, TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION, TERMINAL_PROJECTION_CAPABILITY_VERSION, TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
|
|
2
5
|
import { PANE_STREAM_INPUT_LIFETIME_CAPABILITY_VERSION, PANE_STREAM_INPUT_BYTES_CAPABILITY_VERSION, } from "@tmux-ide/contracts";
|
|
3
6
|
import { inflateSync } from "node:zlib";
|
|
4
7
|
import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
|
|
@@ -116,6 +119,7 @@ function errorFrameCode(error) {
|
|
|
116
119
|
* ticket digests; a daemon restart makes every prior descriptor inert.
|
|
117
120
|
*/
|
|
118
121
|
export class PaneStreamAdmissionCoordinator {
|
|
122
|
+
#nativeSurfaces;
|
|
119
123
|
#localImages;
|
|
120
124
|
#instanceId;
|
|
121
125
|
#webSocketUrl;
|
|
@@ -147,6 +151,9 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
147
151
|
#shuttingDown = false;
|
|
148
152
|
#shutdownPromise = null;
|
|
149
153
|
constructor(options) {
|
|
154
|
+
this.#nativeSurfaces = options.nativeSurfaces;
|
|
155
|
+
if (this.#nativeSurfaces && this.#nativeSurfaces.generation !== options.daemonInstanceId)
|
|
156
|
+
throw new Error("Native surface provider generation mismatch");
|
|
150
157
|
this.#localImages = options.localImages;
|
|
151
158
|
this.#instanceId = BindingIdSchemaZ.parse(options.daemonInstanceId);
|
|
152
159
|
this.#webSocketUrl = PaneStreamLoopbackWebSocketUrlSchemaZ.parse(options.webSocketUrl);
|
|
@@ -410,6 +417,7 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
410
417
|
let live;
|
|
411
418
|
try {
|
|
412
419
|
live = new PaneStreamLiveConnection({
|
|
420
|
+
nativeSurfaces: this.#nativeSurfaces,
|
|
413
421
|
localImages: this.#localImages,
|
|
414
422
|
clientId: `psc-${this.#clientCounter}`,
|
|
415
423
|
socket,
|
|
@@ -618,6 +626,13 @@ export function rawGraphicsState(state) {
|
|
|
618
626
|
}
|
|
619
627
|
const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
620
628
|
export class PaneStreamLiveConnection {
|
|
629
|
+
// One bounded native negotiation per redeemed socket; never reuse on reconnect.
|
|
630
|
+
#nativeSurfaces;
|
|
631
|
+
#surfaceViewer = null;
|
|
632
|
+
#surfaceDemands = new Map();
|
|
633
|
+
#surfaceNative = false;
|
|
634
|
+
#surfaceSession = null;
|
|
635
|
+
#surfaceReceiver = null;
|
|
621
636
|
#localImages;
|
|
622
637
|
#localLeases = new Map();
|
|
623
638
|
#clientId;
|
|
@@ -707,6 +722,7 @@ export class PaneStreamLiveConnection {
|
|
|
707
722
|
return elapsed;
|
|
708
723
|
}
|
|
709
724
|
constructor(options) {
|
|
725
|
+
this.#nativeSurfaces = options.nativeSurfaces;
|
|
710
726
|
this.#localImages = options.localImages;
|
|
711
727
|
this.#clientId = options.clientId;
|
|
712
728
|
this.#socket = options.socket;
|
|
@@ -801,6 +817,8 @@ export class PaneStreamLiveConnection {
|
|
|
801
817
|
}
|
|
802
818
|
this.#stopAuthoritySnapshots =
|
|
803
819
|
this.#sessionRuntimeBinding?.onAuthoritySnapshot?.((snapshot) => this.#usesExplicitAuthority ? this.#sendAuthoritySnapshot(snapshot) : undefined) ?? null;
|
|
820
|
+
if (!this.#descriptor.terminalDelivery)
|
|
821
|
+
this.#observeSurfaceDemand(this.#descriptor.panes);
|
|
804
822
|
void this.#subscribeAll();
|
|
805
823
|
}
|
|
806
824
|
close(code = 1000, reason = "stream-closed") {
|
|
@@ -808,6 +826,13 @@ export class PaneStreamLiveConnection {
|
|
|
808
826
|
return;
|
|
809
827
|
this.#recordDiagnosticLifecycle("pane-stream-terminal", code, reason);
|
|
810
828
|
this.#closed = true;
|
|
829
|
+
for (const demand of this.#surfaceDemands.values())
|
|
830
|
+
demand.close();
|
|
831
|
+
this.#surfaceDemands.clear();
|
|
832
|
+
this.#surfaceViewer?.close();
|
|
833
|
+
this.#surfaceViewer = null;
|
|
834
|
+
this.#surfaceSession?.close();
|
|
835
|
+
this.#surfaceSession = null;
|
|
811
836
|
this.#cancelDrainTick?.();
|
|
812
837
|
this.#cancelDrainTick = null;
|
|
813
838
|
// Departure force-returns every ticket within this same tick.
|
|
@@ -839,6 +864,8 @@ export class PaneStreamLiveConnection {
|
|
|
839
864
|
const delivery = channel.delivery;
|
|
840
865
|
channel.delivery = null;
|
|
841
866
|
channel.closed = true;
|
|
867
|
+
this.#surfaceDemands.get(channel.semanticPaneId)?.close();
|
|
868
|
+
this.#surfaceDemands.delete(channel.semanticPaneId);
|
|
842
869
|
if (sub)
|
|
843
870
|
closures.push(sub.close().catch(() => undefined));
|
|
844
871
|
if (delivery)
|
|
@@ -1264,11 +1291,30 @@ export class PaneStreamLiveConnection {
|
|
|
1264
1291
|
}
|
|
1265
1292
|
await this.#awaitSemanticCredit(pane);
|
|
1266
1293
|
}
|
|
1294
|
+
#observeSurfaceDemand(panes) {
|
|
1295
|
+
for (const pane of panes) {
|
|
1296
|
+
if (this.#surfaceDemands.has(pane))
|
|
1297
|
+
continue;
|
|
1298
|
+
const demand = this.#nativeSurfaces?.demand.viewer(this.#descriptor.sessionName, [pane]);
|
|
1299
|
+
if (!demand)
|
|
1300
|
+
return;
|
|
1301
|
+
demand.native(this.#surfaceNative);
|
|
1302
|
+
this.#surfaceDemands.set(pane, demand);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
#surfaceControl(type) {
|
|
1306
|
+
if (type === "terminal-surface-offer")
|
|
1307
|
+
return;
|
|
1308
|
+
this.#surfaceNative = type === "terminal-surface-peer";
|
|
1309
|
+
for (const demand of this.#surfaceDemands.values())
|
|
1310
|
+
demand.native(this.#surfaceNative);
|
|
1311
|
+
}
|
|
1267
1312
|
/** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
|
|
1268
1313
|
#startGraphics(channel) {
|
|
1269
1314
|
const graphics = channel.delivery?.graphics;
|
|
1270
1315
|
if (!graphics)
|
|
1271
1316
|
return;
|
|
1317
|
+
this.#observeSurfaceDemand([channel.semanticPaneId]);
|
|
1272
1318
|
// Renderers that cannot inflate see raw formats; their bytes are inflated
|
|
1273
1319
|
// when served.
|
|
1274
1320
|
const zlib = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION) === true;
|
|
@@ -1277,7 +1323,11 @@ export class PaneStreamLiveConnection {
|
|
|
1277
1323
|
!!this.#localImages &&
|
|
1278
1324
|
this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_LOCAL_CAPABILITY_VERSION ||
|
|
1279
1325
|
version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
1280
|
-
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION
|
|
1326
|
+
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
|
|
1327
|
+
version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
|
|
1328
|
+
channel.graphicsCompressed =
|
|
1329
|
+
this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
|
|
1330
|
+
version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
|
|
1281
1331
|
channel.graphicsInline = new Set();
|
|
1282
1332
|
channel.graphicsBinary =
|
|
1283
1333
|
this.#descriptor.terminalDelivery?.protocolVersions.some((version) => version === TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION ||
|
|
@@ -1285,7 +1335,9 @@ export class PaneStreamLiveConnection {
|
|
|
1285
1335
|
version === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
|
|
1286
1336
|
version === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
1287
1337
|
version === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
1288
|
-
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION
|
|
1338
|
+
version === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
|
|
1339
|
+
version === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
|
|
1340
|
+
version === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION) === true;
|
|
1289
1341
|
// Renderers that report cell pixels learn up front that this daemon
|
|
1290
1342
|
// accepts them, before any image exists.
|
|
1291
1343
|
const cellPixels = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION) === true;
|
|
@@ -1314,6 +1366,7 @@ export class PaneStreamLiveConnection {
|
|
|
1314
1366
|
...(cellPixels ? { cellPixels: true } : {}),
|
|
1315
1367
|
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1316
1368
|
...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
|
|
1369
|
+
...(channel.graphicsCompressed ? { compressedBlobs: true } : {}),
|
|
1317
1370
|
...(channel.graphicsLocal ? { localFiles: true } : {}),
|
|
1318
1371
|
},
|
|
1319
1372
|
});
|
|
@@ -1332,6 +1385,7 @@ export class PaneStreamLiveConnection {
|
|
|
1332
1385
|
cellPixels: true,
|
|
1333
1386
|
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1334
1387
|
...(channel.graphicsBinary ? { binaryBlobs: true } : {}),
|
|
1388
|
+
...(channel.graphicsCompressed ? { compressedBlobs: true } : {}),
|
|
1335
1389
|
...(channel.graphicsLocal ? { localFiles: true } : {}),
|
|
1336
1390
|
},
|
|
1337
1391
|
});
|
|
@@ -1452,6 +1506,29 @@ export class PaneStreamLiveConnection {
|
|
|
1452
1506
|
break;
|
|
1453
1507
|
const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
|
|
1454
1508
|
if (channel.graphicsBinary) {
|
|
1509
|
+
const header = {
|
|
1510
|
+
pane: channel.semanticPaneId,
|
|
1511
|
+
blob: {
|
|
1512
|
+
digest,
|
|
1513
|
+
format,
|
|
1514
|
+
width: image.width,
|
|
1515
|
+
height: image.height,
|
|
1516
|
+
total,
|
|
1517
|
+
offset,
|
|
1518
|
+
},
|
|
1519
|
+
};
|
|
1520
|
+
const compressed = channel.graphicsCompressed
|
|
1521
|
+
? await compressGraphicsChunk(header, data.subarray(offset, end))
|
|
1522
|
+
: null;
|
|
1523
|
+
// Compression yields: a pane may close or replace this image meanwhile.
|
|
1524
|
+
if (this.#closed || channel.closed)
|
|
1525
|
+
return;
|
|
1526
|
+
if (graphics.image(digest) !== image)
|
|
1527
|
+
break;
|
|
1528
|
+
if (compressed) {
|
|
1529
|
+
this.#sendEncoded(null, Buffer.from(compressed), true);
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1455
1532
|
this.#sendEncoded(null, Buffer.from(encodeTerminalGraphicsBinary({
|
|
1456
1533
|
pane: channel.semanticPaneId,
|
|
1457
1534
|
blob: {
|
|
@@ -1621,6 +1698,8 @@ export class PaneStreamLiveConnection {
|
|
|
1621
1698
|
if (channel.closed)
|
|
1622
1699
|
return;
|
|
1623
1700
|
channel.closed = true;
|
|
1701
|
+
this.#surfaceDemands.get(channel.semanticPaneId)?.close();
|
|
1702
|
+
this.#surfaceDemands.delete(channel.semanticPaneId);
|
|
1624
1703
|
const sub = channel.sub;
|
|
1625
1704
|
channel.sub = null;
|
|
1626
1705
|
if (sub)
|
|
@@ -2081,6 +2160,120 @@ export class PaneStreamLiveConnection {
|
|
|
2081
2160
|
this.#acceptSemanticIntent(frame.operationId, frame.intent);
|
|
2082
2161
|
return;
|
|
2083
2162
|
}
|
|
2163
|
+
if (frame.type === "terminal-surface-watch") {
|
|
2164
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
|
|
2165
|
+
frame.generation !== this.#sessionRuntimeBinding?.generation ||
|
|
2166
|
+
!this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_SURFACE_CAPABILITY_VERSION) ||
|
|
2167
|
+
!this.#nativeSurfaces ||
|
|
2168
|
+
this.#surfaceReceiver)
|
|
2169
|
+
return this.#failProtocol("protocol-error");
|
|
2170
|
+
if (this.#surfaceViewer)
|
|
2171
|
+
return;
|
|
2172
|
+
this.#surfaceViewer = new NativeSurfaceViewer(this.#nativeSurfaces, this.#descriptor.panes, (control) => {
|
|
2173
|
+
if (this.#closed)
|
|
2174
|
+
return false;
|
|
2175
|
+
this.#surfaceControl(control.type);
|
|
2176
|
+
this.#sendFrame(null, control);
|
|
2177
|
+
return !this.#closed;
|
|
2178
|
+
}, (pane, generation) => {
|
|
2179
|
+
const channel = this.#panes.get(pane);
|
|
2180
|
+
const address = channel?.deliveryAddress;
|
|
2181
|
+
return !this.#closed &&
|
|
2182
|
+
channel &&
|
|
2183
|
+
!channel.closed &&
|
|
2184
|
+
channel.delivery &&
|
|
2185
|
+
address?.generation === generation
|
|
2186
|
+
? address.deliveryNonce
|
|
2187
|
+
: null;
|
|
2188
|
+
});
|
|
2189
|
+
this.#surfaceViewer.start();
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (frame.type === "terminal-surface-receiver") {
|
|
2193
|
+
if (this.#surfaceViewer) {
|
|
2194
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.receive(frame))
|
|
2195
|
+
this.#failProtocol("protocol-error");
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
|
|
2199
|
+
!this.#descriptor.terminalDelivery ||
|
|
2200
|
+
frame.generation !== this.#sessionRuntimeBinding?.generation) {
|
|
2201
|
+
return this.#failProtocol("protocol-error");
|
|
2202
|
+
}
|
|
2203
|
+
if (this.#surfaceReceiver) {
|
|
2204
|
+
// Parsed schema order is deterministic. A duplicate cannot change the
|
|
2205
|
+
// challenge, kernel identity or endpoint, nor amplify fallback replies.
|
|
2206
|
+
if (JSON.stringify(frame) !== JSON.stringify(this.#surfaceReceiver)) {
|
|
2207
|
+
return this.#failProtocol("protocol-error");
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
this.#surfaceReceiver = frame;
|
|
2212
|
+
if (this.#descriptor.terminalDelivery.protocolVersions.includes(TERMINAL_SURFACE_CAPABILITY_VERSION)) {
|
|
2213
|
+
const native = this.#nativeSurfaces?.open(frame, this.#descriptor.panes, (control) => {
|
|
2214
|
+
if (this.#closed)
|
|
2215
|
+
return false;
|
|
2216
|
+
this.#surfaceControl(control.type);
|
|
2217
|
+
this.#sendFrame(null, control);
|
|
2218
|
+
return !this.#closed;
|
|
2219
|
+
}, (pane, generation) => {
|
|
2220
|
+
const channel = this.#panes.get(pane);
|
|
2221
|
+
const address = channel?.deliveryAddress;
|
|
2222
|
+
if (this.#closed ||
|
|
2223
|
+
!channel ||
|
|
2224
|
+
channel.closed ||
|
|
2225
|
+
!channel.delivery ||
|
|
2226
|
+
!address ||
|
|
2227
|
+
address.generation !== generation)
|
|
2228
|
+
return null;
|
|
2229
|
+
return address.deliveryNonce;
|
|
2230
|
+
}) ?? null;
|
|
2231
|
+
if (this.#closed) {
|
|
2232
|
+
native?.close();
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
this.#surfaceSession = native;
|
|
2236
|
+
if (native)
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
// No provider/capability means no PID/slot lookup and no inferred locality.
|
|
2240
|
+
this.#sendFrame(null, {
|
|
2241
|
+
type: "terminal-surface-fallback",
|
|
2242
|
+
version: 1,
|
|
2243
|
+
generation: frame.generation,
|
|
2244
|
+
connectionNonce: frame.connectionNonce,
|
|
2245
|
+
reason: "unsupported",
|
|
2246
|
+
});
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (frame.type === "terminal-surface-fallback") {
|
|
2250
|
+
if (this.#surfaceViewer) {
|
|
2251
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.fallback(frame))
|
|
2252
|
+
this.#failProtocol("protocol-error");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES ||
|
|
2256
|
+
frame.generation !== this.#surfaceReceiver?.generation ||
|
|
2257
|
+
frame.connectionNonce !== this.#surfaceReceiver.connectionNonce) {
|
|
2258
|
+
return this.#failProtocol("protocol-error");
|
|
2259
|
+
}
|
|
2260
|
+
this.#surfaceSession?.fallback(frame.reason);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (frame.type === "terminal-surface-retired") {
|
|
2264
|
+
if (this.#surfaceViewer) {
|
|
2265
|
+
if (byteLength > TERMINAL_SURFACE_MAX_CONTROL_BYTES || !this.#surfaceViewer.retire(frame))
|
|
2266
|
+
this.#failProtocol("protocol-error");
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (byteLength <= TERMINAL_SURFACE_MAX_CONTROL_BYTES &&
|
|
2270
|
+
frame.generation === this.#surfaceReceiver?.generation &&
|
|
2271
|
+
frame.connectionNonce === this.#surfaceReceiver.connectionNonce &&
|
|
2272
|
+
this.#surfaceSession?.retire(frame.sequence))
|
|
2273
|
+
return;
|
|
2274
|
+
// Unissued/replayed ACKs must not retire another socket's native storage.
|
|
2275
|
+
return this.#failProtocol("protocol-error");
|
|
2276
|
+
}
|
|
2084
2277
|
if (frame.type === "terminal-graphics-request") {
|
|
2085
2278
|
this.#requestGraphics(frame.pane, frame.digests, frame.preferInline === true);
|
|
2086
2279
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeSurfaceProviderRegistry } from "../graphics/native-surface-provider.js";
|
|
1
2
|
import { SessionRuntimeTransportBinder } from "../session-runtime/transport-binding.js";
|
|
2
3
|
import { PaneStreamLeaseManager } from "./lease-manager.js";
|
|
3
4
|
import { PaneStreamAdmissionCoordinator, } from "./pane-stream-websocket.js";
|
|
@@ -8,8 +9,14 @@ import { PaneStreamAdmissionCoordinator, } from "./pane-stream-websocket.js";
|
|
|
8
9
|
*/
|
|
9
10
|
export class PaneStreamRuntime {
|
|
10
11
|
coordinator;
|
|
12
|
+
nativeSurfaces;
|
|
11
13
|
#disposePromise = null;
|
|
12
14
|
constructor(options) {
|
|
15
|
+
this.nativeSurfaces =
|
|
16
|
+
options.nativeSurfaces ??
|
|
17
|
+
(process.platform === "darwin"
|
|
18
|
+
? new NativeSurfaceProviderRegistry(options.daemonInstanceId)
|
|
19
|
+
: undefined);
|
|
13
20
|
const leaseManager = new PaneStreamLeaseManager({
|
|
14
21
|
daemonInstanceId: options.daemonInstanceId,
|
|
15
22
|
});
|
|
@@ -17,6 +24,7 @@ export class PaneStreamRuntime {
|
|
|
17
24
|
this.coordinator = new PaneStreamAdmissionCoordinator({
|
|
18
25
|
...options.admission,
|
|
19
26
|
localImages: options.localImages,
|
|
27
|
+
nativeSurfaces: this.nativeSurfaces,
|
|
20
28
|
daemonInstanceId: options.daemonInstanceId,
|
|
21
29
|
webSocketUrl: options.webSocketUrl,
|
|
22
30
|
leaseManager,
|
|
@@ -43,6 +51,7 @@ export class PaneStreamRuntime {
|
|
|
43
51
|
dispose() {
|
|
44
52
|
if (!this.#disposePromise) {
|
|
45
53
|
this.#disposePromise = (async () => {
|
|
54
|
+
this.nativeSurfaces?.close();
|
|
46
55
|
await this.coordinator.shutdown();
|
|
47
56
|
})();
|
|
48
57
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mountSavedMachineWatch } from "./saved-machine-watch.ts";
|
|
1
2
|
import type { Hono } from "hono";
|
|
2
3
|
import { bodyLimit } from "hono/body-limit";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
loadSavedMachines,
|
|
8
9
|
mergeSavedMachines,
|
|
9
10
|
updateSavedMachines,
|
|
11
|
+
watchSavedMachines,
|
|
10
12
|
} from "../../lib/saved-machines.ts";
|
|
11
13
|
import { ownerAuthorityGate } from "../owner-authority.ts";
|
|
12
14
|
|
|
@@ -26,8 +28,10 @@ export function mountSavedMachineRoute(
|
|
|
26
28
|
read?: typeof loadSavedMachines;
|
|
27
29
|
merge?: typeof mergeSavedMachines;
|
|
28
30
|
update?: typeof updateSavedMachines;
|
|
31
|
+
watch?: typeof watchSavedMachines;
|
|
29
32
|
},
|
|
30
33
|
) {
|
|
34
|
+
mountSavedMachineWatch(app, options);
|
|
31
35
|
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
32
36
|
whenOwnerless: "unavailable",
|
|
33
37
|
unavailableMessage: "Machine registry unavailable",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
SavedMachineRegistrySchema,
|
|
6
|
+
type SavedMachineRegistry,
|
|
7
|
+
type DaemonInstanceIdentity,
|
|
8
|
+
} from "@tmux-ide/contracts";
|
|
9
|
+
import { loadSavedMachines, watchSavedMachines } from "../../lib/saved-machines.ts";
|
|
10
|
+
import { ownerAuthorityGate } from "../owner-authority.ts";
|
|
11
|
+
|
|
12
|
+
export const SAVED_MACHINE_WATCH_WAIT_MS = 20_000;
|
|
13
|
+
const MAX_WATCHERS = 32;
|
|
14
|
+
const Query = z.strictObject({
|
|
15
|
+
expectedInstanceId: z.uuid(),
|
|
16
|
+
after: z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
19
|
+
.optional(),
|
|
20
|
+
});
|
|
21
|
+
type Snapshot = { revision: string; registry: SavedMachineRegistry };
|
|
22
|
+
type Result =
|
|
23
|
+
| { status: 200; revision: string; registry: SavedMachineRegistry | null }
|
|
24
|
+
| { status: 408 | 503 };
|
|
25
|
+
function snapshot(value: SavedMachineRegistry): Snapshot {
|
|
26
|
+
const registry = SavedMachineRegistrySchema.parse(value);
|
|
27
|
+
return {
|
|
28
|
+
registry,
|
|
29
|
+
revision: createHash("sha256").update(JSON.stringify(registry)).digest("hex"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** One bounded notification request. A new request always observes the latest registry. */
|
|
34
|
+
export function mountSavedMachineWatch(
|
|
35
|
+
app: Hono,
|
|
36
|
+
options: {
|
|
37
|
+
daemon: DaemonInstanceIdentity;
|
|
38
|
+
ownerToken: string | null;
|
|
39
|
+
read?: typeof loadSavedMachines;
|
|
40
|
+
watch?: typeof watchSavedMachines;
|
|
41
|
+
},
|
|
42
|
+
) {
|
|
43
|
+
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
44
|
+
whenOwnerless: "unavailable",
|
|
45
|
+
unavailableMessage: "Machine registry unavailable",
|
|
46
|
+
mismatchMessage: "Machine registry requires owner authority",
|
|
47
|
+
});
|
|
48
|
+
const read = options.read ?? loadSavedMachines;
|
|
49
|
+
const watch = options.watch ?? watchSavedMachines;
|
|
50
|
+
let active = 0;
|
|
51
|
+
app.get("/api/resources/saved-machines/watch", async (c) => {
|
|
52
|
+
const gate = authorize(c);
|
|
53
|
+
if (gate) return gate;
|
|
54
|
+
c.header("Cache-Control", "no-store");
|
|
55
|
+
const query = Query.safeParse(c.req.query());
|
|
56
|
+
if (!query.success) return c.json({ error: "Invalid registry watch request" }, 400);
|
|
57
|
+
if (query.data.expectedInstanceId !== options.daemon.instanceId)
|
|
58
|
+
return c.json({ error: "Daemon generation changed" }, 409);
|
|
59
|
+
if (active >= MAX_WATCHERS) return c.json({ error: "Registry watch capacity reached" }, 429);
|
|
60
|
+
active++;
|
|
61
|
+
try {
|
|
62
|
+
const initial = snapshot(read());
|
|
63
|
+
if (query.data.after !== initial.revision)
|
|
64
|
+
return c.json({ daemon: options.daemon, ...initial });
|
|
65
|
+
const signal = c.req.raw.signal;
|
|
66
|
+
const result = await new Promise<Result>((resolve) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
let stop = () => {};
|
|
69
|
+
const finish = (result: Result) => {
|
|
70
|
+
if (settled) return;
|
|
71
|
+
settled = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
signal.removeEventListener("abort", aborted);
|
|
74
|
+
try {
|
|
75
|
+
stop();
|
|
76
|
+
} finally {
|
|
77
|
+
resolve(result);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const aborted = () => finish({ status: 408 });
|
|
81
|
+
const timer = setTimeout(
|
|
82
|
+
() => finish({ status: 200, revision: initial.revision, registry: null }),
|
|
83
|
+
SAVED_MACHINE_WATCH_WAIT_MS,
|
|
84
|
+
);
|
|
85
|
+
timer.unref();
|
|
86
|
+
const changed = (value: SavedMachineRegistry) => {
|
|
87
|
+
if (settled) return;
|
|
88
|
+
try {
|
|
89
|
+
const next = snapshot(value);
|
|
90
|
+
if (next.revision !== initial.revision) finish({ status: 200, ...next });
|
|
91
|
+
} catch {
|
|
92
|
+
finish({ status: 503 });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
96
|
+
if (signal.aborted) {
|
|
97
|
+
aborted();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
stop = watch(changed, () => finish({ status: 503 }));
|
|
102
|
+
// Watch implementations may synchronously report an initial error/change.
|
|
103
|
+
if (settled) {
|
|
104
|
+
stop();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Close the read-before-subscribe race independently of watcher behavior.
|
|
108
|
+
changed(read());
|
|
109
|
+
} catch {
|
|
110
|
+
finish({ status: 503 });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (result.status !== 200)
|
|
114
|
+
return c.json(
|
|
115
|
+
{
|
|
116
|
+
error:
|
|
117
|
+
result.status === 408 ? "Registry watch cancelled" : "Machine registry unavailable",
|
|
118
|
+
},
|
|
119
|
+
result.status,
|
|
120
|
+
);
|
|
121
|
+
return c.json({
|
|
122
|
+
daemon: options.daemon,
|
|
123
|
+
revision: result.revision,
|
|
124
|
+
registry: result.registry,
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
return c.json({ error: "Machine registry unavailable" }, 503);
|
|
128
|
+
} finally {
|
|
129
|
+
active--;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TERMINAL_SURFACE_CAPABILITY_VERSION } from "@tmux-ide/contracts";
|
|
1
2
|
import { mountAutomationRoutes } from "./automation.ts";
|
|
2
3
|
import { mountTmuxServerRoutes } from "./tmux-servers.ts";
|
|
3
4
|
import { streamBoundedLogs } from "./log-stream.ts";
|
|
@@ -197,6 +198,9 @@ export interface CreateAppOptions {
|
|
|
197
198
|
/** Catalog + startup-barrier facts for the startup readiness ladder. */
|
|
198
199
|
startupReadinessAttachmentBackend?: StartupReadinessAttachmentAuthority | null;
|
|
199
200
|
paneStreamIssueBackend?: PaneStreamIssueBackend | null;
|
|
201
|
+
/** Current authenticated provider availability, never inferred from platform or PID. */
|
|
202
|
+
nativeSurfaceProviderAvailable?: () => boolean;
|
|
203
|
+
nativeSurfaceProviderSupported?: () => boolean;
|
|
200
204
|
applicationShellInventoryBackend?: {
|
|
201
205
|
discoverApplicationShellSession(
|
|
202
206
|
requestedSessionName: string,
|
|
@@ -657,6 +661,20 @@ export function createApp(options: CreateAppOptions = {}): Hono {
|
|
|
657
661
|
status: "ok" as const,
|
|
658
662
|
daemon: daemonInstanceIdentity,
|
|
659
663
|
capabilities: {
|
|
664
|
+
...(c.req.query("nativeSurfaces") === "1"
|
|
665
|
+
? {
|
|
666
|
+
nativeSurfaces: {
|
|
667
|
+
supported:
|
|
668
|
+
!!options.paneStreamIssueBackend &&
|
|
669
|
+
options.nativeSurfaceProviderSupported?.() === true,
|
|
670
|
+
available:
|
|
671
|
+
options.paneStreamIssueBackend !== undefined &&
|
|
672
|
+
options.paneStreamIssueBackend !== null &&
|
|
673
|
+
options.nativeSurfaceProviderAvailable?.() === true,
|
|
674
|
+
protocolVersion: TERMINAL_SURFACE_CAPABILITY_VERSION,
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
: {}),
|
|
660
678
|
// Opt-in keeps the response compatible with older strict host schemas.
|
|
661
679
|
...(c.req.query("windowViewport") === "1"
|
|
662
680
|
? {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeSurfaceLocationSchema } from "../lib/native-surface-context.ts";
|
|
1
2
|
import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.ts";
|
|
2
3
|
import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.ts";
|
|
3
4
|
import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.ts";
|
|
@@ -277,6 +278,19 @@ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOption
|
|
|
277
278
|
);
|
|
278
279
|
}),
|
|
279
280
|
);
|
|
281
|
+
app.post(
|
|
282
|
+
`${scoped}/native-surface-context`,
|
|
283
|
+
route(async (c) => {
|
|
284
|
+
const server = scope(c);
|
|
285
|
+
const target = NativeSurfaceLocationSchema.parse(await boundedJson(c.req.raw, 8192));
|
|
286
|
+
c.req.raw.signal.throwIfAborted();
|
|
287
|
+
const context = await options.owners.withOwner(server, (owner) =>
|
|
288
|
+
owner.resolveNativeSurfaceContext(target, c.req.raw.signal),
|
|
289
|
+
);
|
|
290
|
+
c.req.raw.signal.throwIfAborted();
|
|
291
|
+
return c.json({ version: 1, server, context });
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
280
294
|
app.get(
|
|
281
295
|
`${scoped}/inventory/:workspaceName`,
|
|
282
296
|
route(async (c) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createNativeSurfaceContextResolver } from "./native-surface-context.ts";
|
|
2
|
+
import { NativeSurfaceProviderRegistry } from "../terminal/graphics/native-surface-provider.ts";
|
|
1
3
|
import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
|
|
2
4
|
import {
|
|
3
5
|
createNativeSplitCapabilityProbe,
|
|
@@ -139,6 +141,7 @@ import {
|
|
|
139
141
|
attachTerminalAttachmentWebSocket,
|
|
140
142
|
type TerminalAttachmentWebSocketBoundary,
|
|
141
143
|
} from "../server/terminal-attachment-upgrade.ts";
|
|
144
|
+
import { attachNativeSurfaceProvider } from "../server/native-surface-provider-upgrade.ts";
|
|
142
145
|
import {
|
|
143
146
|
attachPaneStreamWebSocket,
|
|
144
147
|
type PaneStreamWebSocketBoundary,
|
|
@@ -920,6 +923,10 @@ async function startHttpServer({
|
|
|
920
923
|
issue: (request, context) => getTerminalAttachmentRuntime().admission.issue(request, context),
|
|
921
924
|
},
|
|
922
925
|
paneStreamIssueBackend: paneStreamRuntime.coordinator,
|
|
926
|
+
nativeSurfaceProviderSupported: () =>
|
|
927
|
+
serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces !== undefined,
|
|
928
|
+
nativeSurfaceProviderAvailable: () =>
|
|
929
|
+
serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces?.available === true,
|
|
923
930
|
applicationShellInventoryBackend: terminalInventoryRuntime,
|
|
924
931
|
startupReadinessAttachmentBackend: terminalInventoryRuntime,
|
|
925
932
|
catalogLiveSessions,
|
|
@@ -970,12 +977,26 @@ async function startHttpServer({
|
|
|
970
977
|
(create ? getTerminalAttachmentRuntime() : peekTerminalAttachmentRuntime())?.admission ??
|
|
971
978
|
null,
|
|
972
979
|
);
|
|
973
|
-
const
|
|
980
|
+
const paneStreamSocketBoundary = attachPaneStreamWebSocket(
|
|
974
981
|
server,
|
|
975
982
|
paneStreamRuntime.coordinator,
|
|
976
983
|
PANE_STREAM_REDEEM_PATH,
|
|
977
984
|
() => serverOwners.isDefaultCurrent(),
|
|
978
985
|
);
|
|
986
|
+
const nativeProviderBoundary = paneStreamRuntime.nativeSurfaces
|
|
987
|
+
? attachNativeSurfaceProvider(
|
|
988
|
+
server,
|
|
989
|
+
paneStreamRuntime.nativeSurfaces,
|
|
990
|
+
localBypassToken ?? null,
|
|
991
|
+
() => serverOwners.isDefaultCurrent(),
|
|
992
|
+
)
|
|
993
|
+
: null;
|
|
994
|
+
const paneStreamBoundary: PaneStreamWebSocketBoundary = {
|
|
995
|
+
async close() {
|
|
996
|
+
nativeProviderBoundary?.close();
|
|
997
|
+
await paneStreamSocketBoundary.close();
|
|
998
|
+
},
|
|
999
|
+
};
|
|
979
1000
|
serverOwners.attach(server);
|
|
980
1001
|
|
|
981
1002
|
try {
|
|
@@ -1526,6 +1547,8 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1526
1547
|
externalInteractionObserver.setDiagnostics(diagnostics);
|
|
1527
1548
|
setFleetFactsObserverDiagnostics(diagnostics);
|
|
1528
1549
|
}
|
|
1550
|
+
const nativeSurfaces =
|
|
1551
|
+
process.platform === "darwin" ? new NativeSurfaceProviderRegistry(instanceId) : undefined;
|
|
1529
1552
|
sessionRuntimeRegistry = new SessionRuntimeRegistry({
|
|
1530
1553
|
generation: instanceId,
|
|
1531
1554
|
...(runtimeObservability ? { observability: runtimeObservability } : {}),
|
|
@@ -1561,6 +1584,8 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1561
1584
|
publishResourceChange: (change) => broadcastResourceChanged(change, instanceId),
|
|
1562
1585
|
},
|
|
1563
1586
|
mirror: {
|
|
1587
|
+
onNativeClientPresence: (session, present) =>
|
|
1588
|
+
nativeSurfaces?.demand.presence(session, present),
|
|
1564
1589
|
splitLayoutEpoch: () => observationSelector?.nativeServerEpoch ?? null,
|
|
1565
1590
|
splitLayoutPrepare: createNativeSplitReadiness({
|
|
1566
1591
|
observation: () => observationSelector ?? null,
|
|
@@ -1762,6 +1787,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1762
1787
|
}
|
|
1763
1788
|
}
|
|
1764
1789
|
paneStreamRuntime = createPaneStreamRuntime({
|
|
1790
|
+
nativeSurfaces,
|
|
1765
1791
|
localImages,
|
|
1766
1792
|
daemonInstanceId: instanceId,
|
|
1767
1793
|
webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
|
|
@@ -1880,6 +1906,22 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1880
1906
|
return grant ? interactionEvidence.captureSourceBinding(grant) : null;
|
|
1881
1907
|
},
|
|
1882
1908
|
interactionReceipts,
|
|
1909
|
+
resolveNativeSurfaceContext: createNativeSurfaceContextResolver({
|
|
1910
|
+
generation: instanceId,
|
|
1911
|
+
registry: workspaceRegistry,
|
|
1912
|
+
catalog: createNativeTmuxServerCatalog(workspaceRegistry, fleetFactsTmuxRunner),
|
|
1913
|
+
inventory: terminalInventoryRuntime,
|
|
1914
|
+
run: (args, signal) => fleetFactsTmuxRunner(args, signal),
|
|
1915
|
+
assertOpen: () => {
|
|
1916
|
+
if (
|
|
1917
|
+
defaultEditorContextRetired ||
|
|
1918
|
+
serverOwners?.defaultRetired ||
|
|
1919
|
+
!initialNativeServerIdentity ||
|
|
1920
|
+
terminalInventoryRuntime?.lifecycleState() !== "ready"
|
|
1921
|
+
)
|
|
1922
|
+
throw new Error("Default native surface context owner is unavailable");
|
|
1923
|
+
},
|
|
1924
|
+
}),
|
|
1883
1925
|
resolvePaneEditorContext: createPaneEditorContextResolver({
|
|
1884
1926
|
generation: instanceId,
|
|
1885
1927
|
registry: workspaceRegistry,
|