tmux-ide 2.9.11 → 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 +6036 -3047
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +6 -0
- package/packages/contracts/src/pane-stream.ts +77 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +19 -3
- package/packages/contracts/src/terminal-graphics-compressed.ts +66 -0
- package/packages/contracts/src/terminal-graphics.ts +73 -0
- package/packages/contracts/src/terminal-projection.ts +153 -0
- package/packages/contracts/src/terminal-surfaces.ts +137 -0
- package/packages/core/src/fixtures/terminal-projection-vectors.json +585 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/terminal-delivery-binary.ts +160 -0
- package/packages/core/src/terminal-delivery.ts +377 -16
- package/packages/core/src/terminal-projection-vectors.mjs +82 -0
- package/packages/core/src/terminal-projection.ts +476 -0
- package/packages/core/src/terminal-replica.ts +34 -1
- 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 +71 -4
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/native-surface-context.js +96 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +10 -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/local-image-owner.js +330 -0
- package/packages/daemon/dist/terminal/graphics/local-image-store.js +255 -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 +369 -16
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +10 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +2 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +146 -16
- package/packages/daemon/dist/terminal/session-runtime/terminal-projection-delivery.js +32 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +3 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +28 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +4 -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 +81 -3
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/native-surface-context.ts +123 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +12 -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/local-image-owner.ts +346 -0
- package/packages/daemon/src/terminal/graphics/local-image-store.ts +286 -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 +446 -15
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +14 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +216 -22
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts +7 -4
- package/packages/daemon/src/terminal/session-runtime/terminal-projection-delivery.ts +59 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +3 -4
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +26 -4
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +75 -1
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { isOwnedTerminalReplicaRow } from "./terminal-replica-owned-row.ts";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES,
|
|
5
|
+
TerminalProjectionSnapshotSchemaZ,
|
|
6
|
+
TerminalProjectionPatchSchemaZ,
|
|
7
|
+
TerminalProjectionPayloadSchemaZ,
|
|
8
|
+
TerminalDeferredHistorySchemaZ,
|
|
9
|
+
TerminalProjectionEnvelopeSchemaZ,
|
|
10
|
+
TerminalProjectionReceiptSchemaZ,
|
|
11
|
+
type TerminalReplicaSnapshot,
|
|
12
|
+
type TerminalProjectionSnapshot,
|
|
13
|
+
type TerminalProjectionPatch,
|
|
14
|
+
type TerminalProjectionPayload,
|
|
15
|
+
type TerminalDeferredHistory,
|
|
16
|
+
type TerminalProjectionEnvelope,
|
|
17
|
+
type TerminalProjectionReceipt,
|
|
18
|
+
} from "@tmux-ide/contracts";
|
|
19
|
+
import {
|
|
20
|
+
hashTerminalReplicaRows,
|
|
21
|
+
hashTerminalReplicaTombstone,
|
|
22
|
+
freezeTerminalReplicaRow,
|
|
23
|
+
terminalReplicaRowsEqual,
|
|
24
|
+
} from "./terminal-replica.ts";
|
|
25
|
+
import { hashCanonicalTerminalValue } from "./terminal-replica-hash-cache.ts";
|
|
26
|
+
import {
|
|
27
|
+
BinaryTerminalWriter,
|
|
28
|
+
BinaryTerminalLimitError,
|
|
29
|
+
decodeBinaryTerminalTuple,
|
|
30
|
+
} from "./terminal-delivery-binary.ts";
|
|
31
|
+
import {
|
|
32
|
+
TerminalDeliveryStateTooLargeError,
|
|
33
|
+
writeTerminalProjectionRowCooperatively,
|
|
34
|
+
encodeTerminalProjectionMetadataTuple,
|
|
35
|
+
encodeTerminalProjectionViewportTuple,
|
|
36
|
+
decodeTerminalProjectionViewportTuple,
|
|
37
|
+
encodeTerminalProjectionPatchTuple,
|
|
38
|
+
decodeTerminalProjectionPatchTuple,
|
|
39
|
+
hashTerminalDeliveryRepresentation,
|
|
40
|
+
} from "./terminal-delivery.ts";
|
|
41
|
+
|
|
42
|
+
export function terminalHistoryCommitment(
|
|
43
|
+
snapshot: TerminalReplicaSnapshot,
|
|
44
|
+
): TerminalDeferredHistory {
|
|
45
|
+
return Object.freeze(
|
|
46
|
+
TerminalDeferredHistorySchemaZ.parse({
|
|
47
|
+
rows: snapshot.history.length,
|
|
48
|
+
digest: hashTerminalReplicaRows(snapshot.history),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export function projectAlternateSnapshot(
|
|
53
|
+
snapshot: TerminalReplicaSnapshot,
|
|
54
|
+
): TerminalProjectionSnapshot {
|
|
55
|
+
const { history: _history, ...viewport } = snapshot;
|
|
56
|
+
void _history;
|
|
57
|
+
const candidate = { ...viewport, deferredHistory: terminalHistoryCommitment(snapshot) };
|
|
58
|
+
if (snapshot.grid.every(isOwnedTerminalReplicaRow)) {
|
|
59
|
+
const metadata = projectionMetadata(candidate);
|
|
60
|
+
validateViewportShape(snapshot.grid, metadata);
|
|
61
|
+
return freezeProjection({ ...metadata, grid: snapshot.grid });
|
|
62
|
+
}
|
|
63
|
+
return freezeProjection(TerminalProjectionSnapshotSchemaZ.parse(candidate));
|
|
64
|
+
}
|
|
65
|
+
function freezeProjection(value: TerminalProjectionSnapshot): TerminalProjectionSnapshot {
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
...value,
|
|
68
|
+
grid: (Object.isFrozen(value.grid) && value.grid.every(isOwnedTerminalReplicaRow)
|
|
69
|
+
? value.grid
|
|
70
|
+
: Object.freeze(
|
|
71
|
+
value.grid.map((row) =>
|
|
72
|
+
isOwnedTerminalReplicaRow(row) ? row : freezeTerminalReplicaRow(row),
|
|
73
|
+
),
|
|
74
|
+
)) as unknown as TerminalProjectionSnapshot["grid"],
|
|
75
|
+
cursor: Object.freeze(value.cursor),
|
|
76
|
+
modes: Object.freeze(value.modes),
|
|
77
|
+
placements: Object.freeze(
|
|
78
|
+
value.placements.map((p) => Object.freeze(p)),
|
|
79
|
+
) as unknown as TerminalProjectionSnapshot["placements"],
|
|
80
|
+
bootstrap: Object.freeze(value.bootstrap),
|
|
81
|
+
deferredHistory: Object.freeze(value.deferredHistory),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const { grid: _projectionGrid, ...projectionMetadataShape } =
|
|
85
|
+
TerminalProjectionSnapshotSchemaZ.shape;
|
|
86
|
+
void _projectionGrid;
|
|
87
|
+
const ProjectionMetadataSchema = z.object(projectionMetadataShape).strict();
|
|
88
|
+
function projectionMetadata(
|
|
89
|
+
value: Omit<TerminalReplicaSnapshot, "history"> & { deferredHistory: TerminalDeferredHistory },
|
|
90
|
+
) {
|
|
91
|
+
const { grid: _grid, ...metadata } = value;
|
|
92
|
+
void _grid;
|
|
93
|
+
return ProjectionMetadataSchema.parse(metadata);
|
|
94
|
+
}
|
|
95
|
+
function validateViewportShape(
|
|
96
|
+
grid: TerminalProjectionSnapshot["grid"],
|
|
97
|
+
metadata: Omit<TerminalProjectionSnapshot, "grid">,
|
|
98
|
+
): void {
|
|
99
|
+
if (
|
|
100
|
+
grid.length !== metadata.rows ||
|
|
101
|
+
metadata.cols * metadata.rows > 1_000_000 ||
|
|
102
|
+
grid.some((row) => !row || row.cells.length !== metadata.cols) ||
|
|
103
|
+
metadata.cursor.x >= metadata.cols ||
|
|
104
|
+
metadata.cursor.y >= metadata.rows ||
|
|
105
|
+
metadata.placements.some(
|
|
106
|
+
(p) => p.row + p.rows > metadata.rows || p.column + p.columns > metadata.cols,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
throw new TypeError("Invalid projection viewport");
|
|
110
|
+
}
|
|
111
|
+
export function hashTerminalProjection(value: TerminalProjectionSnapshot): string {
|
|
112
|
+
return hashCanonicalTerminalValue([
|
|
113
|
+
"terminal-projection-v1",
|
|
114
|
+
value.cols,
|
|
115
|
+
value.rows,
|
|
116
|
+
hashTerminalReplicaRows(value.grid),
|
|
117
|
+
value.cursor,
|
|
118
|
+
value.modes,
|
|
119
|
+
value.placements,
|
|
120
|
+
value.bootstrap,
|
|
121
|
+
[value.deferredHistory.rows, value.deferredHistory.digest],
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
/** Checks the source root using its omitted-history commitment, not history materialization. */
|
|
125
|
+
export function hashSourceSnapshotFromProjection(value: TerminalProjectionSnapshot): string {
|
|
126
|
+
return hashCanonicalTerminalValue([
|
|
127
|
+
"terminal-replica-v1",
|
|
128
|
+
value.cols,
|
|
129
|
+
value.rows,
|
|
130
|
+
hashTerminalReplicaRows(value.grid),
|
|
131
|
+
value.deferredHistory.digest,
|
|
132
|
+
value.cursor,
|
|
133
|
+
value.modes,
|
|
134
|
+
value.placements,
|
|
135
|
+
value.bootstrap,
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
function projectionEncodingError(error: unknown): never {
|
|
139
|
+
if (error instanceof BinaryTerminalLimitError)
|
|
140
|
+
throw new TerminalDeliveryStateTooLargeError(TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES + 1);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
export function encodeTerminalProjection(input: TerminalProjectionPayload): Uint8Array {
|
|
144
|
+
try {
|
|
145
|
+
return encodeProjection(input);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return projectionEncodingError(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function encodeProjection(input: TerminalProjectionPayload): Uint8Array {
|
|
151
|
+
const value = TerminalProjectionPayloadSchemaZ.parse(input);
|
|
152
|
+
const writer = new BinaryTerminalWriter();
|
|
153
|
+
writer.header(
|
|
154
|
+
value.frame,
|
|
155
|
+
value.revision,
|
|
156
|
+
value.frame === "seed" ? undefined : value.baseRevision,
|
|
157
|
+
"TCP1",
|
|
158
|
+
);
|
|
159
|
+
if (value.frame === "tombstone") writer.value(value.tombstone.reason);
|
|
160
|
+
else {
|
|
161
|
+
const data = value.frame === "seed" ? value.projection : value.patch;
|
|
162
|
+
const tuple =
|
|
163
|
+
value.frame === "seed"
|
|
164
|
+
? encodeTerminalProjectionViewportTuple(value.projection)
|
|
165
|
+
: encodeTerminalProjectionPatchTuple(value.patch);
|
|
166
|
+
writer.value([...tuple, [data.deferredHistory.rows, data.deferredHistory.digest]]);
|
|
167
|
+
}
|
|
168
|
+
return writer.finish();
|
|
169
|
+
}
|
|
170
|
+
export function decodeTerminalProjection(bytes: Uint8Array): TerminalProjectionPayload {
|
|
171
|
+
const decoded = decodeBinaryTerminalTuple(bytes, "TCP1");
|
|
172
|
+
if (decoded.frame === "tombstone")
|
|
173
|
+
return TerminalProjectionPayloadSchemaZ.parse({
|
|
174
|
+
frame: decoded.frame,
|
|
175
|
+
baseRevision: decoded.baseRevision,
|
|
176
|
+
revision: decoded.revision,
|
|
177
|
+
tombstone: { reason: decoded.value },
|
|
178
|
+
});
|
|
179
|
+
const tuple = decoded.value;
|
|
180
|
+
const expected = decoded.frame === "seed" ? 8 : 7;
|
|
181
|
+
if (!Array.isArray(tuple) || tuple.length !== expected)
|
|
182
|
+
throw new TypeError("Invalid projection tuple");
|
|
183
|
+
const deferred = tuple[expected - 1];
|
|
184
|
+
if (!Array.isArray(deferred) || deferred.length !== 2)
|
|
185
|
+
throw new TypeError("Invalid deferred history tuple");
|
|
186
|
+
const deferredHistory = TerminalDeferredHistorySchemaZ.parse({
|
|
187
|
+
rows: deferred[0],
|
|
188
|
+
digest: deferred[1],
|
|
189
|
+
});
|
|
190
|
+
if (decoded.frame === "seed")
|
|
191
|
+
return {
|
|
192
|
+
frame: "seed",
|
|
193
|
+
revision: decoded.revision,
|
|
194
|
+
projection: freezeProjection(
|
|
195
|
+
TerminalProjectionSnapshotSchemaZ.parse({
|
|
196
|
+
...decodeTerminalProjectionViewportTuple(tuple.slice(0, -1)),
|
|
197
|
+
deferredHistory,
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
};
|
|
201
|
+
return TerminalProjectionPayloadSchemaZ.parse({
|
|
202
|
+
frame: "patch",
|
|
203
|
+
baseRevision: decoded.baseRevision,
|
|
204
|
+
revision: decoded.revision,
|
|
205
|
+
patch: { ...decodeTerminalProjectionPatchTuple(tuple.slice(0, -1)), deferredHistory },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
export function applyTerminalProjectionPatch(
|
|
209
|
+
current: TerminalProjectionSnapshot,
|
|
210
|
+
input: TerminalProjectionPatch,
|
|
211
|
+
): TerminalProjectionSnapshot {
|
|
212
|
+
const patch = TerminalProjectionPatchSchemaZ.parse(input);
|
|
213
|
+
const cols = patch.dimensions?.cols ?? current.cols,
|
|
214
|
+
rows = patch.dimensions?.rows ?? current.rows;
|
|
215
|
+
// A resize requires every visible row, avoiding invented blank cells or implicit history.
|
|
216
|
+
if (cols !== current.cols || rows !== current.rows) {
|
|
217
|
+
if (patch.rows.length !== rows || new Set(patch.rows.map((r) => r.index)).size !== rows)
|
|
218
|
+
throw new TypeError("Projection resize requires complete visible rows");
|
|
219
|
+
}
|
|
220
|
+
const grid = current.grid.slice(0, rows);
|
|
221
|
+
const indices = new Set<number>();
|
|
222
|
+
for (const change of patch.rows) {
|
|
223
|
+
if (indices.has(change.index) || change.index >= rows || change.row.cells.length !== cols)
|
|
224
|
+
throw new TypeError("Invalid projection row patch");
|
|
225
|
+
indices.add(change.index);
|
|
226
|
+
const old = grid[change.index];
|
|
227
|
+
grid[change.index] =
|
|
228
|
+
old && terminalReplicaRowsEqual(old, change.row) ? old : freezeTerminalReplicaRow(change.row);
|
|
229
|
+
}
|
|
230
|
+
const candidate = {
|
|
231
|
+
...current,
|
|
232
|
+
cols,
|
|
233
|
+
rows,
|
|
234
|
+
grid,
|
|
235
|
+
cursor: patch.cursor ?? current.cursor,
|
|
236
|
+
modes: patch.modes ?? current.modes,
|
|
237
|
+
placements: patch.placements ?? current.placements,
|
|
238
|
+
bootstrap: patch.bootstrap ?? current.bootstrap,
|
|
239
|
+
deferredHistory: patch.deferredHistory,
|
|
240
|
+
};
|
|
241
|
+
// Validate without using the parsed copy so unchanged immutable rows retain identity.
|
|
242
|
+
validateViewportShape(grid, projectionMetadata(candidate));
|
|
243
|
+
return freezeProjection(candidate);
|
|
244
|
+
}
|
|
245
|
+
export interface TerminalProjectionState {
|
|
246
|
+
readonly receipt: TerminalProjectionReceipt;
|
|
247
|
+
readonly projection: TerminalProjectionSnapshot | null;
|
|
248
|
+
}
|
|
249
|
+
export function hashTerminalProjectionTombstone(reason: string): string {
|
|
250
|
+
return hashCanonicalTerminalValue(["terminal-projection-tombstone-v1", reason]);
|
|
251
|
+
}
|
|
252
|
+
/** Atomic projection commit. Only this verified path creates a receipt; failures leave current intact. */
|
|
253
|
+
export function commitTerminalProjection(
|
|
254
|
+
current: TerminalProjectionState | null,
|
|
255
|
+
envelope: TerminalProjectionEnvelope,
|
|
256
|
+
bytes: Uint8Array,
|
|
257
|
+
): TerminalProjectionState {
|
|
258
|
+
const frame = TerminalProjectionEnvelopeSchemaZ.parse(envelope);
|
|
259
|
+
if (
|
|
260
|
+
bytes.length !== frame.representationBytes ||
|
|
261
|
+
hashTerminalDeliveryRepresentation(bytes) !== frame.representationHash
|
|
262
|
+
)
|
|
263
|
+
throw new TypeError("Projection representation mismatch");
|
|
264
|
+
const value = decodeTerminalProjection(bytes);
|
|
265
|
+
if (
|
|
266
|
+
value.frame !== frame.frame ||
|
|
267
|
+
value.revision !== frame.canonicalRevision ||
|
|
268
|
+
(value.frame === "seed"
|
|
269
|
+
? frame.baseRevision !== null
|
|
270
|
+
: value.baseRevision !== frame.baseRevision)
|
|
271
|
+
)
|
|
272
|
+
throw new TypeError("Projection envelope mismatch");
|
|
273
|
+
if (current) {
|
|
274
|
+
for (const key of [
|
|
275
|
+
"workspaceName",
|
|
276
|
+
"semanticPaneId",
|
|
277
|
+
"generation",
|
|
278
|
+
"incarnation",
|
|
279
|
+
"deliveryNonce",
|
|
280
|
+
] as const)
|
|
281
|
+
if (current.receipt[key] !== frame[key]) throw new TypeError("Projection lifetime mismatch");
|
|
282
|
+
if (
|
|
283
|
+
value.revision < current.receipt.canonicalRevision ||
|
|
284
|
+
(value.revision === current.receipt.canonicalRevision &&
|
|
285
|
+
(frame.canonicalStateHash !== current.receipt.canonicalStateHash ||
|
|
286
|
+
frame.projectionStateHash !== current.receipt.projectionStateHash))
|
|
287
|
+
)
|
|
288
|
+
throw new TypeError("Stale projection revision");
|
|
289
|
+
if (current.projection === null) throw new TypeError("Projection lifetime is closed");
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
value.frame !== "seed" &&
|
|
293
|
+
(!current || current.receipt.canonicalRevision !== value.baseRevision)
|
|
294
|
+
)
|
|
295
|
+
throw new TypeError("Projection baseline mismatch");
|
|
296
|
+
const projection =
|
|
297
|
+
value.frame === "seed"
|
|
298
|
+
? value.projection
|
|
299
|
+
: value.frame === "patch"
|
|
300
|
+
? applyTerminalProjectionPatch(current!.projection!, value.patch)
|
|
301
|
+
: null;
|
|
302
|
+
const projectionHash = projection
|
|
303
|
+
? hashTerminalProjection(projection)
|
|
304
|
+
: hashTerminalProjectionTombstone(value.frame === "tombstone" ? value.tombstone.reason : "");
|
|
305
|
+
const sourceHash = projection
|
|
306
|
+
? hashSourceSnapshotFromProjection(projection)
|
|
307
|
+
: hashTerminalReplicaTombstone(value.frame === "tombstone" ? value.tombstone.reason : "");
|
|
308
|
+
if (projectionHash !== frame.projectionStateHash || sourceHash !== frame.canonicalStateHash)
|
|
309
|
+
throw new TypeError("Projection state proof mismatch");
|
|
310
|
+
const receipt = TerminalProjectionReceiptSchemaZ.parse({
|
|
311
|
+
type: "terminal.projection.receipt",
|
|
312
|
+
workspaceName: frame.workspaceName,
|
|
313
|
+
semanticPaneId: frame.semanticPaneId,
|
|
314
|
+
generation: frame.generation,
|
|
315
|
+
incarnation: frame.incarnation,
|
|
316
|
+
deliveryNonce: frame.deliveryNonce,
|
|
317
|
+
transactionId: frame.transactionId,
|
|
318
|
+
canonicalRevision: frame.canonicalRevision,
|
|
319
|
+
canonicalStateHash: frame.canonicalStateHash,
|
|
320
|
+
projectionStateHash: frame.projectionStateHash,
|
|
321
|
+
representationHash: frame.representationHash,
|
|
322
|
+
});
|
|
323
|
+
return Object.freeze({ projection, receipt: Object.freeze(receipt) });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function rowCheckpoints(
|
|
327
|
+
writer: BinaryTerminalWriter,
|
|
328
|
+
options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
|
|
329
|
+
) {
|
|
330
|
+
let cells = 0,
|
|
331
|
+
bytes = writer.byteLength;
|
|
332
|
+
return async (length: number) => {
|
|
333
|
+
cells += length;
|
|
334
|
+
if (cells >= 256 || writer.byteLength - bytes >= 32 * 1024) {
|
|
335
|
+
cells = 0;
|
|
336
|
+
bytes = writer.byteLength;
|
|
337
|
+
await options.yieldControl();
|
|
338
|
+
options.signal?.throwIfAborted();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Task checkpoints bound cell validation and emission even for a single giant row. */
|
|
344
|
+
async function encodeProjectionCooperatively(
|
|
345
|
+
input: TerminalProjectionPayload,
|
|
346
|
+
options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
|
|
347
|
+
): Promise<Uint8Array> {
|
|
348
|
+
options.signal?.throwIfAborted();
|
|
349
|
+
if (input.frame === "tombstone") return encodeTerminalProjection(input);
|
|
350
|
+
if (input.frame === "patch") {
|
|
351
|
+
const { patch, ...header } = input;
|
|
352
|
+
const parsedHeader = z
|
|
353
|
+
.object({
|
|
354
|
+
frame: z.literal("patch"),
|
|
355
|
+
revision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
356
|
+
baseRevision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
357
|
+
})
|
|
358
|
+
.strict()
|
|
359
|
+
.parse(header);
|
|
360
|
+
if (parsedHeader.revision <= parsedHeader.baseRevision)
|
|
361
|
+
throw new TypeError("Projection revision must advance");
|
|
362
|
+
const { rows, ...metadata } = patch;
|
|
363
|
+
const { rows: _rowSchema, ...shape } = TerminalProjectionPatchSchemaZ.shape;
|
|
364
|
+
void _rowSchema;
|
|
365
|
+
const parsed = z.object(shape).strict().parse(metadata);
|
|
366
|
+
if (
|
|
367
|
+
!Array.isArray(rows) ||
|
|
368
|
+
rows.length > 4096 ||
|
|
369
|
+
(parsed.dimensions && (parsed.dimensions.cols > 4096 || parsed.dimensions.rows > 4096))
|
|
370
|
+
)
|
|
371
|
+
throw new TypeError("Projection patch bound");
|
|
372
|
+
let cells = 0;
|
|
373
|
+
const indices = new Set<number>();
|
|
374
|
+
const writer = new BinaryTerminalWriter();
|
|
375
|
+
writer.header("patch", parsedHeader.revision, parsedHeader.baseRevision, "TCP1");
|
|
376
|
+
writer.array(7);
|
|
377
|
+
writer.value(parsed.dimensions ? [parsed.dimensions.cols, parsed.dimensions.rows] : null);
|
|
378
|
+
writer.array(rows.length);
|
|
379
|
+
const checkpoint = rowCheckpoints(writer, options);
|
|
380
|
+
for (const entry of rows) {
|
|
381
|
+
const parsedEntry = z
|
|
382
|
+
.object({ index: z.number().int().min(0).max(4095), row: z.unknown() })
|
|
383
|
+
.strict()
|
|
384
|
+
.parse(entry);
|
|
385
|
+
if (indices.has(parsedEntry.index)) throw new TypeError("Duplicate projection row");
|
|
386
|
+
indices.add(parsedEntry.index);
|
|
387
|
+
cells += entry.row.cells.length;
|
|
388
|
+
if (cells > 1_000_000) throw new TypeError("Projection cell bound");
|
|
389
|
+
writer.array(2);
|
|
390
|
+
writer.value(parsedEntry.index);
|
|
391
|
+
await writeTerminalProjectionRowCooperatively(
|
|
392
|
+
writer,
|
|
393
|
+
entry.row,
|
|
394
|
+
parsed.dimensions?.cols ?? null,
|
|
395
|
+
options,
|
|
396
|
+
);
|
|
397
|
+
await checkpoint(entry.row.cells.length);
|
|
398
|
+
}
|
|
399
|
+
const tuple = encodeTerminalProjectionPatchTuple({ ...parsed, rows: [] });
|
|
400
|
+
writer.value(tuple[2]);
|
|
401
|
+
writer.value(tuple[3]);
|
|
402
|
+
if (tuple[4] === null) writer.value(null);
|
|
403
|
+
else {
|
|
404
|
+
const placements = tuple[4] as readonly unknown[];
|
|
405
|
+
writer.array(placements.length);
|
|
406
|
+
for (const value of placements) {
|
|
407
|
+
writer.value(value);
|
|
408
|
+
await options.yieldControl();
|
|
409
|
+
options.signal?.throwIfAborted();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
writer.value(tuple[5]);
|
|
413
|
+
writer.value([parsed.deferredHistory.rows, parsed.deferredHistory.digest]);
|
|
414
|
+
options.signal?.throwIfAborted();
|
|
415
|
+
return writer.finish();
|
|
416
|
+
}
|
|
417
|
+
const { projection, ...header } = input;
|
|
418
|
+
const parsedHeader = z
|
|
419
|
+
.object({
|
|
420
|
+
frame: z.literal("seed"),
|
|
421
|
+
revision: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
422
|
+
})
|
|
423
|
+
.strict()
|
|
424
|
+
.parse(header);
|
|
425
|
+
const { grid, ...metadata } = projection;
|
|
426
|
+
const { grid: _gridSchema, ...metadataShape } = TerminalProjectionSnapshotSchemaZ.shape;
|
|
427
|
+
void _gridSchema;
|
|
428
|
+
const parsed = z.object(metadataShape).strict().parse(metadata);
|
|
429
|
+
if (
|
|
430
|
+
!Array.isArray(grid) ||
|
|
431
|
+
grid.length !== parsed.rows ||
|
|
432
|
+
parsed.cols * parsed.rows > 1_000_000 ||
|
|
433
|
+
parsed.cursor.x >= parsed.cols ||
|
|
434
|
+
parsed.cursor.y >= parsed.rows ||
|
|
435
|
+
parsed.placements.some(
|
|
436
|
+
(p) => p.row + p.rows > parsed.rows || p.column + p.columns > parsed.cols,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
throw new TypeError("Invalid projection dimensions");
|
|
440
|
+
const writer = new BinaryTerminalWriter();
|
|
441
|
+
writer.header("seed", parsedHeader.revision, undefined, "TCP1");
|
|
442
|
+
writer.array(8);
|
|
443
|
+
writer.value(parsed.cols);
|
|
444
|
+
writer.value(parsed.rows);
|
|
445
|
+
writer.array(grid.length);
|
|
446
|
+
const checkpoint = rowCheckpoints(writer, options);
|
|
447
|
+
for (const row of grid) {
|
|
448
|
+
await writeTerminalProjectionRowCooperatively(writer, row, parsed.cols, options);
|
|
449
|
+
await checkpoint(row.cells.length);
|
|
450
|
+
}
|
|
451
|
+
const tuple = encodeTerminalProjectionMetadataTuple(parsed);
|
|
452
|
+
writer.value(tuple[0]);
|
|
453
|
+
writer.value(tuple[1]);
|
|
454
|
+
const placements = tuple[2] as readonly unknown[];
|
|
455
|
+
writer.array(placements.length);
|
|
456
|
+
for (const placement of placements) {
|
|
457
|
+
writer.value(placement);
|
|
458
|
+
await options.yieldControl();
|
|
459
|
+
options.signal?.throwIfAborted();
|
|
460
|
+
}
|
|
461
|
+
writer.value(tuple[3]);
|
|
462
|
+
writer.value([parsed.deferredHistory.rows, parsed.deferredHistory.digest]);
|
|
463
|
+
options.signal?.throwIfAborted();
|
|
464
|
+
return writer.finish();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export async function encodeTerminalProjectionCooperatively(
|
|
468
|
+
input: TerminalProjectionPayload,
|
|
469
|
+
options: { readonly yieldControl: () => Promise<void>; readonly signal?: AbortSignal },
|
|
470
|
+
): Promise<Uint8Array> {
|
|
471
|
+
try {
|
|
472
|
+
return await encodeProjectionCooperatively(input, options);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
return projectionEncodingError(error);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -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));
|
|
@@ -1048,7 +1051,7 @@ function hashTerminalReplicaRow(
|
|
|
1048
1051
|
return hash;
|
|
1049
1052
|
}
|
|
1050
1053
|
|
|
1051
|
-
function hashTerminalReplicaRows(
|
|
1054
|
+
export function hashTerminalReplicaRows(
|
|
1052
1055
|
rows: readonly TerminalReplicaRow[],
|
|
1053
1056
|
profile?: MutableTerminalReplicaApplyProfile,
|
|
1054
1057
|
instrumentation?: TerminalReplicaApplyOptions["instrumentation"],
|
|
@@ -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
|
? {
|