tmux-ide 2.9.11 → 2.9.12
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 +4158 -2545
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +60 -0
- package/packages/contracts/src/terminal-delivery-binary-chunk.ts +55 -0
- package/packages/contracts/src/terminal-delivery.ts +17 -2
- package/packages/contracts/src/terminal-graphics.ts +68 -0
- package/packages/contracts/src/terminal-projection.ts +153 -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 +371 -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 +1 -1
- package/packages/daemon/dist/lib/daemon-embed.js +39 -3
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +1 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -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/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/pane-stream/pane-stream-websocket.js +176 -16
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -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/lib/daemon-embed.ts +38 -2
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +3 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +3 -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/pane-stream/pane-stream-websocket.ts +219 -15
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- 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
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { prepareProjection, type ProjectionBaseline } from "./terminal-projection-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
TerminalProjectionEnvelopeSchemaZ,
|
|
4
|
+
TerminalProjectionReceiptSchemaZ,
|
|
5
|
+
TerminalProjectionMaterializeSchemaZ,
|
|
6
|
+
type TerminalProjectionEnvelope,
|
|
7
|
+
type TerminalProjectionReceipt,
|
|
8
|
+
type TerminalProjectionMaterialize,
|
|
9
|
+
type TerminalProjectionPayload,
|
|
10
|
+
} from "@tmux-ide/contracts";
|
|
11
|
+
import { encodeTerminalProjectionCooperatively } from "@tmux-ide/core";
|
|
12
|
+
type DeliveryEnvelope = TerminalDeliveryEnvelope | TerminalProjectionEnvelope;
|
|
1
13
|
import type { KittyImage } from "../graphics/kitty-graphics.ts";
|
|
2
14
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
3
15
|
import {
|
|
@@ -37,6 +49,8 @@ import {
|
|
|
37
49
|
applyTerminalReplicaUpdate,
|
|
38
50
|
applyTerminalReplicaUpdateCooperatively,
|
|
39
51
|
terminalReplicaUpdateNeedsCooperativeReduction,
|
|
52
|
+
encodeBinarySemanticTerminalUpdate,
|
|
53
|
+
encodeBinarySemanticTerminalUpdateCooperatively,
|
|
40
54
|
encodeCompactSemanticTerminalUpdate,
|
|
41
55
|
encodeCompactSemanticTerminalUpdateCooperatively,
|
|
42
56
|
terminalSemanticUpdateNeedsCooperativeEncoding,
|
|
@@ -124,6 +138,8 @@ export interface TerminalDeliveryConnection {
|
|
|
124
138
|
readonly closed: Promise<"closed" | "sink-failed">;
|
|
125
139
|
readonly negotiation: TerminalDeliveryNegotiationResult;
|
|
126
140
|
ack(ack: TerminalDeliveryAck): void;
|
|
141
|
+
projectionReceipt?(receipt: TerminalProjectionReceipt): void;
|
|
142
|
+
materialize?(request: TerminalProjectionMaterialize): void;
|
|
127
143
|
nack(nack: TerminalDeliveryNack): void;
|
|
128
144
|
setVisibility(visibility: TerminalDeliveryVisibility): void;
|
|
129
145
|
close(): Promise<void>;
|
|
@@ -205,9 +221,14 @@ interface ClientState {
|
|
|
205
221
|
visibility: TerminalDeliveryVisibility;
|
|
206
222
|
baselineRevision: number;
|
|
207
223
|
baselineHash: string | null;
|
|
224
|
+
projectionBaseline: ProjectionBaseline | null;
|
|
225
|
+
materializeRequested: boolean;
|
|
226
|
+
lastProjectionReceipt: TerminalProjectionReceipt | null;
|
|
227
|
+
lastMaterialize: TerminalProjectionMaterialize | null;
|
|
208
228
|
reseedRequired: boolean;
|
|
209
229
|
inFlight: {
|
|
210
|
-
envelope:
|
|
230
|
+
envelope: DeliveryEnvelope;
|
|
231
|
+
projection: ProjectionBaseline | null;
|
|
211
232
|
deliveryOrdinal: number;
|
|
212
233
|
bytes: Uint8Array;
|
|
213
234
|
nextChunk: number;
|
|
@@ -226,7 +247,7 @@ interface ClientState {
|
|
|
226
247
|
captureUnavailable: boolean;
|
|
227
248
|
lastAck: TerminalDeliveryAck | null;
|
|
228
249
|
/** Delivery displaced by authoritative source close; its racing ACK is benign. */
|
|
229
|
-
sourceClosedFlight:
|
|
250
|
+
sourceClosedFlight: DeliveryEnvelope | null;
|
|
230
251
|
backgroundTimer: SessionRuntimeTimer | null;
|
|
231
252
|
}
|
|
232
253
|
|
|
@@ -246,6 +267,11 @@ interface PendingClientEncoding {
|
|
|
246
267
|
readonly reseedRequired: boolean;
|
|
247
268
|
readonly traceStarted: number;
|
|
248
269
|
}
|
|
270
|
+
class DeferredProjectionEncoding extends Error {
|
|
271
|
+
constructor(readonly payload: TerminalProjectionPayload) {
|
|
272
|
+
super("Deferred projection encoding");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
249
275
|
class DeferredSeedEncoding extends Error {
|
|
250
276
|
constructor(readonly payload: TerminalSemanticDeliveryPayload) {
|
|
251
277
|
super("Deferred compact seed encoding");
|
|
@@ -254,10 +280,16 @@ class DeferredSeedEncoding extends Error {
|
|
|
254
280
|
|
|
255
281
|
interface CachedRepresentation {
|
|
256
282
|
readonly bytes: Uint8Array;
|
|
257
|
-
readonly encoding?:
|
|
283
|
+
readonly encoding?:
|
|
284
|
+
| "semantic-v1"
|
|
285
|
+
| "semantic-compact-v1"
|
|
286
|
+
| "semantic-binary-v1"
|
|
287
|
+
| "alternate-projection-binary-v1";
|
|
288
|
+
readonly projection?: ProjectionBaseline;
|
|
289
|
+
readonly projectionBaseRevision?: number | null;
|
|
258
290
|
readonly frame: "seed" | "patch" | "tombstone";
|
|
259
291
|
readonly canonicalEquivalent: boolean;
|
|
260
|
-
readonly history: "complete" | "truncated" | "not-applicable";
|
|
292
|
+
readonly history: "complete" | "truncated" | "not-applicable" | "deferred";
|
|
261
293
|
readonly representationHash?: string;
|
|
262
294
|
/** Internal reachability metadata; never serialized onto the wire. */
|
|
263
295
|
readonly cachePaneId?: string;
|
|
@@ -274,7 +306,7 @@ interface CachedRepresentation {
|
|
|
274
306
|
attemptedLegacySeedSizeCapped: boolean;
|
|
275
307
|
attemptedCompactPatchBytes: number | null;
|
|
276
308
|
attemptedCompactSeedBytes: number | null;
|
|
277
|
-
selectedEncoding: "semantic-v1" | "semantic-compact-v1";
|
|
309
|
+
selectedEncoding: "semantic-v1" | "semantic-compact-v1" | "semantic-binary-v1";
|
|
278
310
|
selectionStatus:
|
|
279
311
|
| "patch-preferred"
|
|
280
312
|
| "seed-preferred"
|
|
@@ -432,6 +464,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
432
464
|
visibility: "visible",
|
|
433
465
|
baselineRevision: -1,
|
|
434
466
|
baselineHash: null,
|
|
467
|
+
projectionBaseline: null,
|
|
468
|
+
materializeRequested: false,
|
|
469
|
+
lastProjectionReceipt: null,
|
|
470
|
+
lastMaterialize: null,
|
|
435
471
|
reseedRequired: false,
|
|
436
472
|
inFlight: null,
|
|
437
473
|
latestRevision: pane.latest?.update.revision ?? null,
|
|
@@ -456,6 +492,8 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
456
492
|
negotiation,
|
|
457
493
|
closed,
|
|
458
494
|
ack: (ack) => this.#ack(client, ack),
|
|
495
|
+
projectionReceipt: (receipt) => this.#projectionReceipt(client, receipt),
|
|
496
|
+
materialize: (request) => this.#materialize(client, request),
|
|
459
497
|
nack: (nack) => this.#nack(client, nack),
|
|
460
498
|
setVisibility: (visibilityInput) => {
|
|
461
499
|
if (client.closed) return;
|
|
@@ -956,7 +994,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
956
994
|
client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
|
|
957
995
|
if (
|
|
958
996
|
update.type === "terminal.tombstone" &&
|
|
959
|
-
(client.inFlight !== null ||
|
|
997
|
+
(client.inFlight !== null ||
|
|
998
|
+
client.encoding !== null ||
|
|
999
|
+
client.visibility !== "visible" ||
|
|
1000
|
+
client.projectionBaseline !== null)
|
|
960
1001
|
) {
|
|
961
1002
|
this.#fault(client, "source-closed", "Terminal source closed before final state delivery");
|
|
962
1003
|
continue;
|
|
@@ -1079,15 +1120,22 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1079
1120
|
let deferred = false;
|
|
1080
1121
|
let encodedRepresentation: CachedRepresentation | null = null;
|
|
1081
1122
|
let failedSelectionObservation: SemanticSelectionObservation | null = null;
|
|
1082
|
-
let deliveryEnvelope:
|
|
1123
|
+
let deliveryEnvelope: DeliveryEnvelope | null = null;
|
|
1083
1124
|
let deliveryOrdinal: number | null = null;
|
|
1084
1125
|
try {
|
|
1085
1126
|
const representation = this.#representation(client, pane, target, completed?.outcome);
|
|
1086
1127
|
encodedRepresentation = representation;
|
|
1087
1128
|
const transactionId = this.#scheduler.createId();
|
|
1088
1129
|
const chunkCount = Math.max(1, Math.ceil(representation.bytes.byteLength / (256 * 1024)));
|
|
1089
|
-
const envelope =
|
|
1090
|
-
|
|
1130
|
+
const envelope = (
|
|
1131
|
+
representation.projection
|
|
1132
|
+
? TerminalProjectionEnvelopeSchemaZ
|
|
1133
|
+
: TerminalDeliveryEnvelopeSchemaZ
|
|
1134
|
+
).parse({
|
|
1135
|
+
type: representation.projection ? "terminal.projection" : "terminal.delivery",
|
|
1136
|
+
...(representation.projection
|
|
1137
|
+
? { projectionStateHash: representation.projection.hash }
|
|
1138
|
+
: {}),
|
|
1091
1139
|
workspaceName: client.deliveryWorkspaceName,
|
|
1092
1140
|
semanticPaneId: target.update.semanticPaneId,
|
|
1093
1141
|
generation: target.update.generation,
|
|
@@ -1098,7 +1146,11 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1098
1146
|
protocolVersion: 1,
|
|
1099
1147
|
encoding: representation.encoding ?? client.negotiated.encoding,
|
|
1100
1148
|
frame: representation.frame,
|
|
1101
|
-
baseRevision: representation.
|
|
1149
|
+
baseRevision: representation.projection
|
|
1150
|
+
? representation.projectionBaseRevision
|
|
1151
|
+
: representation.frame === "seed"
|
|
1152
|
+
? null
|
|
1153
|
+
: Math.max(0, client.baselineRevision),
|
|
1102
1154
|
canonicalRevision: target.update.revision,
|
|
1103
1155
|
canonicalStateHash: target.update.stateHash,
|
|
1104
1156
|
representationHash:
|
|
@@ -1115,6 +1167,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1115
1167
|
registerTerminalDeliveryObservationOrdinal(envelope, deliveryOrdinal);
|
|
1116
1168
|
client.inFlight = {
|
|
1117
1169
|
envelope,
|
|
1170
|
+
projection: representation.projection ?? null,
|
|
1118
1171
|
deliveryOrdinal,
|
|
1119
1172
|
bytes: representation.bytes,
|
|
1120
1173
|
nextChunk: 0,
|
|
@@ -1125,9 +1178,16 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1125
1178
|
if (target.trace?.traceId === pane.pendingDeliveryTrace?.traceId)
|
|
1126
1179
|
pane.pendingDeliveryTrace = null;
|
|
1127
1180
|
} catch (error) {
|
|
1128
|
-
if (error instanceof DeferredSeedEncoding) {
|
|
1181
|
+
if (error instanceof DeferredSeedEncoding || error instanceof DeferredProjectionEncoding) {
|
|
1129
1182
|
deferred = true;
|
|
1130
|
-
this.#startEncoding(
|
|
1183
|
+
this.#startEncoding(
|
|
1184
|
+
client,
|
|
1185
|
+
pane,
|
|
1186
|
+
target,
|
|
1187
|
+
error.payload,
|
|
1188
|
+
traceStarted,
|
|
1189
|
+
error instanceof DeferredProjectionEncoding,
|
|
1190
|
+
);
|
|
1131
1191
|
return;
|
|
1132
1192
|
}
|
|
1133
1193
|
failedSelectionObservation = semanticSelectionObservationFromError(error);
|
|
@@ -1205,8 +1265,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1205
1265
|
client: ClientState,
|
|
1206
1266
|
pane: PaneState,
|
|
1207
1267
|
target: RevisionRecord,
|
|
1208
|
-
payload: TerminalSemanticDeliveryPayload,
|
|
1268
|
+
payload: TerminalSemanticDeliveryPayload | TerminalProjectionPayload,
|
|
1209
1269
|
traceStarted: number,
|
|
1270
|
+
projection = false,
|
|
1210
1271
|
): void {
|
|
1211
1272
|
const key = cacheKey([
|
|
1212
1273
|
target.update.workspaceName,
|
|
@@ -1217,6 +1278,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1217
1278
|
target.update.stateHash,
|
|
1218
1279
|
client.negotiated.encoding,
|
|
1219
1280
|
client.negotiated.richPlacements ? "rich" : "plain",
|
|
1281
|
+
projection
|
|
1282
|
+
? `projection:${client.projectionBaseline?.revision ?? -1}:${client.projectionBaseline?.hash ?? "none"}`
|
|
1283
|
+
: "complete",
|
|
1220
1284
|
]);
|
|
1221
1285
|
let job = this.#pendingEncodings.get(key);
|
|
1222
1286
|
let created = false;
|
|
@@ -1250,10 +1314,19 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1250
1314
|
};
|
|
1251
1315
|
owned.clients.add(client);
|
|
1252
1316
|
if (!created) return;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1317
|
+
const options = { signal: owned.abort.signal, yieldControl: () => this.#yieldEncoding(owned) };
|
|
1318
|
+
const result = projection
|
|
1319
|
+
? encodeTerminalProjectionCooperatively(payload as TerminalProjectionPayload, options)
|
|
1320
|
+
: client.negotiated.encoding === "semantic-binary-v1"
|
|
1321
|
+
? encodeBinarySemanticTerminalUpdateCooperatively(
|
|
1322
|
+
payload as TerminalSemanticDeliveryPayload,
|
|
1323
|
+
options,
|
|
1324
|
+
)
|
|
1325
|
+
: encodeCompactSemanticTerminalUpdateCooperatively(
|
|
1326
|
+
payload as TerminalSemanticDeliveryPayload,
|
|
1327
|
+
options,
|
|
1328
|
+
);
|
|
1329
|
+
void result.then(
|
|
1257
1330
|
(bytes) => this.#completeEncoding(owned, { bytes }),
|
|
1258
1331
|
(error) => this.#completeEncoding(owned, { error }),
|
|
1259
1332
|
);
|
|
@@ -1328,6 +1401,32 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1328
1401
|
target: RevisionRecord,
|
|
1329
1402
|
seedOutcome?: SeedEncodingOutcome,
|
|
1330
1403
|
): CachedRepresentation {
|
|
1404
|
+
const snapshotForProjection = target.state.snapshot;
|
|
1405
|
+
if (
|
|
1406
|
+
client.negotiated.alternateScreenProjection &&
|
|
1407
|
+
!client.materializeRequested &&
|
|
1408
|
+
(client.baselineRevision < 0 || client.projectionBaseline !== null) &&
|
|
1409
|
+
snapshotForProjection?.modes.alternateScreen &&
|
|
1410
|
+
snapshotForProjection.history.length > 0
|
|
1411
|
+
) {
|
|
1412
|
+
const { payload, retained } = prepareProjection(
|
|
1413
|
+
snapshotForProjection,
|
|
1414
|
+
target.update.revision,
|
|
1415
|
+
target.update.incarnation,
|
|
1416
|
+
client.reseedRequired ? null : client.projectionBaseline,
|
|
1417
|
+
);
|
|
1418
|
+
if (!seedOutcome) throw new DeferredProjectionEncoding(payload);
|
|
1419
|
+
if ("error" in seedOutcome) throw seedOutcome.error;
|
|
1420
|
+
return {
|
|
1421
|
+
bytes: seedOutcome.bytes,
|
|
1422
|
+
encoding: "alternate-projection-binary-v1",
|
|
1423
|
+
frame: payload.frame,
|
|
1424
|
+
history: "deferred",
|
|
1425
|
+
canonicalEquivalent: false,
|
|
1426
|
+
projection: retained,
|
|
1427
|
+
projectionBaseRevision: payload.frame === "seed" ? null : payload.baseRevision,
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1331
1430
|
const key = cacheKey([
|
|
1332
1431
|
target.update.workspaceName,
|
|
1333
1432
|
target.update.semanticPaneId,
|
|
@@ -1351,17 +1450,27 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1351
1450
|
let result: CachedRepresentation | null = null;
|
|
1352
1451
|
if (
|
|
1353
1452
|
client.negotiated.encoding === "semantic-v1" ||
|
|
1354
|
-
client.negotiated.encoding === "semantic-compact-v1"
|
|
1453
|
+
client.negotiated.encoding === "semantic-compact-v1" ||
|
|
1454
|
+
client.negotiated.encoding === "semantic-binary-v1"
|
|
1355
1455
|
) {
|
|
1356
1456
|
const encodeSemantic = (payload: TerminalSemanticDeliveryPayload): Uint8Array => {
|
|
1357
|
-
if (
|
|
1457
|
+
if (
|
|
1458
|
+
client.negotiated.encoding !== "semantic-compact-v1" &&
|
|
1459
|
+
client.negotiated.encoding !== "semantic-binary-v1"
|
|
1460
|
+
)
|
|
1358
1461
|
return encodeSemanticTerminalUpdate(payload);
|
|
1359
|
-
if (
|
|
1462
|
+
if (
|
|
1463
|
+
payload.frame === "seed" &&
|
|
1464
|
+
(client.negotiated.encoding === "semantic-binary-v1" ||
|
|
1465
|
+
terminalSemanticUpdateNeedsCooperativeEncoding(payload))
|
|
1466
|
+
) {
|
|
1360
1467
|
if (!seedOutcome) throw new DeferredSeedEncoding(payload);
|
|
1361
1468
|
if ("error" in seedOutcome) throw seedOutcome.error;
|
|
1362
1469
|
return seedOutcome.bytes;
|
|
1363
1470
|
}
|
|
1364
|
-
return
|
|
1471
|
+
return client.negotiated.encoding === "semantic-binary-v1"
|
|
1472
|
+
? encodeBinarySemanticTerminalUpdate(payload)
|
|
1473
|
+
: encodeCompactSemanticTerminalUpdate(payload);
|
|
1365
1474
|
};
|
|
1366
1475
|
if (client.reseedRequired) {
|
|
1367
1476
|
const payload = semanticSeed(target);
|
|
@@ -1693,6 +1802,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1693
1802
|
return;
|
|
1694
1803
|
}
|
|
1695
1804
|
const envelope = flight.envelope;
|
|
1805
|
+
if (envelope.type !== "terminal.delivery") {
|
|
1806
|
+
this.#fault(client, "protocol-violation", "Canonical ACK cannot acknowledge a projection");
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1696
1809
|
if (
|
|
1697
1810
|
flight.nextChunk !== envelope.chunkCount ||
|
|
1698
1811
|
ack.workspaceName !== envelope.workspaceName ||
|
|
@@ -1712,6 +1825,8 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1712
1825
|
this.#maxSlowClientMs,
|
|
1713
1826
|
this.#scheduler.nowMs() - flight.sentAt,
|
|
1714
1827
|
);
|
|
1828
|
+
client.projectionBaseline = null;
|
|
1829
|
+
client.lastProjectionReceipt = null;
|
|
1715
1830
|
client.baselineRevision = ack.canonicalRevision;
|
|
1716
1831
|
client.baselineHash = ack.canonicalStateHash;
|
|
1717
1832
|
client.reseedRequired = false;
|
|
@@ -1769,6 +1884,85 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1769
1884
|
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
1770
1885
|
}
|
|
1771
1886
|
|
|
1887
|
+
#projectionReceipt(client: ClientState, input: TerminalProjectionReceipt): void {
|
|
1888
|
+
const receipt = TerminalProjectionReceiptSchemaZ.parse(input);
|
|
1889
|
+
if (
|
|
1890
|
+
client.lastProjectionReceipt &&
|
|
1891
|
+
JSON.stringify(client.lastProjectionReceipt) === JSON.stringify(receipt)
|
|
1892
|
+
)
|
|
1893
|
+
return;
|
|
1894
|
+
const flight = client.inFlight;
|
|
1895
|
+
const envelope = flight?.envelope;
|
|
1896
|
+
if (
|
|
1897
|
+
!flight ||
|
|
1898
|
+
!flight.projection ||
|
|
1899
|
+
envelope?.type !== "terminal.projection" ||
|
|
1900
|
+
flight.nextChunk !== envelope.chunkCount ||
|
|
1901
|
+
receipt.workspaceName !== envelope.workspaceName ||
|
|
1902
|
+
receipt.semanticPaneId !== envelope.semanticPaneId ||
|
|
1903
|
+
receipt.generation !== envelope.generation ||
|
|
1904
|
+
receipt.incarnation !== envelope.incarnation ||
|
|
1905
|
+
receipt.deliveryNonce !== envelope.deliveryNonce ||
|
|
1906
|
+
receipt.transactionId !== envelope.transactionId ||
|
|
1907
|
+
receipt.canonicalRevision !== envelope.canonicalRevision ||
|
|
1908
|
+
receipt.canonicalStateHash !== envelope.canonicalStateHash ||
|
|
1909
|
+
receipt.projectionStateHash !== envelope.projectionStateHash ||
|
|
1910
|
+
receipt.representationHash !== envelope.representationHash
|
|
1911
|
+
) {
|
|
1912
|
+
this.#fault(
|
|
1913
|
+
client,
|
|
1914
|
+
"protocol-violation",
|
|
1915
|
+
"Projection receipt does not identify the in-flight projection",
|
|
1916
|
+
);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
client.projectionBaseline = flight.projection;
|
|
1920
|
+
client.lastProjectionReceipt = receipt;
|
|
1921
|
+
client.reseedRequired = false;
|
|
1922
|
+
client.inFlight = null;
|
|
1923
|
+
if (client.latestRevision === receipt.canonicalRevision) client.latestRevision = null;
|
|
1924
|
+
if (client.materializeRequested)
|
|
1925
|
+
client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
|
|
1926
|
+
this.#pruneCanonicalRevisions(client.paneId);
|
|
1927
|
+
this.#schedule(client);
|
|
1928
|
+
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
#materialize(client: ClientState, input: TerminalProjectionMaterialize): void {
|
|
1932
|
+
const request = TerminalProjectionMaterializeSchemaZ.parse(input);
|
|
1933
|
+
if (
|
|
1934
|
+
client.lastMaterialize &&
|
|
1935
|
+
JSON.stringify(client.lastMaterialize) === JSON.stringify(request)
|
|
1936
|
+
)
|
|
1937
|
+
return;
|
|
1938
|
+
const baseline = client.projectionBaseline;
|
|
1939
|
+
if (
|
|
1940
|
+
!client.negotiated.alternateScreenProjection ||
|
|
1941
|
+
!baseline ||
|
|
1942
|
+
request.workspaceName !== client.deliveryWorkspaceName ||
|
|
1943
|
+
request.semanticPaneId !== client.paneId ||
|
|
1944
|
+
request.generation !== client.negotiated.generation ||
|
|
1945
|
+
request.incarnation !== baseline.incarnation ||
|
|
1946
|
+
request.deliveryNonce !== client.negotiated.deliveryNonce ||
|
|
1947
|
+
request.canonicalRevision !== baseline.revision ||
|
|
1948
|
+
request.projectionStateHash !== baseline.hash
|
|
1949
|
+
) {
|
|
1950
|
+
this.#fault(
|
|
1951
|
+
client,
|
|
1952
|
+
"protocol-violation",
|
|
1953
|
+
"History request does not identify the retained projection",
|
|
1954
|
+
);
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
client.lastMaterialize = request;
|
|
1958
|
+
client.materializeRequested = true;
|
|
1959
|
+
this.#cancelEncoding(client);
|
|
1960
|
+
client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
|
|
1961
|
+
// A queued projection still owns its transaction until its receipt arrives.
|
|
1962
|
+
// The following complete seed then supersedes it, including at the same revision.
|
|
1963
|
+
this.#schedule(client);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1772
1966
|
#nack(client: ClientState, input: TerminalDeliveryNack): void {
|
|
1773
1967
|
const nack = TerminalDeliveryNackSchemaZ.parse(input);
|
|
1774
1968
|
const closed = client.sourceClosedFlight;
|
|
@@ -2224,7 +2418,7 @@ export function selectExactSemanticRepresentation<T extends { readonly frame: st
|
|
|
2224
2418
|
|
|
2225
2419
|
function completeSemanticSelectionObservation(
|
|
2226
2420
|
observation: ExactSemanticSelectionObservation,
|
|
2227
|
-
selectedEncoding: "semantic-v1" | "semantic-compact-v1",
|
|
2421
|
+
selectedEncoding: "semantic-v1" | "semantic-compact-v1" | "semantic-binary-v1",
|
|
2228
2422
|
legacyAttempts: Readonly<{ patchBytes: number | null; seedBytes: number | null }> | null,
|
|
2229
2423
|
): SemanticSelectionObservation {
|
|
2230
2424
|
const compact = selectedEncoding === "semantic-compact-v1";
|
package/packages/daemon/src/terminal/session-runtime/terminal-delivery-observation-identity.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { TerminalDeliveryEnvelope } from "@tmux-ide/contracts";
|
|
1
|
+
import type { TerminalDeliveryEnvelope, TerminalProjectionEnvelope } from "@tmux-ide/contracts";
|
|
2
2
|
|
|
3
|
-
const deliveryOrdinals = new WeakMap<
|
|
3
|
+
const deliveryOrdinals = new WeakMap<
|
|
4
|
+
TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
|
|
5
|
+
number
|
|
6
|
+
>();
|
|
4
7
|
|
|
5
8
|
export function registerTerminalDeliveryObservationOrdinal(
|
|
6
|
-
envelope: TerminalDeliveryEnvelope,
|
|
9
|
+
envelope: TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
|
|
7
10
|
ordinal: number,
|
|
8
11
|
): void {
|
|
9
12
|
if (!Number.isSafeInteger(ordinal) || ordinal < 1)
|
|
@@ -12,7 +15,7 @@ export function registerTerminalDeliveryObservationOrdinal(
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function terminalDeliveryObservationOrdinal(
|
|
15
|
-
envelope: TerminalDeliveryEnvelope,
|
|
18
|
+
envelope: TerminalDeliveryEnvelope | TerminalProjectionEnvelope,
|
|
16
19
|
): number | null {
|
|
17
20
|
return deliveryOrdinals.get(envelope) ?? null;
|
|
18
21
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TerminalProjectionPayload,
|
|
3
|
+
TerminalProjectionSnapshot,
|
|
4
|
+
TerminalReplicaSnapshot,
|
|
5
|
+
} from "@tmux-ide/contracts";
|
|
6
|
+
import {
|
|
7
|
+
hashTerminalProjection,
|
|
8
|
+
projectAlternateSnapshot,
|
|
9
|
+
terminalReplicaRowsEqual,
|
|
10
|
+
} from "@tmux-ide/core";
|
|
11
|
+
|
|
12
|
+
/** A projection receipt never becomes a complete canonical baseline. */
|
|
13
|
+
export interface ProjectionBaseline {
|
|
14
|
+
readonly revision: number;
|
|
15
|
+
readonly incarnation: string;
|
|
16
|
+
readonly hash: string;
|
|
17
|
+
readonly snapshot: TerminalProjectionSnapshot;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function prepareProjection(
|
|
21
|
+
snapshot: TerminalReplicaSnapshot,
|
|
22
|
+
revision: number,
|
|
23
|
+
incarnation: string,
|
|
24
|
+
baseline: ProjectionBaseline | null,
|
|
25
|
+
): { payload: TerminalProjectionPayload; retained: ProjectionBaseline } {
|
|
26
|
+
const projected = projectAlternateSnapshot(snapshot);
|
|
27
|
+
const retained = {
|
|
28
|
+
revision,
|
|
29
|
+
incarnation,
|
|
30
|
+
hash: hashTerminalProjection(projected),
|
|
31
|
+
snapshot: projected,
|
|
32
|
+
};
|
|
33
|
+
if (
|
|
34
|
+
!baseline ||
|
|
35
|
+
baseline.incarnation !== incarnation ||
|
|
36
|
+
baseline.snapshot.cols !== projected.cols ||
|
|
37
|
+
baseline.snapshot.rows !== projected.rows
|
|
38
|
+
)
|
|
39
|
+
return { payload: { frame: "seed", revision, projection: projected }, retained };
|
|
40
|
+
const rows = projected.grid.flatMap((row, index) =>
|
|
41
|
+
terminalReplicaRowsEqual(row, baseline.snapshot.grid[index]!) ? [] : [{ index, row }],
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
retained,
|
|
45
|
+
payload: {
|
|
46
|
+
frame: "patch",
|
|
47
|
+
baseRevision: baseline.revision,
|
|
48
|
+
revision,
|
|
49
|
+
patch: {
|
|
50
|
+
rows,
|
|
51
|
+
deferredHistory: projected.deferredHistory,
|
|
52
|
+
cursor: projected.cursor,
|
|
53
|
+
modes: projected.modes,
|
|
54
|
+
placements: projected.placements,
|
|
55
|
+
bootstrap: projected.bootstrap,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -259,11 +259,10 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
259
259
|
return this.#disposed ? null : this.#publishedIncarnation;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
/**
|
|
262
|
+
/** Screen recapture does not replace the terminal lifetime. Keep its input fence
|
|
263
|
+
* while resizing; disposal or a new owner still invalidates held input. */
|
|
263
264
|
inputAdmissionIncarnation(): string | null {
|
|
264
|
-
return this.#disposed
|
|
265
|
-
? null
|
|
266
|
-
: this.#publishedIncarnation;
|
|
265
|
+
return this.#disposed ? null : this.#publishedIncarnation;
|
|
267
266
|
}
|
|
268
267
|
|
|
269
268
|
qualificationSnapshot(): TerminalReplicaQualificationSnapshot {
|
|
@@ -532,6 +532,10 @@ class WireTerminalEndpoint {
|
|
|
532
532
|
return;
|
|
533
533
|
}
|
|
534
534
|
if (this.#unavailable) return;
|
|
535
|
+
if (message.type === "terminal.projection") {
|
|
536
|
+
this.#failConnection(new Error("Unsolicited alternate-screen projection"));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
535
539
|
if (message.type === "terminal.delivery") {
|
|
536
540
|
this.#acceptEnvelope(message);
|
|
537
541
|
return;
|
package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.ts";
|
|
1
2
|
import { createTerminalSelectionWaiter } from "./terminal-selection-waiter.ts";
|
|
2
3
|
import { WorkspaceWindowLinkSelectResultSchemaZ } from "@tmux-ide/contracts";
|
|
3
4
|
import { windowLinkForPane, windowLinkTarget } from "./application-terminal-workspace-policy.ts";
|
|
@@ -1050,11 +1051,15 @@ export function createApplicationTerminalInteractionController(
|
|
|
1050
1051
|
}
|
|
1051
1052
|
return outcome.status === "sent";
|
|
1052
1053
|
},
|
|
1053
|
-
onFocusedPane:
|
|
1054
|
+
onFocusedPane: (pane) => {
|
|
1055
|
+
rendererProbe()?.cancel("selection-changed", nowMicros());
|
|
1056
|
+
options.setFocusedPane(pane);
|
|
1057
|
+
},
|
|
1054
1058
|
});
|
|
1055
1059
|
|
|
1056
1060
|
return {
|
|
1057
1061
|
cancelPendingInput() {
|
|
1062
|
+
rendererProbe()?.cancel("input-cancelled", nowMicros());
|
|
1058
1063
|
selectionWaiter.cancel();
|
|
1059
1064
|
finishLinkSelection(false);
|
|
1060
1065
|
paneInput.invalidateSelection();
|
|
@@ -1064,6 +1069,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1064
1069
|
selectionGeneration = snapshot;
|
|
1065
1070
|
const control = controlIdentity(snapshot);
|
|
1066
1071
|
if (!sameControl(selectedControl, control)) {
|
|
1072
|
+
rendererProbe()?.cancel("control-changed", nowMicros());
|
|
1067
1073
|
finishLinkSelection(false);
|
|
1068
1074
|
paneInput.invalidateSelection();
|
|
1069
1075
|
}
|
|
@@ -1118,6 +1124,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1118
1124
|
next.fastLane !== inputAuthorityIdentity.fastLane ||
|
|
1119
1125
|
next.adapter !== inputAuthorityIdentity.adapter);
|
|
1120
1126
|
if (next === null || replaced) {
|
|
1127
|
+
rendererProbe()?.cancel("generation-changed", nowMicros());
|
|
1121
1128
|
finishLinkSelection(false);
|
|
1122
1129
|
pendingWindowSwitch = null;
|
|
1123
1130
|
pendingWindowRename = null;
|
|
@@ -1364,10 +1371,25 @@ export function createApplicationTerminalInteractionController(
|
|
|
1364
1371
|
}
|
|
1365
1372
|
},
|
|
1366
1373
|
sendInput: async (input, parserOrigin) => {
|
|
1367
|
-
|
|
1374
|
+
const probe = rendererProbe();
|
|
1375
|
+
const state = probe
|
|
1376
|
+
? (options.generation()?.fastLane?.lane.paneState(paneInput.focusedPane ?? "") ?? null)
|
|
1377
|
+
: null;
|
|
1378
|
+
const measured = probe?.admit(input, state, nowMicros());
|
|
1379
|
+
const sent = await paneInput.sendInput({ input: measured ?? input, parserOrigin });
|
|
1380
|
+
if (measured && !sent) probe?.cancel("dispatch-rejected", nowMicros());
|
|
1381
|
+
},
|
|
1382
|
+
sendInputToPane: async (paneId, input, parserOrigin) => {
|
|
1383
|
+
const probe = rendererProbe();
|
|
1384
|
+
const state = probe ? (options.generation()?.fastLane?.lane.paneState(paneId) ?? null) : null;
|
|
1385
|
+
const measured = probe?.admit(input, state, nowMicros());
|
|
1386
|
+
const sent = await paneInput.sendInputToPane(paneId, {
|
|
1387
|
+
input: measured ?? input,
|
|
1388
|
+
parserOrigin,
|
|
1389
|
+
});
|
|
1390
|
+
if (measured && !sent) probe?.cancel("dispatch-rejected", nowMicros());
|
|
1391
|
+
return sent;
|
|
1368
1392
|
},
|
|
1369
|
-
sendInputToPane: (paneId, input, parserOrigin) =>
|
|
1370
|
-
paneInput.sendInputToPane(paneId, { input, parserOrigin }),
|
|
1371
1393
|
beginResizePointerIngress(input) {
|
|
1372
1394
|
if (!options.diagnosticsEnabled) return null;
|
|
1373
1395
|
const atMicros = diagnosticNowMicros();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.ts";
|
|
1
2
|
import { paneInteractionDisplayDestination } from "../ui/pane-interaction-presentation.ts";
|
|
2
3
|
import type { InteractionObservationStatus, NativePaneIdentity } from "@tmux-ide/contracts";
|
|
3
4
|
import { interactionPaneEndpointKey } from "@tmux-ide/core";
|
|
@@ -1350,6 +1351,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1350
1351
|
return;
|
|
1351
1352
|
}
|
|
1352
1353
|
if (event.type === "scroll") {
|
|
1354
|
+
const probeAt = rendererProbe() ? Math.floor(performance.now() * 1000) : 0;
|
|
1353
1355
|
// A held selection owns the gesture even when the pointer crosses a sibling.
|
|
1354
1356
|
const activeSelection = selecting;
|
|
1355
1357
|
const selectionCell =
|
|
@@ -1359,7 +1361,10 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1359
1361
|
activeSelection && selectionCell
|
|
1360
1362
|
? { frame: activeSelection.frame, ...selectionCell }
|
|
1361
1363
|
: paneContentAt(point);
|
|
1362
|
-
const before =
|
|
1364
|
+
const before =
|
|
1365
|
+
(props.onWheelObservation || rendererProbe()) && hit
|
|
1366
|
+
? scrollback.offset(hit.frame.paneId)
|
|
1367
|
+
: null;
|
|
1363
1368
|
const observe = (route: string): void => {
|
|
1364
1369
|
props.onWheelObservation?.({
|
|
1365
1370
|
paneId: hit?.frame.paneId ?? null,
|
|
@@ -1457,6 +1462,20 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1457
1462
|
scrollback.seek(hit.frame.paneId, { ...origin, y: next.originY });
|
|
1458
1463
|
setKeyboardCopy({ ...keyboard, cursor: next.cursor });
|
|
1459
1464
|
} else scrollback.move(hit.frame.paneId, motion.lines);
|
|
1465
|
+
if (rendererProbe() && identity && before !== null)
|
|
1466
|
+
rendererProbe()?.scroll(
|
|
1467
|
+
{
|
|
1468
|
+
pane: hit.frame.paneId,
|
|
1469
|
+
generation: identity.generation,
|
|
1470
|
+
incarnation: identity.incarnation,
|
|
1471
|
+
cols: snapshot.cols,
|
|
1472
|
+
rows: snapshot.rows,
|
|
1473
|
+
},
|
|
1474
|
+
snapshot,
|
|
1475
|
+
before,
|
|
1476
|
+
scrollback.offset(hit.frame.paneId),
|
|
1477
|
+
probeAt,
|
|
1478
|
+
);
|
|
1460
1479
|
observe("local-history");
|
|
1461
1480
|
if (selecting?.paneId === hit.frame.paneId) updatePointerSelection(selecting.pointer);
|
|
1462
1481
|
else {
|