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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { prepareProjection } from "./terminal-projection-delivery.js";
|
|
2
|
+
import { TerminalProjectionEnvelopeSchemaZ, TerminalProjectionReceiptSchemaZ, TerminalProjectionMaterializeSchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
import { encodeTerminalProjectionCooperatively } from "@tmux-ide/core";
|
|
1
4
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
|
|
2
5
|
import { MAX_RETAINED_NATIVE_BACKING_BYTES, takeNativeSeedBacking, } from "./native-seed-backing.js";
|
|
3
6
|
import { WorkspaceIdSchemaZ, TERMINAL_GRAPHICS_CAPABILITY_VERSION, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
|
|
4
|
-
import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
|
|
7
|
+
import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeBinarySemanticTerminalUpdate, encodeBinarySemanticTerminalUpdateCooperatively, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
|
|
5
8
|
import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
|
|
6
9
|
import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
|
|
7
10
|
import { registerTerminalDeliveryObservationOrdinal } from "./terminal-delivery-observation-identity.js";
|
|
@@ -15,6 +18,13 @@ const MAX_PANES = 32;
|
|
|
15
18
|
const MAX_PENDING_CANONICAL_CELLS = 1_000_000;
|
|
16
19
|
const MAX_CONNECTIONS = MAX_CLIENTS * MAX_PANES;
|
|
17
20
|
const BACKGROUND_CADENCE_MS = 100;
|
|
21
|
+
class DeferredProjectionEncoding extends Error {
|
|
22
|
+
payload;
|
|
23
|
+
constructor(payload) {
|
|
24
|
+
super("Deferred projection encoding");
|
|
25
|
+
this.payload = payload;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
18
28
|
class DeferredSeedEncoding extends Error {
|
|
19
29
|
payload;
|
|
20
30
|
constructor(payload) {
|
|
@@ -135,6 +145,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
135
145
|
visibility: "visible",
|
|
136
146
|
baselineRevision: -1,
|
|
137
147
|
baselineHash: null,
|
|
148
|
+
projectionBaseline: null,
|
|
149
|
+
materializeRequested: false,
|
|
150
|
+
lastProjectionReceipt: null,
|
|
151
|
+
lastMaterialize: null,
|
|
138
152
|
reseedRequired: false,
|
|
139
153
|
inFlight: null,
|
|
140
154
|
latestRevision: pane.latest?.update.revision ?? null,
|
|
@@ -159,6 +173,8 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
159
173
|
negotiation,
|
|
160
174
|
closed,
|
|
161
175
|
ack: (ack) => this.#ack(client, ack),
|
|
176
|
+
projectionReceipt: (receipt) => this.#projectionReceipt(client, receipt),
|
|
177
|
+
materialize: (request) => this.#materialize(client, request),
|
|
162
178
|
nack: (nack) => this.#nack(client, nack),
|
|
163
179
|
setVisibility: (visibilityInput) => {
|
|
164
180
|
if (client.closed)
|
|
@@ -587,7 +603,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
587
603
|
if (!client.lifecycleOpenRecorded)
|
|
588
604
|
client.lifecycleOpenRecorded = this.#recordDeliveryLifecycle(client, pane, "open");
|
|
589
605
|
if (update.type === "terminal.tombstone" &&
|
|
590
|
-
(client.inFlight !== null ||
|
|
606
|
+
(client.inFlight !== null ||
|
|
607
|
+
client.encoding !== null ||
|
|
608
|
+
client.visibility !== "visible" ||
|
|
609
|
+
client.projectionBaseline !== null)) {
|
|
591
610
|
this.#fault(client, "source-closed", "Terminal source closed before final state delivery");
|
|
592
611
|
continue;
|
|
593
612
|
}
|
|
@@ -705,8 +724,13 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
705
724
|
encodedRepresentation = representation;
|
|
706
725
|
const transactionId = this.#scheduler.createId();
|
|
707
726
|
const chunkCount = Math.max(1, Math.ceil(representation.bytes.byteLength / (256 * 1024)));
|
|
708
|
-
const envelope =
|
|
709
|
-
|
|
727
|
+
const envelope = (representation.projection
|
|
728
|
+
? TerminalProjectionEnvelopeSchemaZ
|
|
729
|
+
: TerminalDeliveryEnvelopeSchemaZ).parse({
|
|
730
|
+
type: representation.projection ? "terminal.projection" : "terminal.delivery",
|
|
731
|
+
...(representation.projection
|
|
732
|
+
? { projectionStateHash: representation.projection.hash }
|
|
733
|
+
: {}),
|
|
710
734
|
workspaceName: client.deliveryWorkspaceName,
|
|
711
735
|
semanticPaneId: target.update.semanticPaneId,
|
|
712
736
|
generation: target.update.generation,
|
|
@@ -717,7 +741,11 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
717
741
|
protocolVersion: 1,
|
|
718
742
|
encoding: representation.encoding ?? client.negotiated.encoding,
|
|
719
743
|
frame: representation.frame,
|
|
720
|
-
baseRevision: representation.
|
|
744
|
+
baseRevision: representation.projection
|
|
745
|
+
? representation.projectionBaseRevision
|
|
746
|
+
: representation.frame === "seed"
|
|
747
|
+
? null
|
|
748
|
+
: Math.max(0, client.baselineRevision),
|
|
721
749
|
canonicalRevision: target.update.revision,
|
|
722
750
|
canonicalStateHash: target.update.stateHash,
|
|
723
751
|
representationHash: representation.representationHash ??
|
|
@@ -733,6 +761,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
733
761
|
registerTerminalDeliveryObservationOrdinal(envelope, deliveryOrdinal);
|
|
734
762
|
client.inFlight = {
|
|
735
763
|
envelope,
|
|
764
|
+
projection: representation.projection ?? null,
|
|
736
765
|
deliveryOrdinal,
|
|
737
766
|
bytes: representation.bytes,
|
|
738
767
|
nextChunk: 0,
|
|
@@ -744,9 +773,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
744
773
|
pane.pendingDeliveryTrace = null;
|
|
745
774
|
}
|
|
746
775
|
catch (error) {
|
|
747
|
-
if (error instanceof DeferredSeedEncoding) {
|
|
776
|
+
if (error instanceof DeferredSeedEncoding || error instanceof DeferredProjectionEncoding) {
|
|
748
777
|
deferred = true;
|
|
749
|
-
this.#startEncoding(client, pane, target, error.payload, traceStarted);
|
|
778
|
+
this.#startEncoding(client, pane, target, error.payload, traceStarted, error instanceof DeferredProjectionEncoding);
|
|
750
779
|
return;
|
|
751
780
|
}
|
|
752
781
|
failedSelectionObservation = semanticSelectionObservationFromError(error);
|
|
@@ -804,7 +833,7 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
804
833
|
}
|
|
805
834
|
}
|
|
806
835
|
}
|
|
807
|
-
#startEncoding(client, pane, target, payload, traceStarted) {
|
|
836
|
+
#startEncoding(client, pane, target, payload, traceStarted, projection = false) {
|
|
808
837
|
const key = cacheKey([
|
|
809
838
|
target.update.workspaceName,
|
|
810
839
|
client.paneId,
|
|
@@ -814,6 +843,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
814
843
|
target.update.stateHash,
|
|
815
844
|
client.negotiated.encoding,
|
|
816
845
|
client.negotiated.richPlacements ? "rich" : "plain",
|
|
846
|
+
projection
|
|
847
|
+
? `projection:${client.projectionBaseline?.revision ?? -1}:${client.projectionBaseline?.hash ?? "none"}`
|
|
848
|
+
: "complete",
|
|
817
849
|
]);
|
|
818
850
|
let job = this.#pendingEncodings.get(key);
|
|
819
851
|
let created = false;
|
|
@@ -844,10 +876,13 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
844
876
|
owned.clients.add(client);
|
|
845
877
|
if (!created)
|
|
846
878
|
return;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
879
|
+
const options = { signal: owned.abort.signal, yieldControl: () => this.#yieldEncoding(owned) };
|
|
880
|
+
const result = projection
|
|
881
|
+
? encodeTerminalProjectionCooperatively(payload, options)
|
|
882
|
+
: client.negotiated.encoding === "semantic-binary-v1"
|
|
883
|
+
? encodeBinarySemanticTerminalUpdateCooperatively(payload, options)
|
|
884
|
+
: encodeCompactSemanticTerminalUpdateCooperatively(payload, options);
|
|
885
|
+
void result.then((bytes) => this.#completeEncoding(owned, { bytes }), (error) => this.#completeEncoding(owned, { error }));
|
|
851
886
|
}
|
|
852
887
|
#yieldEncoding(job) {
|
|
853
888
|
if (this.#scheduler.nowMs() - job.yieldStartedAt < 2)
|
|
@@ -914,6 +949,27 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
914
949
|
this.#pruneCanonicalRevisions(client.paneId);
|
|
915
950
|
}
|
|
916
951
|
#representation(client, pane, target, seedOutcome) {
|
|
952
|
+
const snapshotForProjection = target.state.snapshot;
|
|
953
|
+
if (client.negotiated.alternateScreenProjection &&
|
|
954
|
+
!client.materializeRequested &&
|
|
955
|
+
(client.baselineRevision < 0 || client.projectionBaseline !== null) &&
|
|
956
|
+
snapshotForProjection?.modes.alternateScreen &&
|
|
957
|
+
snapshotForProjection.history.length > 0) {
|
|
958
|
+
const { payload, retained } = prepareProjection(snapshotForProjection, target.update.revision, target.update.incarnation, client.reseedRequired ? null : client.projectionBaseline);
|
|
959
|
+
if (!seedOutcome)
|
|
960
|
+
throw new DeferredProjectionEncoding(payload);
|
|
961
|
+
if ("error" in seedOutcome)
|
|
962
|
+
throw seedOutcome.error;
|
|
963
|
+
return {
|
|
964
|
+
bytes: seedOutcome.bytes,
|
|
965
|
+
encoding: "alternate-projection-binary-v1",
|
|
966
|
+
frame: payload.frame,
|
|
967
|
+
history: "deferred",
|
|
968
|
+
canonicalEquivalent: false,
|
|
969
|
+
projection: retained,
|
|
970
|
+
projectionBaseRevision: payload.frame === "seed" ? null : payload.baseRevision,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
917
973
|
const key = cacheKey([
|
|
918
974
|
target.update.workspaceName,
|
|
919
975
|
target.update.semanticPaneId,
|
|
@@ -937,18 +993,24 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
937
993
|
const snapshot = target.state.snapshot;
|
|
938
994
|
let result = null;
|
|
939
995
|
if (client.negotiated.encoding === "semantic-v1" ||
|
|
940
|
-
client.negotiated.encoding === "semantic-compact-v1"
|
|
996
|
+
client.negotiated.encoding === "semantic-compact-v1" ||
|
|
997
|
+
client.negotiated.encoding === "semantic-binary-v1") {
|
|
941
998
|
const encodeSemantic = (payload) => {
|
|
942
|
-
if (client.negotiated.encoding !== "semantic-compact-v1"
|
|
999
|
+
if (client.negotiated.encoding !== "semantic-compact-v1" &&
|
|
1000
|
+
client.negotiated.encoding !== "semantic-binary-v1")
|
|
943
1001
|
return encodeSemanticTerminalUpdate(payload);
|
|
944
|
-
if (
|
|
1002
|
+
if (payload.frame === "seed" &&
|
|
1003
|
+
(client.negotiated.encoding === "semantic-binary-v1" ||
|
|
1004
|
+
terminalSemanticUpdateNeedsCooperativeEncoding(payload))) {
|
|
945
1005
|
if (!seedOutcome)
|
|
946
1006
|
throw new DeferredSeedEncoding(payload);
|
|
947
1007
|
if ("error" in seedOutcome)
|
|
948
1008
|
throw seedOutcome.error;
|
|
949
1009
|
return seedOutcome.bytes;
|
|
950
1010
|
}
|
|
951
|
-
return
|
|
1011
|
+
return client.negotiated.encoding === "semantic-binary-v1"
|
|
1012
|
+
? encodeBinarySemanticTerminalUpdate(payload)
|
|
1013
|
+
: encodeCompactSemanticTerminalUpdate(payload);
|
|
952
1014
|
};
|
|
953
1015
|
if (client.reseedRequired) {
|
|
954
1016
|
const payload = semanticSeed(target);
|
|
@@ -1213,6 +1275,10 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1213
1275
|
return;
|
|
1214
1276
|
}
|
|
1215
1277
|
const envelope = flight.envelope;
|
|
1278
|
+
if (envelope.type !== "terminal.delivery") {
|
|
1279
|
+
this.#fault(client, "protocol-violation", "Canonical ACK cannot acknowledge a projection");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1216
1282
|
if (flight.nextChunk !== envelope.chunkCount ||
|
|
1217
1283
|
ack.workspaceName !== envelope.workspaceName ||
|
|
1218
1284
|
ack.semanticPaneId !== envelope.semanticPaneId ||
|
|
@@ -1227,6 +1293,8 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1227
1293
|
return;
|
|
1228
1294
|
}
|
|
1229
1295
|
this.#maxSlowClientMs = Math.max(this.#maxSlowClientMs, this.#scheduler.nowMs() - flight.sentAt);
|
|
1296
|
+
client.projectionBaseline = null;
|
|
1297
|
+
client.lastProjectionReceipt = null;
|
|
1230
1298
|
client.baselineRevision = ack.canonicalRevision;
|
|
1231
1299
|
client.baselineHash = ack.canonicalStateHash;
|
|
1232
1300
|
client.reseedRequired = false;
|
|
@@ -1277,6 +1345,68 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
1277
1345
|
this.#schedule(client);
|
|
1278
1346
|
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
1279
1347
|
}
|
|
1348
|
+
#projectionReceipt(client, input) {
|
|
1349
|
+
const receipt = TerminalProjectionReceiptSchemaZ.parse(input);
|
|
1350
|
+
if (client.lastProjectionReceipt &&
|
|
1351
|
+
JSON.stringify(client.lastProjectionReceipt) === JSON.stringify(receipt))
|
|
1352
|
+
return;
|
|
1353
|
+
const flight = client.inFlight;
|
|
1354
|
+
const envelope = flight?.envelope;
|
|
1355
|
+
if (!flight ||
|
|
1356
|
+
!flight.projection ||
|
|
1357
|
+
envelope?.type !== "terminal.projection" ||
|
|
1358
|
+
flight.nextChunk !== envelope.chunkCount ||
|
|
1359
|
+
receipt.workspaceName !== envelope.workspaceName ||
|
|
1360
|
+
receipt.semanticPaneId !== envelope.semanticPaneId ||
|
|
1361
|
+
receipt.generation !== envelope.generation ||
|
|
1362
|
+
receipt.incarnation !== envelope.incarnation ||
|
|
1363
|
+
receipt.deliveryNonce !== envelope.deliveryNonce ||
|
|
1364
|
+
receipt.transactionId !== envelope.transactionId ||
|
|
1365
|
+
receipt.canonicalRevision !== envelope.canonicalRevision ||
|
|
1366
|
+
receipt.canonicalStateHash !== envelope.canonicalStateHash ||
|
|
1367
|
+
receipt.projectionStateHash !== envelope.projectionStateHash ||
|
|
1368
|
+
receipt.representationHash !== envelope.representationHash) {
|
|
1369
|
+
this.#fault(client, "protocol-violation", "Projection receipt does not identify the in-flight projection");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
client.projectionBaseline = flight.projection;
|
|
1373
|
+
client.lastProjectionReceipt = receipt;
|
|
1374
|
+
client.reseedRequired = false;
|
|
1375
|
+
client.inFlight = null;
|
|
1376
|
+
if (client.latestRevision === receipt.canonicalRevision)
|
|
1377
|
+
client.latestRevision = null;
|
|
1378
|
+
if (client.materializeRequested)
|
|
1379
|
+
client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
|
|
1380
|
+
this.#pruneCanonicalRevisions(client.paneId);
|
|
1381
|
+
this.#schedule(client);
|
|
1382
|
+
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
1383
|
+
}
|
|
1384
|
+
#materialize(client, input) {
|
|
1385
|
+
const request = TerminalProjectionMaterializeSchemaZ.parse(input);
|
|
1386
|
+
if (client.lastMaterialize &&
|
|
1387
|
+
JSON.stringify(client.lastMaterialize) === JSON.stringify(request))
|
|
1388
|
+
return;
|
|
1389
|
+
const baseline = client.projectionBaseline;
|
|
1390
|
+
if (!client.negotiated.alternateScreenProjection ||
|
|
1391
|
+
!baseline ||
|
|
1392
|
+
request.workspaceName !== client.deliveryWorkspaceName ||
|
|
1393
|
+
request.semanticPaneId !== client.paneId ||
|
|
1394
|
+
request.generation !== client.negotiated.generation ||
|
|
1395
|
+
request.incarnation !== baseline.incarnation ||
|
|
1396
|
+
request.deliveryNonce !== client.negotiated.deliveryNonce ||
|
|
1397
|
+
request.canonicalRevision !== baseline.revision ||
|
|
1398
|
+
request.projectionStateHash !== baseline.hash) {
|
|
1399
|
+
this.#fault(client, "protocol-violation", "History request does not identify the retained projection");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
client.lastMaterialize = request;
|
|
1403
|
+
client.materializeRequested = true;
|
|
1404
|
+
this.#cancelEncoding(client);
|
|
1405
|
+
client.latestRevision = this.#panes.get(client.paneId)?.latest?.update.revision ?? null;
|
|
1406
|
+
// A queued projection still owns its transaction until its receipt arrives.
|
|
1407
|
+
// The following complete seed then supersedes it, including at the same revision.
|
|
1408
|
+
this.#schedule(client);
|
|
1409
|
+
}
|
|
1280
1410
|
#nack(client, input) {
|
|
1281
1411
|
const nack = TerminalDeliveryNackSchemaZ.parse(input);
|
|
1282
1412
|
const closed = client.sourceClosedFlight;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { hashTerminalProjection, projectAlternateSnapshot, terminalReplicaRowsEqual, } from "@tmux-ide/core";
|
|
2
|
+
export function prepareProjection(snapshot, revision, incarnation, baseline) {
|
|
3
|
+
const projected = projectAlternateSnapshot(snapshot);
|
|
4
|
+
const retained = {
|
|
5
|
+
revision,
|
|
6
|
+
incarnation,
|
|
7
|
+
hash: hashTerminalProjection(projected),
|
|
8
|
+
snapshot: projected,
|
|
9
|
+
};
|
|
10
|
+
if (!baseline ||
|
|
11
|
+
baseline.incarnation !== incarnation ||
|
|
12
|
+
baseline.snapshot.cols !== projected.cols ||
|
|
13
|
+
baseline.snapshot.rows !== projected.rows)
|
|
14
|
+
return { payload: { frame: "seed", revision, projection: projected }, retained };
|
|
15
|
+
const rows = projected.grid.flatMap((row, index) => terminalReplicaRowsEqual(row, baseline.snapshot.grid[index]) ? [] : [{ index, row }]);
|
|
16
|
+
return {
|
|
17
|
+
retained,
|
|
18
|
+
payload: {
|
|
19
|
+
frame: "patch",
|
|
20
|
+
baseRevision: baseline.revision,
|
|
21
|
+
revision,
|
|
22
|
+
patch: {
|
|
23
|
+
rows,
|
|
24
|
+
deferredHistory: projected.deferredHistory,
|
|
25
|
+
cursor: projected.cursor,
|
|
26
|
+
modes: projected.modes,
|
|
27
|
+
placements: projected.placements,
|
|
28
|
+
bootstrap: projected.bootstrap,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -155,11 +155,10 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
155
155
|
inputDispatchIncarnation() {
|
|
156
156
|
return this.#disposed ? null : this.#publishedIncarnation;
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
158
|
+
/** Screen recapture does not replace the terminal lifetime. Keep its input fence
|
|
159
|
+
* while resizing; disposal or a new owner still invalidates held input. */
|
|
159
160
|
inputAdmissionIncarnation() {
|
|
160
|
-
return this.#disposed
|
|
161
|
-
? null
|
|
162
|
-
: this.#publishedIncarnation;
|
|
161
|
+
return this.#disposed ? null : this.#publishedIncarnation;
|
|
163
162
|
}
|
|
164
163
|
qualificationSnapshot() {
|
|
165
164
|
const seed = this.#interpreter.currentSeed();
|
|
@@ -335,6 +335,10 @@ class WireTerminalEndpoint {
|
|
|
335
335
|
}
|
|
336
336
|
if (this.#unavailable)
|
|
337
337
|
return;
|
|
338
|
+
if (message.type === "terminal.projection") {
|
|
339
|
+
this.#failConnection(new Error("Unsolicited alternate-screen projection"));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
338
342
|
if (message.type === "terminal.delivery") {
|
|
339
343
|
this.#acceptEnvelope(message);
|
|
340
344
|
return;
|
package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.js";
|
|
1
2
|
import { createTerminalSelectionWaiter } from "./terminal-selection-waiter.js";
|
|
2
3
|
import { WorkspaceWindowLinkSelectResultSchemaZ } from "@tmux-ide/contracts";
|
|
3
4
|
import { windowLinkForPane, windowLinkTarget } from "./application-terminal-workspace-policy.js";
|
|
@@ -738,10 +739,14 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
738
739
|
}
|
|
739
740
|
return outcome.status === "sent";
|
|
740
741
|
},
|
|
741
|
-
onFocusedPane:
|
|
742
|
+
onFocusedPane: (pane) => {
|
|
743
|
+
rendererProbe()?.cancel("selection-changed", nowMicros());
|
|
744
|
+
options.setFocusedPane(pane);
|
|
745
|
+
},
|
|
742
746
|
});
|
|
743
747
|
return {
|
|
744
748
|
cancelPendingInput() {
|
|
749
|
+
rendererProbe()?.cancel("input-cancelled", nowMicros());
|
|
745
750
|
selectionWaiter.cancel();
|
|
746
751
|
finishLinkSelection(false);
|
|
747
752
|
paneInput.invalidateSelection();
|
|
@@ -752,6 +757,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
752
757
|
selectionGeneration = snapshot;
|
|
753
758
|
const control = controlIdentity(snapshot);
|
|
754
759
|
if (!sameControl(selectedControl, control)) {
|
|
760
|
+
rendererProbe()?.cancel("control-changed", nowMicros());
|
|
755
761
|
finishLinkSelection(false);
|
|
756
762
|
paneInput.invalidateSelection();
|
|
757
763
|
}
|
|
@@ -807,6 +813,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
807
813
|
next.fastLane !== inputAuthorityIdentity.fastLane ||
|
|
808
814
|
next.adapter !== inputAuthorityIdentity.adapter);
|
|
809
815
|
if (next === null || replaced) {
|
|
816
|
+
rendererProbe()?.cancel("generation-changed", nowMicros());
|
|
810
817
|
finishLinkSelection(false);
|
|
811
818
|
pendingWindowSwitch = null;
|
|
812
819
|
pendingWindowRename = null;
|
|
@@ -1039,9 +1046,27 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
1039
1046
|
}
|
|
1040
1047
|
},
|
|
1041
1048
|
sendInput: async (input, parserOrigin) => {
|
|
1042
|
-
|
|
1049
|
+
const probe = rendererProbe();
|
|
1050
|
+
const state = probe
|
|
1051
|
+
? (options.generation()?.fastLane?.lane.paneState(paneInput.focusedPane ?? "") ?? null)
|
|
1052
|
+
: null;
|
|
1053
|
+
const measured = probe?.admit(input, state, nowMicros());
|
|
1054
|
+
const sent = await paneInput.sendInput({ input: measured ?? input, parserOrigin });
|
|
1055
|
+
if (measured && !sent)
|
|
1056
|
+
probe?.cancel("dispatch-rejected", nowMicros());
|
|
1057
|
+
},
|
|
1058
|
+
sendInputToPane: async (paneId, input, parserOrigin) => {
|
|
1059
|
+
const probe = rendererProbe();
|
|
1060
|
+
const state = probe ? (options.generation()?.fastLane?.lane.paneState(paneId) ?? null) : null;
|
|
1061
|
+
const measured = probe?.admit(input, state, nowMicros());
|
|
1062
|
+
const sent = await paneInput.sendInputToPane(paneId, {
|
|
1063
|
+
input: measured ?? input,
|
|
1064
|
+
parserOrigin,
|
|
1065
|
+
});
|
|
1066
|
+
if (measured && !sent)
|
|
1067
|
+
probe?.cancel("dispatch-rejected", nowMicros());
|
|
1068
|
+
return sent;
|
|
1043
1069
|
},
|
|
1044
|
-
sendInputToPane: (paneId, input, parserOrigin) => paneInput.sendInputToPane(paneId, { input, parserOrigin }),
|
|
1045
1070
|
beginResizePointerIngress(input) {
|
|
1046
1071
|
if (!options.diagnosticsEnabled)
|
|
1047
1072
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.js";
|
|
1
2
|
import { paneInteractionDisplayDestination } from "../ui/pane-interaction-presentation.js";
|
|
2
3
|
import { interactionPaneEndpointKey } from "@tmux-ide/core";
|
|
3
4
|
import { interactionForCurrentPane } from "../ui/pane-interaction-presentation.js";
|
|
@@ -935,6 +936,7 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
935
936
|
return;
|
|
936
937
|
}
|
|
937
938
|
if (event.type === "scroll") {
|
|
939
|
+
const probeAt = rendererProbe() ? Math.floor(performance.now() * 1000) : 0;
|
|
938
940
|
// A held selection owns the gesture even when the pointer crosses a sibling.
|
|
939
941
|
const activeSelection = selecting;
|
|
940
942
|
const selectionCell = activeSelection &&
|
|
@@ -942,7 +944,9 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
942
944
|
const hit = activeSelection && selectionCell
|
|
943
945
|
? { frame: activeSelection.frame, ...selectionCell }
|
|
944
946
|
: paneContentAt(point);
|
|
945
|
-
const before = props.onWheelObservation
|
|
947
|
+
const before = (props.onWheelObservation || rendererProbe()) && hit
|
|
948
|
+
? scrollback.offset(hit.frame.paneId)
|
|
949
|
+
: null;
|
|
946
950
|
const observe = (route) => {
|
|
947
951
|
props.onWheelObservation?.({
|
|
948
952
|
paneId: hit?.frame.paneId ?? null,
|
|
@@ -1021,6 +1025,14 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1021
1025
|
}
|
|
1022
1026
|
else
|
|
1023
1027
|
scrollback.move(hit.frame.paneId, motion.lines);
|
|
1028
|
+
if (rendererProbe() && identity && before !== null)
|
|
1029
|
+
rendererProbe()?.scroll({
|
|
1030
|
+
pane: hit.frame.paneId,
|
|
1031
|
+
generation: identity.generation,
|
|
1032
|
+
incarnation: identity.incarnation,
|
|
1033
|
+
cols: snapshot.cols,
|
|
1034
|
+
rows: snapshot.rows,
|
|
1035
|
+
}, snapshot, before, scrollback.offset(hit.frame.paneId), probeAt);
|
|
1024
1036
|
observe("local-history");
|
|
1025
1037
|
if (selecting?.paneId === hit.frame.paneId)
|
|
1026
1038
|
updatePointerSelection(selecting.pointer);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { visibleTerminalRowAt } from "../semantic-pane-render-source.js";
|
|
2
|
+
import { appendFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
function scope(state) {
|
|
5
|
+
return state.snapshot
|
|
6
|
+
? {
|
|
7
|
+
pane: state.semanticPaneId,
|
|
8
|
+
generation: state.generation,
|
|
9
|
+
incarnation: state.incarnation,
|
|
10
|
+
cols: state.snapshot.cols,
|
|
11
|
+
rows: state.snapshot.rows,
|
|
12
|
+
}
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
const key = (s) => JSON.stringify([s.pane, s.generation, s.incarnation, s.cols, s.rows]);
|
|
16
|
+
export class RendererProbe {
|
|
17
|
+
trials;
|
|
18
|
+
emit;
|
|
19
|
+
#next = 0;
|
|
20
|
+
#pending = null;
|
|
21
|
+
constructor(trials, emit) {
|
|
22
|
+
this.trials = trials;
|
|
23
|
+
this.emit = emit;
|
|
24
|
+
if (trials.length > 64 ||
|
|
25
|
+
new Set(trials.map((t) => t.nonce)).size !== trials.length ||
|
|
26
|
+
trials.some((t) => !t.id ||
|
|
27
|
+
t.id.length > 128 ||
|
|
28
|
+
!/^[a-f0-9]{32}$/.test(t.nonce) ||
|
|
29
|
+
(t.kind !== undefined && !["input", "scroll", "output"].includes(t.kind))))
|
|
30
|
+
throw new Error("Invalid renderer probe trials");
|
|
31
|
+
}
|
|
32
|
+
get pending() {
|
|
33
|
+
return this.#pending !== null;
|
|
34
|
+
}
|
|
35
|
+
admit(input, state, at) {
|
|
36
|
+
if (input.kind !== "text" ||
|
|
37
|
+
input.data !== "!" ||
|
|
38
|
+
!state?.snapshot ||
|
|
39
|
+
this.#pending ||
|
|
40
|
+
!this.trials[this.#next])
|
|
41
|
+
return null;
|
|
42
|
+
if (this.trials[this.#next]?.kind === "scroll")
|
|
43
|
+
return { kind: "text", data: `PROBE-PREPARE-SCROLL:${this.trials[this.#next].nonce}\n` };
|
|
44
|
+
const trial = this.trials[this.#next++], marker = `PROBE-OUTPUT:${trial.nonce}`, identity = scope(state);
|
|
45
|
+
this.#pending = { trial, scope: identity, at, marker, offset: 0, sourceOffset: 0 };
|
|
46
|
+
if (state.snapshot.cols < marker.length ||
|
|
47
|
+
state.snapshot.grid.some((r) => r.cells
|
|
48
|
+
.map((c) => c.grapheme)
|
|
49
|
+
.join("")
|
|
50
|
+
.includes(marker))) {
|
|
51
|
+
this.cancel("negative-control", at);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
kind: "text",
|
|
56
|
+
data: `${trial.kind === "output" ? "PROBE-BURST" : "PROBE-IN"}:${trial.nonce}\n`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
scroll(identity, snapshot, before, after, at) {
|
|
60
|
+
const trial = this.trials[this.#next];
|
|
61
|
+
if (this.#pending || trial?.kind !== "scroll" || before === after)
|
|
62
|
+
return;
|
|
63
|
+
const row = visibleTerminalRowAt(snapshot, after, 0);
|
|
64
|
+
const text = row?.cells
|
|
65
|
+
.map((c) => c.grapheme)
|
|
66
|
+
.join("")
|
|
67
|
+
.trimEnd() ?? "";
|
|
68
|
+
if (!new RegExp(`^PROBE-ROW:${trial.nonce}:[0-9]{4}$`).test(text))
|
|
69
|
+
return;
|
|
70
|
+
for (let y = 0; y < snapshot.rows; y++)
|
|
71
|
+
if (visibleTerminalRowAt(snapshot, before, y)
|
|
72
|
+
?.cells.map((c) => c.grapheme)
|
|
73
|
+
.join("")
|
|
74
|
+
.trimEnd() === text)
|
|
75
|
+
return;
|
|
76
|
+
this.#next++;
|
|
77
|
+
this.#pending = {
|
|
78
|
+
trial,
|
|
79
|
+
scope: identity,
|
|
80
|
+
at,
|
|
81
|
+
marker: text,
|
|
82
|
+
offset: after,
|
|
83
|
+
sourceOffset: before,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
tick(at) {
|
|
87
|
+
if (this.#pending && at - this.#pending.at > 5_000_000)
|
|
88
|
+
this.cancel("timeout", at);
|
|
89
|
+
}
|
|
90
|
+
cancel(reason, at) {
|
|
91
|
+
const p = this.#pending;
|
|
92
|
+
if (!p)
|
|
93
|
+
return;
|
|
94
|
+
this.#pending = null;
|
|
95
|
+
this.emit({
|
|
96
|
+
...p.trial,
|
|
97
|
+
side: "tui",
|
|
98
|
+
kind: p.trial.kind ?? "input",
|
|
99
|
+
sourceScope: p.scope,
|
|
100
|
+
expectedScope: p.scope,
|
|
101
|
+
sourceOffset: p.sourceOffset,
|
|
102
|
+
expectedOffset: p.offset,
|
|
103
|
+
admittedAtMicros: p.at,
|
|
104
|
+
completedAtMicros: at,
|
|
105
|
+
endpoint: "input-admission-to-renderer-work-complete",
|
|
106
|
+
status: "failed",
|
|
107
|
+
reason,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
observe(state, at) {
|
|
111
|
+
if (!this.#pending)
|
|
112
|
+
return;
|
|
113
|
+
if (state && state.semanticPaneId !== this.#pending.scope.pane)
|
|
114
|
+
return;
|
|
115
|
+
if (!state || !scope(state) || key(scope(state)) !== key(this.#pending.scope))
|
|
116
|
+
this.cancel("scope-changed", at);
|
|
117
|
+
else if (at - this.#pending.at > 5_000_000)
|
|
118
|
+
this.cancel("timeout", at);
|
|
119
|
+
}
|
|
120
|
+
row(state, row, viewportRow, width, height, left, scrollOffset, at) {
|
|
121
|
+
this.observe(state, at);
|
|
122
|
+
const p = this.#pending;
|
|
123
|
+
if (!p || state.semanticPaneId !== p.scope.pane)
|
|
124
|
+
return;
|
|
125
|
+
if (scrollOffset !== p.offset ||
|
|
126
|
+
left !== 0 ||
|
|
127
|
+
viewportRow !== 0 ||
|
|
128
|
+
height < 1 ||
|
|
129
|
+
width < p.marker.length ||
|
|
130
|
+
!row)
|
|
131
|
+
return;
|
|
132
|
+
const cells = row.cells.slice(0, p.marker.length);
|
|
133
|
+
if (cells.some((c) => c.width !== 1) || cells.map((c) => c.grapheme).join("") !== p.marker)
|
|
134
|
+
return;
|
|
135
|
+
if (!Number.isSafeInteger(at) || at < p.at) {
|
|
136
|
+
this.cancel("clock-invalid", at);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.#pending = null;
|
|
140
|
+
this.emit({
|
|
141
|
+
...p.trial,
|
|
142
|
+
side: "tui",
|
|
143
|
+
kind: p.trial.kind ?? "input",
|
|
144
|
+
sourceScope: p.scope,
|
|
145
|
+
expectedScope: p.scope,
|
|
146
|
+
sourceOffset: p.sourceOffset,
|
|
147
|
+
expectedOffset: p.offset,
|
|
148
|
+
admittedAtMicros: p.at,
|
|
149
|
+
completedAtMicros: at,
|
|
150
|
+
endpoint: "input-admission-to-renderer-work-complete",
|
|
151
|
+
status: "completed",
|
|
152
|
+
durationMicros: at - p.at,
|
|
153
|
+
witness: {
|
|
154
|
+
row: viewportRow,
|
|
155
|
+
column: 0,
|
|
156
|
+
marker: p.marker,
|
|
157
|
+
revision: state.revision,
|
|
158
|
+
stateHash: state.hash,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let singleton;
|
|
164
|
+
/** All filesystem work is asynchronous and deferred; at most64 short receipts. */
|
|
165
|
+
export function rendererProbe() {
|
|
166
|
+
if (singleton !== undefined)
|
|
167
|
+
return singleton;
|
|
168
|
+
singleton = null;
|
|
169
|
+
if (process.env.TMUX_IDE_RENDERER_FIXTURE !== "1")
|
|
170
|
+
return null;
|
|
171
|
+
const raw = process.env.TMUX_IDE_RENDERER_PROBE_TRIALS, path = process.env.TMUX_IDE_RENDERER_PROBE_LOG;
|
|
172
|
+
if (!raw || raw.length > 16384 || !path || !isAbsolute(path))
|
|
173
|
+
return null;
|
|
174
|
+
try {
|
|
175
|
+
const trials = JSON.parse(raw);
|
|
176
|
+
if (!Array.isArray(trials))
|
|
177
|
+
return null;
|
|
178
|
+
let writes = Promise.resolve(), failed = false;
|
|
179
|
+
singleton = new RendererProbe(trials, (record) => {
|
|
180
|
+
if (failed)
|
|
181
|
+
return;
|
|
182
|
+
writes = writes
|
|
183
|
+
.then(() => appendFile(path, JSON.stringify(record) + "\n", { mode: 0o600 }))
|
|
184
|
+
.catch(() => {
|
|
185
|
+
failed = true;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
const timer = setInterval(() => singleton?.tick(Math.floor(performance.now() * 1000)), 250);
|
|
189
|
+
timer.unref();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
singleton = null;
|
|
193
|
+
}
|
|
194
|
+
return singleton;
|
|
195
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.js";
|
|
1
2
|
import { terminalReplicaRowsEqual } from "@tmux-ide/core";
|
|
2
3
|
import { reflowTerminalPosition, reflowRetainedTerminalSnapshot, retainNativeTerminalBacking, } from "../terminal-viewport.js";
|
|
3
4
|
import { blitSemanticRow, changedTerminalRows, visibleTerminalRowAt, } from "../semantic-pane-render-source.js";
|
|
@@ -481,6 +482,8 @@ export class TerminalFastLaneRendererAdapter {
|
|
|
481
482
|
if (interest.retainedView && next)
|
|
482
483
|
this.#resizeRetainedView(interest, next);
|
|
483
484
|
interest.state = publication.state;
|
|
485
|
+
if (rendererProbe()?.pending)
|
|
486
|
+
rendererProbe()?.observe(publication.state, Math.floor(performance.now() * 1000));
|
|
484
487
|
// Preserve coordinates for an unchanged prefix or an exact retained suffix.
|
|
485
488
|
// The suffix offset is fixed by the length difference; never search repeated
|
|
486
489
|
// log lines for a guessed overlap. Changed grids/reflow need a separate map.
|
|
@@ -810,6 +813,9 @@ export class TerminalFastLaneRendererAdapter {
|
|
|
810
813
|
if (interest?.dirtyRows.has(canonicalRow))
|
|
811
814
|
paintedCanonicalChange = true;
|
|
812
815
|
blit(sourceRow, buffers, row, width, defaultFg, defaultBg, options.graphemes, options.palette, options.viewportOrigin?.x ?? 0);
|
|
816
|
+
if (interest?.state && rendererProbe()?.pending) {
|
|
817
|
+
rendererProbe()?.row(interest.retainedView?.state ?? interest.state, sourceRow, row, width, height, options.viewportOrigin?.x ?? 0, options.viewportOrigin ? Math.max(0, -options.viewportOrigin.y) : scrollOffset, Math.floor(performance.now() * 1_000));
|
|
818
|
+
}
|
|
813
819
|
options.dirtyRows.push(row);
|
|
814
820
|
paintedRows[row] = sourceRow;
|
|
815
821
|
}
|