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
package/package.json
CHANGED
|
@@ -50,6 +50,8 @@ export * from "./session-runtime.ts";
|
|
|
50
50
|
export * from "./terminal-replica.ts";
|
|
51
51
|
export * from "./terminal-delivery.ts";
|
|
52
52
|
export * from "./terminal-graphics.ts";
|
|
53
|
+
export * from "./terminal-graphics-compressed.ts";
|
|
54
|
+
export * from "./terminal-surfaces.ts";
|
|
53
55
|
export * from "./terminal-runtime-inventory.ts";
|
|
54
56
|
export * from "./experience-shell.ts";
|
|
55
57
|
export * from "./application-shell.ts";
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TerminalSurfaceWatchSchemaZ,
|
|
3
|
+
TerminalSurfaceReceiverSchemaZ,
|
|
4
|
+
TerminalSurfaceAvailableSchemaZ,
|
|
5
|
+
TerminalSurfacePeerSchemaZ,
|
|
6
|
+
TerminalSurfaceOfferSchemaZ,
|
|
7
|
+
TerminalSurfaceRetiredSchemaZ,
|
|
8
|
+
TerminalSurfaceFallbackSchemaZ,
|
|
9
|
+
} from "./terminal-surfaces.ts";
|
|
1
10
|
import {
|
|
2
11
|
TerminalProjectionEnvelopeSchemaZ,
|
|
3
12
|
TerminalProjectionReceiptSchemaZ,
|
|
@@ -436,6 +445,10 @@ export const PaneStreamClientFrameSchemaZ = z.union([
|
|
|
436
445
|
PaneStreamTerminalGraphicsRequestFrameSchemaZ,
|
|
437
446
|
PaneStreamTerminalGraphicsLocalReleaseFrameSchemaZ,
|
|
438
447
|
PaneStreamTerminalCellPixelsFrameSchemaZ,
|
|
448
|
+
TerminalSurfaceWatchSchemaZ,
|
|
449
|
+
TerminalSurfaceReceiverSchemaZ,
|
|
450
|
+
TerminalSurfaceRetiredSchemaZ,
|
|
451
|
+
TerminalSurfaceFallbackSchemaZ,
|
|
439
452
|
]);
|
|
440
453
|
export type PaneStreamClientFrame = z.infer<typeof PaneStreamClientFrameSchemaZ>;
|
|
441
454
|
|
|
@@ -855,6 +868,10 @@ export const PaneStreamErrorFrameSchemaZ = z
|
|
|
855
868
|
.strict();
|
|
856
869
|
|
|
857
870
|
export const PaneStreamServerFrameSchemaZ = z.discriminatedUnion("type", [
|
|
871
|
+
TerminalSurfaceAvailableSchemaZ,
|
|
872
|
+
TerminalSurfacePeerSchemaZ,
|
|
873
|
+
TerminalSurfaceOfferSchemaZ,
|
|
874
|
+
TerminalSurfaceFallbackSchemaZ,
|
|
858
875
|
PaneStreamReadyFrameSchemaZ,
|
|
859
876
|
PaneStreamSeedBatchFrameSchemaZ,
|
|
860
877
|
PaneStreamOutputFrameSchemaZ,
|
|
@@ -35,7 +35,8 @@ export type TerminalDeliveryEncoding = z.infer<typeof TerminalDeliveryEncodingSc
|
|
|
35
35
|
|
|
36
36
|
export const TerminalDeliveryOfferSchemaZ = z
|
|
37
37
|
.object({
|
|
38
|
-
|
|
38
|
+
// Interactive binary/graphics offers use eight slots; native surfaces add one.
|
|
39
|
+
protocolVersions: z.array(z.number().int().positive()).min(1).max(9),
|
|
39
40
|
encodings: z.array(TerminalDeliveryEncodingSchemaZ).min(1).max(4),
|
|
40
41
|
richPlacements: z.boolean(),
|
|
41
42
|
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
TerminalGraphicsBinaryHeaderSchemaZ,
|
|
4
|
+
TERMINAL_GRAPHICS_BINARY_HEADER_BYTES,
|
|
5
|
+
TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
|
|
6
|
+
} from "./terminal-graphics.ts";
|
|
7
|
+
|
|
8
|
+
/** TGB2 uses an independent zlib stream per chunk. Digest/offset/total still
|
|
9
|
+
* describe the original image bytes. Decoders must require exact output length,
|
|
10
|
+
* stream termination and complete input consumption before hashing or painting. */
|
|
11
|
+
export const TerminalGraphicsCompressedHeaderSchemaZ = TerminalGraphicsBinaryHeaderSchemaZ.extend({
|
|
12
|
+
compression: z.literal("zlib"),
|
|
13
|
+
decodedBytes: z.number().int().min(1).max(TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES),
|
|
14
|
+
}).superRefine((header, context) => {
|
|
15
|
+
if (
|
|
16
|
+
header.blob.offset + header.decodedBytes > header.blob.total ||
|
|
17
|
+
(header.blob.format !== "rgb" && header.blob.format !== "rgba")
|
|
18
|
+
)
|
|
19
|
+
context.addIssue({ code: "custom", message: "Invalid compressed graphics range or format" });
|
|
20
|
+
});
|
|
21
|
+
export type TerminalGraphicsCompressedHeader = z.infer<
|
|
22
|
+
typeof TerminalGraphicsCompressedHeaderSchemaZ
|
|
23
|
+
>;
|
|
24
|
+
export function encodeTerminalGraphicsCompressed(
|
|
25
|
+
header: TerminalGraphicsCompressedHeader,
|
|
26
|
+
bytes: Uint8Array,
|
|
27
|
+
): Uint8Array {
|
|
28
|
+
const parsed = TerminalGraphicsCompressedHeaderSchemaZ.parse(header);
|
|
29
|
+
const json = new TextEncoder().encode(JSON.stringify(parsed));
|
|
30
|
+
if (
|
|
31
|
+
json.length > TERMINAL_GRAPHICS_BINARY_HEADER_BYTES ||
|
|
32
|
+
bytes.length < 1 ||
|
|
33
|
+
bytes.length >= parsed.decodedBytes
|
|
34
|
+
)
|
|
35
|
+
throw new Error("Invalid compressed graphics bounds");
|
|
36
|
+
const result = new Uint8Array(8 + json.length + bytes.length);
|
|
37
|
+
result.set([84, 71, 66, 50]);
|
|
38
|
+
new DataView(result.buffer).setUint32(4, json.length, false);
|
|
39
|
+
result.set(json, 8);
|
|
40
|
+
result.set(bytes, 8 + json.length);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/** Only parses the envelope; inflation belongs on the receiving worker. */
|
|
44
|
+
export function decodeTerminalGraphicsCompressed(data: Uint8Array): {
|
|
45
|
+
header: TerminalGraphicsCompressedHeader;
|
|
46
|
+
bytes: Uint8Array;
|
|
47
|
+
} {
|
|
48
|
+
if (
|
|
49
|
+
data.length < 10 ||
|
|
50
|
+
data.length > 8 + TERMINAL_GRAPHICS_BINARY_HEADER_BYTES + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES ||
|
|
51
|
+
data[0] !== 84 ||
|
|
52
|
+
data[1] !== 71 ||
|
|
53
|
+
data[2] !== 66 ||
|
|
54
|
+
data[3] !== 50
|
|
55
|
+
)
|
|
56
|
+
throw new Error("Invalid compressed graphics frame");
|
|
57
|
+
const length = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(4, false);
|
|
58
|
+
if (length < 1 || length > TERMINAL_GRAPHICS_BINARY_HEADER_BYTES || 8 + length >= data.length)
|
|
59
|
+
throw new Error("Invalid compressed graphics header");
|
|
60
|
+
const header = TerminalGraphicsCompressedHeaderSchemaZ.parse(
|
|
61
|
+
JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(8, 8 + length))),
|
|
62
|
+
);
|
|
63
|
+
const bytes = data.subarray(8 + length);
|
|
64
|
+
if (bytes.length >= header.decodedBytes) throw new Error("Invalid compressed graphics payload");
|
|
65
|
+
return { header, bytes };
|
|
66
|
+
}
|
|
@@ -18,6 +18,10 @@ import { TerminalAttachmentSemanticPaneIdSchemaZ } from "./semantic-identity.ts"
|
|
|
18
18
|
export const TERMINAL_GRAPHICS_CAPABILITY_VERSION = 101;
|
|
19
19
|
/** Raw binary blob frames; requires the graphics side channel (101). */
|
|
20
20
|
export const TERMINAL_GRAPHICS_BINARY_CAPABILITY_VERSION = 107;
|
|
21
|
+
/** Bundles 113/114 include projection 111/112 and bounded lossless raw-pixel chunks.
|
|
22
|
+
* Older daemons ignore these and retain the offered compact/text and JSON graphics fallback. */
|
|
23
|
+
export const TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION = 113;
|
|
24
|
+
export const TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION = 114;
|
|
21
25
|
/** Verified same-host file leases, including binary-blob fallback (107).
|
|
22
26
|
* Offer 108 instead of 107 to retain the existing eight-capability bound.
|
|
23
27
|
* Never infer locality from a loopback URL. */
|
|
@@ -100,6 +104,7 @@ export const TerminalGraphicsStateSchemaZ = z
|
|
|
100
104
|
cellPixels: z.literal(true).optional(),
|
|
101
105
|
preciseCellPixels: z.literal(true).optional(),
|
|
102
106
|
binaryBlobs: z.literal(true).optional(),
|
|
107
|
+
compressedBlobs: z.literal(true).optional(),
|
|
103
108
|
localFiles: z.literal(true).optional(),
|
|
104
109
|
})
|
|
105
110
|
.strict();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TerminalAttachmentSemanticPaneIdSchemaZ } from "./semantic-identity.ts";
|
|
3
|
+
import { SessionRuntimeGenerationSchemaZ } from "./session-runtime.ts";
|
|
4
|
+
|
|
5
|
+
/** Native surface control only: no pixel bytes, filesystem paths or raw Mach names.
|
|
6
|
+
* These schemas do not authenticate a peer or advertise support. They are valid
|
|
7
|
+
* for native delivery only after explicit negotiation and verified same-host
|
|
8
|
+
* authentication. A receiver request may instead get an unsupported fallback;
|
|
9
|
+
* that never authorizes endpoint lookup. SSH/unsupported peers retain the
|
|
10
|
+
* existing graphics transport.
|
|
11
|
+
*/
|
|
12
|
+
export const TERMINAL_SURFACE_MAX_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
/** Additive opt-in; does not replace binary/kitty fallback capability bundles. */
|
|
14
|
+
export const TERMINAL_SURFACE_CAPABILITY_VERSION = 116;
|
|
15
|
+
export const TERMINAL_SURFACE_MAX_OUTSTANDING = 8;
|
|
16
|
+
export const TERMINAL_SURFACE_MAX_CONTROL_BYTES = 16 * 1024;
|
|
17
|
+
const NonNilUuid = z.uuid().refine((value) => value !== "00000000-0000-0000-0000-000000000000");
|
|
18
|
+
const Sequence = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
|
19
|
+
/** Opaque full kernel audit identity, including incarnation. Values from control
|
|
20
|
+
* must still match the kernel trailer; numeric PID alone is never proof. */
|
|
21
|
+
export const TerminalSurfaceAuditTokenSchemaZ = z
|
|
22
|
+
.array(z.number().int().min(0).max(0xffff_ffff))
|
|
23
|
+
.length(8)
|
|
24
|
+
.refine((token) => token.some((word) => word !== 0));
|
|
25
|
+
const scope = {
|
|
26
|
+
version: z.literal(1),
|
|
27
|
+
generation: SessionRuntimeGenerationSchemaZ,
|
|
28
|
+
connectionNonce: NonNilUuid,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const TerminalSurfaceWatchSchemaZ = z
|
|
32
|
+
.object({
|
|
33
|
+
type: z.literal("terminal-surface-watch"),
|
|
34
|
+
version: z.literal(1),
|
|
35
|
+
generation: SessionRuntimeGenerationSchemaZ,
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
export type TerminalSurfaceWatch = z.infer<typeof TerminalSurfaceWatchSchemaZ>;
|
|
39
|
+
|
|
40
|
+
export const TerminalSurfaceAvailableSchemaZ = z
|
|
41
|
+
.object({
|
|
42
|
+
type: z.literal("terminal-surface-available"),
|
|
43
|
+
version: z.literal(1),
|
|
44
|
+
generation: SessionRuntimeGenerationSchemaZ,
|
|
45
|
+
connectionNonce: NonNilUuid.nullable(),
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
export type TerminalSurfaceAvailable = z.infer<typeof TerminalSurfaceAvailableSchemaZ>;
|
|
49
|
+
|
|
50
|
+
export const TerminalSurfaceReceiverSchemaZ = z
|
|
51
|
+
.object({
|
|
52
|
+
type: z.literal("terminal-surface-receiver"),
|
|
53
|
+
...scope,
|
|
54
|
+
pid: z.number().int().min(1).max(0x7fff_ffff),
|
|
55
|
+
slot: z.number().int().min(0).max(31),
|
|
56
|
+
auditToken: TerminalSurfaceAuditTokenSchemaZ,
|
|
57
|
+
challenge: NonNilUuid,
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
export type TerminalSurfaceReceiver = z.infer<typeof TerminalSurfaceReceiverSchemaZ>;
|
|
61
|
+
|
|
62
|
+
export const TerminalSurfacePeerSchemaZ = z
|
|
63
|
+
.object({
|
|
64
|
+
type: z.literal("terminal-surface-peer"),
|
|
65
|
+
...scope,
|
|
66
|
+
auditToken: TerminalSurfaceAuditTokenSchemaZ,
|
|
67
|
+
})
|
|
68
|
+
.strict();
|
|
69
|
+
export type TerminalSurfacePeer = z.infer<typeof TerminalSurfacePeerSchemaZ>;
|
|
70
|
+
|
|
71
|
+
/** Mirrors the Rust admission envelope. Admission additionally checks the exact
|
|
72
|
+
* retained committed pane identity before reservation and again before import.
|
|
73
|
+
* A storage ticket correlates a separately transferred capability, never a path.
|
|
74
|
+
*/
|
|
75
|
+
/** Rectangle in the committed pane's visible cell grid. History and pane
|
|
76
|
+
* replacement never inherit this placement; the canonical identity fences it. */
|
|
77
|
+
export const TerminalSurfacePlacementSchemaZ = z
|
|
78
|
+
.object({
|
|
79
|
+
row: z.number().int().min(0).max(16383),
|
|
80
|
+
column: z.number().int().min(0).max(16383),
|
|
81
|
+
rows: z.number().int().min(1).max(16384),
|
|
82
|
+
columns: z.number().int().min(1).max(16384),
|
|
83
|
+
})
|
|
84
|
+
.strict()
|
|
85
|
+
.refine((rect) => rect.row + rect.rows <= 16384 && rect.column + rect.columns <= 16384);
|
|
86
|
+
export type TerminalSurfacePlacement = z.infer<typeof TerminalSurfacePlacementSchemaZ>;
|
|
87
|
+
|
|
88
|
+
export const TerminalSurfaceOfferSchemaZ = z
|
|
89
|
+
.object({
|
|
90
|
+
type: z.literal("terminal-surface-offer"),
|
|
91
|
+
version: z.literal(1),
|
|
92
|
+
pane: TerminalAttachmentSemanticPaneIdSchemaZ,
|
|
93
|
+
generation: SessionRuntimeGenerationSchemaZ,
|
|
94
|
+
incarnation: z.string().min(1).max(256),
|
|
95
|
+
deliveryNonce: NonNilUuid,
|
|
96
|
+
canonicalRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
97
|
+
canonicalStateHash: z.string().regex(/^[0-9a-f]{16}$/u),
|
|
98
|
+
sequence: Sequence,
|
|
99
|
+
surfaceId: NonNilUuid,
|
|
100
|
+
placement: TerminalSurfacePlacementSchemaZ,
|
|
101
|
+
storageTicket: NonNilUuid,
|
|
102
|
+
format: z.literal("bgra8-straight"),
|
|
103
|
+
width: z.number().int().min(1).max(16384),
|
|
104
|
+
height: z.number().int().min(1).max(16384),
|
|
105
|
+
stride: z.number().int().min(4).max(TERMINAL_SURFACE_MAX_BYTES),
|
|
106
|
+
allocationBytes: z.number().int().min(4).max(TERMINAL_SURFACE_MAX_BYTES),
|
|
107
|
+
})
|
|
108
|
+
.strict()
|
|
109
|
+
.refine(
|
|
110
|
+
(offer) =>
|
|
111
|
+
offer.stride % 4 === 0 &&
|
|
112
|
+
offer.stride >= offer.width * 4 &&
|
|
113
|
+
offer.stride * offer.height <= offer.allocationBytes,
|
|
114
|
+
);
|
|
115
|
+
export type TerminalSurfaceOffer = z.infer<typeof TerminalSurfaceOfferSchemaZ>;
|
|
116
|
+
|
|
117
|
+
/** Sent on the original connection only after final backing/GPU ownership drops.
|
|
118
|
+
* Receipt, import, CPU decode or submission to Metal is not retirement. */
|
|
119
|
+
export const TerminalSurfaceRetiredSchemaZ = z
|
|
120
|
+
.object({
|
|
121
|
+
type: z.literal("terminal-surface-retired"),
|
|
122
|
+
...scope,
|
|
123
|
+
sequence: Sequence,
|
|
124
|
+
})
|
|
125
|
+
.strict();
|
|
126
|
+
export type TerminalSurfaceRetired = z.infer<typeof TerminalSurfaceRetiredSchemaZ>;
|
|
127
|
+
|
|
128
|
+
/** Ends this connection's native negotiation. A replacement connection starts
|
|
129
|
+
* fresh; no native storage is freed merely because fallback was requested. */
|
|
130
|
+
export const TerminalSurfaceFallbackSchemaZ = z
|
|
131
|
+
.object({
|
|
132
|
+
type: z.literal("terminal-surface-fallback"),
|
|
133
|
+
...scope,
|
|
134
|
+
reason: z.enum(["unsupported", "timeout", "identity", "budget", "storage"]),
|
|
135
|
+
})
|
|
136
|
+
.strict();
|
|
137
|
+
export type TerminalSurfaceFallback = z.infer<typeof TerminalSurfaceFallbackSchemaZ>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION,
|
|
3
|
+
TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
|
|
2
4
|
TERMINAL_PROJECTION_CAPABILITY_VERSION,
|
|
3
5
|
TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION,
|
|
4
6
|
} from "@tmux-ide/contracts";
|
|
@@ -88,7 +90,9 @@ export function negotiateTerminalDelivery(
|
|
|
88
90
|
v === TERMINAL_DELIVERY_BINARY_CAPABILITY_VERSION ||
|
|
89
91
|
v === TERMINAL_DELIVERY_BINARY_LOCAL_CAPABILITY_VERSION ||
|
|
90
92
|
v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
91
|
-
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION
|
|
93
|
+
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
|
|
94
|
+
v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
|
|
95
|
+
v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
|
|
92
96
|
) && offer.encodings.includes("semantic-compact-v1");
|
|
93
97
|
const encoding = binary
|
|
94
98
|
? ("semantic-binary-v1" as const)
|
|
@@ -112,7 +116,9 @@ export function negotiateTerminalDelivery(
|
|
|
112
116
|
offer.protocolVersions.some(
|
|
113
117
|
(v) =>
|
|
114
118
|
v === TERMINAL_PROJECTION_CAPABILITY_VERSION ||
|
|
115
|
-
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION
|
|
119
|
+
v === TERMINAL_PROJECTION_LOCAL_CAPABILITY_VERSION ||
|
|
120
|
+
v === TERMINAL_GRAPHICS_COMPRESSED_CAPABILITY_VERSION ||
|
|
121
|
+
v === TERMINAL_GRAPHICS_COMPRESSED_LOCAL_CAPABILITY_VERSION,
|
|
116
122
|
)
|
|
117
123
|
? { alternateScreenProjection: "deferred-history-v1" as const }
|
|
118
124
|
: {}),
|
|
@@ -704,6 +704,9 @@ function applyTerminalReplicaPatchProfiled(
|
|
|
704
704
|
patch.placements === undefined &&
|
|
705
705
|
patch.bootstrap === undefined;
|
|
706
706
|
if (unchanged) return current;
|
|
707
|
+
Object.freeze(grid);
|
|
708
|
+
if (grid !== current.grid && cols === current.cols && rows === current.rows)
|
|
709
|
+
registerGridPatchHash(current.grid, grid, preparedRows, profile, instrumentation);
|
|
707
710
|
let history: readonly TerminalReplicaRow[];
|
|
708
711
|
if (patch.history) {
|
|
709
712
|
history = Object.freeze(patch.history.map(freezeRow));
|
|
@@ -1086,6 +1089,36 @@ export async function primeTerminalReplicaRowsHashCooperatively(
|
|
|
1086
1089
|
ROW_ARRAY_HASH_CACHE.set(rows, { hash, length: rows.length });
|
|
1087
1090
|
}
|
|
1088
1091
|
|
|
1092
|
+
function registerGridPatchHash(
|
|
1093
|
+
previous: readonly TerminalReplicaRow[],
|
|
1094
|
+
next: readonly TerminalReplicaRow[],
|
|
1095
|
+
changes: readonly { readonly index: number; readonly row: TerminalReplicaRow }[],
|
|
1096
|
+
profile?: MutableTerminalReplicaApplyProfile,
|
|
1097
|
+
instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
|
|
1098
|
+
): void {
|
|
1099
|
+
const prior = ROW_ARRAY_HASH_CACHE.get(previous);
|
|
1100
|
+
// A cached sequence proves that every retained row is deeply immutable.
|
|
1101
|
+
// Without that proof (including cold or external snapshots), hash normally.
|
|
1102
|
+
// Dense scrolling is cheaper as one linear fold than repeated polynomial
|
|
1103
|
+
// replacements; preserve that path when more than a quarter of rows change.
|
|
1104
|
+
if (!prior || changes.length > previous.length / 4) return;
|
|
1105
|
+
let hash = prior.hash;
|
|
1106
|
+
for (const { index } of changes) {
|
|
1107
|
+
const row = next[index]!;
|
|
1108
|
+
if (row === previous[index]) continue;
|
|
1109
|
+
if (!isTerminalReplicaRowDeeplyFrozen(row)) return;
|
|
1110
|
+
const before = BigInt(`0x${hashTerminalReplicaRow(previous[index]!)}`);
|
|
1111
|
+
const after = BigInt(`0x${hashTerminalReplicaRow(row, profile, instrumentation)}`);
|
|
1112
|
+
hash = BigInt.asUintN(
|
|
1113
|
+
64,
|
|
1114
|
+
hash + (after - before) * pow64(ROW_SEQUENCE_BASE, prior.length - 1 - index),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
// applyTerminalReplicaPatch owns/froze this array and validates unique indices.
|
|
1118
|
+
// The polynomial is unchanged: replace only each changed row's contribution.
|
|
1119
|
+
ROW_ARRAY_HASH_CACHE.set(next, { hash, length: next.length });
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1089
1122
|
export function registerTerminalReplicaRowsDeltaHash(
|
|
1090
1123
|
previous: readonly TerminalReplicaRow[],
|
|
1091
1124
|
next: readonly TerminalReplicaRow[],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mountSavedMachineWatch } from "./saved-machine-watch.js";
|
|
1
2
|
import { bodyLimit } from "hono/body-limit";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { SavedMachineRegistrySchema } from "@tmux-ide/contracts";
|
|
@@ -13,6 +14,7 @@ const MutationRequest = z.strictObject({
|
|
|
13
14
|
change: SavedMachineMutationSchema,
|
|
14
15
|
});
|
|
15
16
|
export function mountSavedMachineRoute(app, options) {
|
|
17
|
+
mountSavedMachineWatch(app, options);
|
|
16
18
|
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
17
19
|
whenOwnerless: "unavailable",
|
|
18
20
|
unavailableMessage: "Machine registry unavailable",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SavedMachineRegistrySchema, } from "@tmux-ide/contracts";
|
|
4
|
+
import { loadSavedMachines, watchSavedMachines } from "../../lib/saved-machines.js";
|
|
5
|
+
import { ownerAuthorityGate } from "../owner-authority.js";
|
|
6
|
+
export const SAVED_MACHINE_WATCH_WAIT_MS = 20_000;
|
|
7
|
+
const MAX_WATCHERS = 32;
|
|
8
|
+
const Query = z.strictObject({
|
|
9
|
+
expectedInstanceId: z.uuid(),
|
|
10
|
+
after: z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
13
|
+
.optional(),
|
|
14
|
+
});
|
|
15
|
+
function snapshot(value) {
|
|
16
|
+
const registry = SavedMachineRegistrySchema.parse(value);
|
|
17
|
+
return {
|
|
18
|
+
registry,
|
|
19
|
+
revision: createHash("sha256").update(JSON.stringify(registry)).digest("hex"),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** One bounded notification request. A new request always observes the latest registry. */
|
|
23
|
+
export function mountSavedMachineWatch(app, options) {
|
|
24
|
+
const authorize = ownerAuthorityGate(options.ownerToken, {
|
|
25
|
+
whenOwnerless: "unavailable",
|
|
26
|
+
unavailableMessage: "Machine registry unavailable",
|
|
27
|
+
mismatchMessage: "Machine registry requires owner authority",
|
|
28
|
+
});
|
|
29
|
+
const read = options.read ?? loadSavedMachines;
|
|
30
|
+
const watch = options.watch ?? watchSavedMachines;
|
|
31
|
+
let active = 0;
|
|
32
|
+
app.get("/api/resources/saved-machines/watch", async (c) => {
|
|
33
|
+
const gate = authorize(c);
|
|
34
|
+
if (gate)
|
|
35
|
+
return gate;
|
|
36
|
+
c.header("Cache-Control", "no-store");
|
|
37
|
+
const query = Query.safeParse(c.req.query());
|
|
38
|
+
if (!query.success)
|
|
39
|
+
return c.json({ error: "Invalid registry watch request" }, 400);
|
|
40
|
+
if (query.data.expectedInstanceId !== options.daemon.instanceId)
|
|
41
|
+
return c.json({ error: "Daemon generation changed" }, 409);
|
|
42
|
+
if (active >= MAX_WATCHERS)
|
|
43
|
+
return c.json({ error: "Registry watch capacity reached" }, 429);
|
|
44
|
+
active++;
|
|
45
|
+
try {
|
|
46
|
+
const initial = snapshot(read());
|
|
47
|
+
if (query.data.after !== initial.revision)
|
|
48
|
+
return c.json({ daemon: options.daemon, ...initial });
|
|
49
|
+
const signal = c.req.raw.signal;
|
|
50
|
+
const result = await new Promise((resolve) => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
let stop = () => { };
|
|
53
|
+
const finish = (result) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
signal.removeEventListener("abort", aborted);
|
|
59
|
+
try {
|
|
60
|
+
stop();
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
resolve(result);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const aborted = () => finish({ status: 408 });
|
|
67
|
+
const timer = setTimeout(() => finish({ status: 200, revision: initial.revision, registry: null }), SAVED_MACHINE_WATCH_WAIT_MS);
|
|
68
|
+
timer.unref();
|
|
69
|
+
const changed = (value) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
const next = snapshot(value);
|
|
74
|
+
if (next.revision !== initial.revision)
|
|
75
|
+
finish({ status: 200, ...next });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
finish({ status: 503 });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
82
|
+
if (signal.aborted) {
|
|
83
|
+
aborted();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
stop = watch(changed, () => finish({ status: 503 }));
|
|
88
|
+
// Watch implementations may synchronously report an initial error/change.
|
|
89
|
+
if (settled) {
|
|
90
|
+
stop();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Close the read-before-subscribe race independently of watcher behavior.
|
|
94
|
+
changed(read());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
finish({ status: 503 });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
if (result.status !== 200)
|
|
101
|
+
return c.json({
|
|
102
|
+
error: result.status === 408 ? "Registry watch cancelled" : "Machine registry unavailable",
|
|
103
|
+
}, result.status);
|
|
104
|
+
return c.json({
|
|
105
|
+
daemon: options.daemon,
|
|
106
|
+
revision: result.revision,
|
|
107
|
+
registry: result.registry,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return c.json({ error: "Machine registry unavailable" }, 503);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
active--;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TERMINAL_SURFACE_CAPABILITY_VERSION } from "@tmux-ide/contracts";
|
|
1
2
|
import { mountAutomationRoutes } from "./automation.js";
|
|
2
3
|
import { mountTmuxServerRoutes } from "./tmux-servers.js";
|
|
3
4
|
import { streamBoundedLogs } from "./log-stream.js";
|
|
@@ -422,6 +423,18 @@ export function createApp(options = {}) {
|
|
|
422
423
|
status: "ok",
|
|
423
424
|
daemon: daemonInstanceIdentity,
|
|
424
425
|
capabilities: {
|
|
426
|
+
...(c.req.query("nativeSurfaces") === "1"
|
|
427
|
+
? {
|
|
428
|
+
nativeSurfaces: {
|
|
429
|
+
supported: !!options.paneStreamIssueBackend &&
|
|
430
|
+
options.nativeSurfaceProviderSupported?.() === true,
|
|
431
|
+
available: options.paneStreamIssueBackend !== undefined &&
|
|
432
|
+
options.paneStreamIssueBackend !== null &&
|
|
433
|
+
options.nativeSurfaceProviderAvailable?.() === true,
|
|
434
|
+
protocolVersion: TERMINAL_SURFACE_CAPABILITY_VERSION,
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
: {}),
|
|
425
438
|
// Opt-in keeps the response compatible with older strict host schemas.
|
|
426
439
|
...(c.req.query("windowViewport") === "1"
|
|
427
440
|
? {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeSurfaceLocationSchema } from "../lib/native-surface-context.js";
|
|
1
2
|
import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.js";
|
|
2
3
|
import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.js";
|
|
3
4
|
import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.js";
|
|
@@ -191,6 +192,14 @@ export function mountTmuxServerRoutes(app, options) {
|
|
|
191
192
|
}),
|
|
192
193
|
}));
|
|
193
194
|
}));
|
|
195
|
+
app.post(`${scoped}/native-surface-context`, route(async (c) => {
|
|
196
|
+
const server = scope(c);
|
|
197
|
+
const target = NativeSurfaceLocationSchema.parse(await boundedJson(c.req.raw, 8192));
|
|
198
|
+
c.req.raw.signal.throwIfAborted();
|
|
199
|
+
const context = await options.owners.withOwner(server, (owner) => owner.resolveNativeSurfaceContext(target, c.req.raw.signal));
|
|
200
|
+
c.req.raw.signal.throwIfAborted();
|
|
201
|
+
return c.json({ version: 1, server, context });
|
|
202
|
+
}));
|
|
194
203
|
app.get(`${scoped}/inventory/:workspaceName`, route(async (c) => {
|
|
195
204
|
const server = scope(c);
|
|
196
205
|
const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
|
|
@@ -6,6 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { createNativeSurfaceContextResolver } from "./native-surface-context.js";
|
|
10
|
+
import { NativeSurfaceProviderRegistry } from "../terminal/graphics/native-surface-provider.js";
|
|
9
11
|
import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
|
|
10
12
|
import { createNativeSplitCapabilityProbe, createNativeSplitReadiness, } from "./native-split-capability.js";
|
|
11
13
|
import { executeCanonicalSplitMutation } from "./canonical-split-mutation.js";
|
|
@@ -80,6 +82,7 @@ import { InteractionEvidenceAuthority } from "./interaction-evidence-authority.j
|
|
|
80
82
|
import { createNativeTerminalAttachmentRuntime, WorkspaceTerminalInventoryRuntime, } from "../terminal/attachments/native-runtime.js";
|
|
81
83
|
import { createTmuxAgentStatusProbe } from "../terminal/attachments/agent-status-probe.js";
|
|
82
84
|
import { attachTerminalAttachmentWebSocket, } from "../server/terminal-attachment-upgrade.js";
|
|
85
|
+
import { attachNativeSurfaceProvider } from "../server/native-surface-provider-upgrade.js";
|
|
83
86
|
import { attachPaneStreamWebSocket, } from "../server/pane-stream-upgrade.js";
|
|
84
87
|
import { createPaneStreamRuntime, } from "../terminal/pane-stream/runtime.js";
|
|
85
88
|
import { SessionRuntimeRegistry } from "../terminal/session-runtime/registry.js";
|
|
@@ -543,6 +546,8 @@ async function startHttpServer({ sessionName, requestedPort, bindHostname, dir,
|
|
|
543
546
|
issue: (request, context) => getTerminalAttachmentRuntime().admission.issue(request, context),
|
|
544
547
|
},
|
|
545
548
|
paneStreamIssueBackend: paneStreamRuntime.coordinator,
|
|
549
|
+
nativeSurfaceProviderSupported: () => serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces !== undefined,
|
|
550
|
+
nativeSurfaceProviderAvailable: () => serverOwners.isDefaultCurrent() && paneStreamRuntime.nativeSurfaces?.available === true,
|
|
546
551
|
applicationShellInventoryBackend: terminalInventoryRuntime,
|
|
547
552
|
startupReadinessAttachmentBackend: terminalInventoryRuntime,
|
|
548
553
|
catalogLiveSessions,
|
|
@@ -586,7 +591,16 @@ async function startHttpServer({ sessionName, requestedPort, bindHostname, dir,
|
|
|
586
591
|
});
|
|
587
592
|
const terminalAttachmentBoundary = attachTerminalAttachmentWebSocket(server, (create) => (create ? getTerminalAttachmentRuntime() : peekTerminalAttachmentRuntime())?.admission ??
|
|
588
593
|
null);
|
|
589
|
-
const
|
|
594
|
+
const paneStreamSocketBoundary = attachPaneStreamWebSocket(server, paneStreamRuntime.coordinator, PANE_STREAM_REDEEM_PATH, () => serverOwners.isDefaultCurrent());
|
|
595
|
+
const nativeProviderBoundary = paneStreamRuntime.nativeSurfaces
|
|
596
|
+
? attachNativeSurfaceProvider(server, paneStreamRuntime.nativeSurfaces, localBypassToken ?? null, () => serverOwners.isDefaultCurrent())
|
|
597
|
+
: null;
|
|
598
|
+
const paneStreamBoundary = {
|
|
599
|
+
async close() {
|
|
600
|
+
nativeProviderBoundary?.close();
|
|
601
|
+
await paneStreamSocketBoundary.close();
|
|
602
|
+
},
|
|
603
|
+
};
|
|
590
604
|
serverOwners.attach(server);
|
|
591
605
|
try {
|
|
592
606
|
await new Promise((resolve, reject) => {
|
|
@@ -1059,6 +1073,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1059
1073
|
externalInteractionObserver.setDiagnostics(diagnostics);
|
|
1060
1074
|
setFleetFactsObserverDiagnostics(diagnostics);
|
|
1061
1075
|
}
|
|
1076
|
+
const nativeSurfaces = process.platform === "darwin" ? new NativeSurfaceProviderRegistry(instanceId) : undefined;
|
|
1062
1077
|
sessionRuntimeRegistry = new SessionRuntimeRegistry({
|
|
1063
1078
|
generation: instanceId,
|
|
1064
1079
|
...(runtimeObservability ? { observability: runtimeObservability } : {}),
|
|
@@ -1085,6 +1100,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1085
1100
|
publishResourceChange: (change) => broadcastResourceChanged(change, instanceId),
|
|
1086
1101
|
},
|
|
1087
1102
|
mirror: {
|
|
1103
|
+
onNativeClientPresence: (session, present) => nativeSurfaces?.demand.presence(session, present),
|
|
1088
1104
|
splitLayoutEpoch: () => observationSelector?.nativeServerEpoch ?? null,
|
|
1089
1105
|
splitLayoutPrepare: createNativeSplitReadiness({
|
|
1090
1106
|
observation: () => observationSelector ?? null,
|
|
@@ -1249,6 +1265,7 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1249
1265
|
}
|
|
1250
1266
|
}
|
|
1251
1267
|
paneStreamRuntime = createPaneStreamRuntime({
|
|
1268
|
+
nativeSurfaces,
|
|
1252
1269
|
localImages,
|
|
1253
1270
|
daemonInstanceId: instanceId,
|
|
1254
1271
|
webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
|
|
@@ -1353,6 +1370,20 @@ async function startEmbeddedDaemonGeneration(opts) {
|
|
|
1353
1370
|
return grant ? interactionEvidence.captureSourceBinding(grant) : null;
|
|
1354
1371
|
},
|
|
1355
1372
|
interactionReceipts,
|
|
1373
|
+
resolveNativeSurfaceContext: createNativeSurfaceContextResolver({
|
|
1374
|
+
generation: instanceId,
|
|
1375
|
+
registry: workspaceRegistry,
|
|
1376
|
+
catalog: createNativeTmuxServerCatalog(workspaceRegistry, fleetFactsTmuxRunner),
|
|
1377
|
+
inventory: terminalInventoryRuntime,
|
|
1378
|
+
run: (args, signal) => fleetFactsTmuxRunner(args, signal),
|
|
1379
|
+
assertOpen: () => {
|
|
1380
|
+
if (defaultEditorContextRetired ||
|
|
1381
|
+
serverOwners?.defaultRetired ||
|
|
1382
|
+
!initialNativeServerIdentity ||
|
|
1383
|
+
terminalInventoryRuntime?.lifecycleState() !== "ready")
|
|
1384
|
+
throw new Error("Default native surface context owner is unavailable");
|
|
1385
|
+
},
|
|
1386
|
+
}),
|
|
1356
1387
|
resolvePaneEditorContext: createPaneEditorContextResolver({
|
|
1357
1388
|
generation: instanceId,
|
|
1358
1389
|
registry: workspaceRegistry,
|