tmux-ide 2.9.10 → 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 +4321 -2596
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +4 -0
- package/packages/contracts/src/pane-stream.ts +72 -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-hash-cache.ts +8 -1
- 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/mirror/mirror-service.js +12 -2
- package/packages/daemon/dist/terminal/mirror/session-channel.js +46 -23
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +190 -19
- package/packages/daemon/dist/terminal/pane-stream/runtime.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +19 -6
- 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-interpreter.js +95 -36
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +5 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +2 -2
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-generation-starter.js +16 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agent-navigation.js +13 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/application-retained-agent.js +18 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +90 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +13 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/renderer-probe.js +195 -0
- package/packages/daemon/dist/tui/mirror/runtime/runtime-layout-presentation.js +6 -3
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +7 -2
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-selection-waiter.js +36 -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/mirror/mirror-service.ts +19 -2
- package/packages/daemon/src/terminal/mirror/session-channel.ts +57 -9
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +248 -16
- package/packages/daemon/src/terminal/pane-stream/runtime.ts +3 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +25 -3
- 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-interpreter.ts +102 -44
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +6 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +3 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +6 -0
- package/packages/daemon/src/tui/mirror/runtime/application-generation-starter.ts +17 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agent-navigation.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +18 -4
- package/packages/daemon/src/tui/mirror/runtime/application-retained-agent.ts +29 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +89 -6
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +2 -2
- package/packages/daemon/src/tui/mirror/runtime/renderer-probe.ts +257 -0
- package/packages/daemon/src/tui/mirror/runtime/runtime-layout-presentation.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +15 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-selection-waiter.ts +36 -0
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +4 -0
- package/packages/daemon-client/src/pane-stream-client.ts +92 -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
|
+
}
|
|
@@ -7,6 +7,9 @@ import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability
|
|
|
7
7
|
import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpreter-backend.js";
|
|
8
8
|
import { CAUSAL_CELL_OSC, CausalCellLedger, cellsEqual, } from "./causal-cell-ledger.js";
|
|
9
9
|
import { KITTY_PLACEMENT_OSC, KittyApcScanner, KittyImageStore, } from "../graphics/kitty-graphics.js";
|
|
10
|
+
// Bound parser work per turn without changing byte-stream semantics. This is a
|
|
11
|
+
// batch bound, not a producer queue cap: upstream pressure must preserve bytes.
|
|
12
|
+
const MAX_INGEST_BATCH_BYTES = 256 * 1024;
|
|
10
13
|
/** Daemon-owned, sans-I/O VT interpreter with one FIFO for bytes and geometry. */
|
|
11
14
|
export class TerminalReplicaInterpreter {
|
|
12
15
|
#nativeSeedBackingCandidate;
|
|
@@ -35,8 +38,7 @@ export class TerminalReplicaInterpreter {
|
|
|
35
38
|
#needsSeed = true;
|
|
36
39
|
#closed = false;
|
|
37
40
|
#walkCount = 0;
|
|
38
|
-
#pendingWrites =
|
|
39
|
-
#writeFlushScheduled = false;
|
|
41
|
+
#pendingWrites = null;
|
|
40
42
|
#widgetGate = false;
|
|
41
43
|
// kitty graphics: images outlive reseeds; the scanner owns split APCs.
|
|
42
44
|
#kittyScanner = new KittyApcScanner();
|
|
@@ -106,16 +108,25 @@ export class TerminalReplicaInterpreter {
|
|
|
106
108
|
if (operation.type === "write") {
|
|
107
109
|
const data = operation.data.slice();
|
|
108
110
|
const trace = operation.trace ?? null;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(pendingTrace?.traceId ?? null) !== (trace?.traceId ?? null))
|
|
111
|
+
if (this.#pendingWrites &&
|
|
112
|
+
(this.#pendingWrites.trace?.traceId ?? null) !== (trace?.traceId ?? null))
|
|
112
113
|
this.#flushWrites();
|
|
114
|
+
const group = (this.#pendingWrites ??= {
|
|
115
|
+
entries: [],
|
|
116
|
+
bytes: 0,
|
|
117
|
+
scheduled: false,
|
|
118
|
+
flushQueued: false,
|
|
119
|
+
trace,
|
|
120
|
+
});
|
|
113
121
|
const promise = new Promise((resolve, reject) => {
|
|
114
|
-
|
|
122
|
+
group.entries.push({ data, offset: 0, resolve, reject });
|
|
123
|
+
group.bytes += data.length;
|
|
115
124
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
// Leave this group open while the parser awaits its callback. Scheduling
|
|
126
|
+
// it once on the FIFO lets later arrivals join the next bounded batch.
|
|
127
|
+
if (!group.scheduled && !group.flushQueued) {
|
|
128
|
+
group.flushQueued = true;
|
|
129
|
+
this.#scheduler.microtask(() => this.#scheduleWriteGroup(group));
|
|
119
130
|
}
|
|
120
131
|
return promise;
|
|
121
132
|
}
|
|
@@ -361,22 +372,26 @@ export class TerminalReplicaInterpreter {
|
|
|
361
372
|
// serialize dozens of turns and fall behind a 30 fps image stream.
|
|
362
373
|
if (scanned.forward.byteLength === 0)
|
|
363
374
|
return;
|
|
364
|
-
|
|
375
|
+
// Scanning can prepend an escape retained from the previous chunk. Bound
|
|
376
|
+
// the actual parser calls too, and union damage across every fragment.
|
|
377
|
+
for (let offset = 0; offset < scanned.forward.length; offset += MAX_INGEST_BATCH_BYTES) {
|
|
378
|
+
await this.#writeToBackend(this.#backend, scanned.forward.subarray(offset, offset + MAX_INGEST_BATCH_BYTES));
|
|
379
|
+
// The parser resets its dirty tracker per write. Synchronized output and
|
|
380
|
+
// bounded fragments can both span writes before canonical publication.
|
|
381
|
+
const dirty = this.#backend.dirtyRange();
|
|
382
|
+
const pending = this.#pendingWriteDirty;
|
|
383
|
+
this.#pendingWriteDirty =
|
|
384
|
+
pending === null
|
|
385
|
+
? dirty
|
|
386
|
+
: pending === undefined || dirty === undefined
|
|
387
|
+
? undefined
|
|
388
|
+
: {
|
|
389
|
+
start: Math.min(pending.start, dirty.start),
|
|
390
|
+
end: Math.max(pending.end, dirty.end),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
365
393
|
if (this.#observability.enabled)
|
|
366
394
|
this.#observability.recordSpan("parse", "terminal-replica-write", parseStarted, this.#observability.nowMicros(), trace);
|
|
367
|
-
// The parser resets its dirty tracker per write. Synchronized output may
|
|
368
|
-
// span many writes before publication, so retain their union until commit.
|
|
369
|
-
const dirty = this.#backend.dirtyRange();
|
|
370
|
-
const pending = this.#pendingWriteDirty;
|
|
371
|
-
this.#pendingWriteDirty =
|
|
372
|
-
pending === null
|
|
373
|
-
? dirty
|
|
374
|
-
: pending === undefined || dirty === undefined
|
|
375
|
-
? undefined
|
|
376
|
-
: {
|
|
377
|
-
start: Math.min(pending.start, dirty.start),
|
|
378
|
-
end: Math.max(pending.end, dirty.end),
|
|
379
|
-
};
|
|
380
395
|
// DEC synchronized-output is atomic: no intermediate frame leaks.
|
|
381
396
|
if (this.#backend.requiresNativeReseed()) {
|
|
382
397
|
this.#commit(false);
|
|
@@ -402,6 +417,7 @@ export class TerminalReplicaInterpreter {
|
|
|
402
417
|
return;
|
|
403
418
|
this.#syncRecovery = this.#scheduler.timer(() => {
|
|
404
419
|
this.#syncRecovery = null;
|
|
420
|
+
this.#flushWrites();
|
|
405
421
|
const run = this.#tail.then(async () => {
|
|
406
422
|
if (this.#closed || !this.#backend.modes().synchronizedOutput)
|
|
407
423
|
return;
|
|
@@ -661,22 +677,65 @@ export class TerminalReplicaInterpreter {
|
|
|
661
677
|
}
|
|
662
678
|
}
|
|
663
679
|
}
|
|
680
|
+
/** Seal the current group at every trace, control and idle barrier. */
|
|
664
681
|
#flushWrites() {
|
|
665
|
-
|
|
666
|
-
|
|
682
|
+
const group = this.#pendingWrites;
|
|
683
|
+
this.#pendingWrites = null;
|
|
684
|
+
if (group)
|
|
685
|
+
this.#scheduleWriteGroup(group);
|
|
686
|
+
}
|
|
687
|
+
#scheduleWriteGroup(group) {
|
|
688
|
+
if (group.scheduled)
|
|
667
689
|
return;
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
690
|
+
group.scheduled = true;
|
|
691
|
+
const run = this.#tail.then(async () => {
|
|
692
|
+
try {
|
|
693
|
+
while (group.entries.length > 0) {
|
|
694
|
+
const length = Math.min(group.bytes, MAX_INGEST_BATCH_BYTES);
|
|
695
|
+
const data = new Uint8Array(length);
|
|
696
|
+
const completed = [];
|
|
697
|
+
let offset = 0;
|
|
698
|
+
for (const entry of group.entries) {
|
|
699
|
+
const take = Math.min(entry.data.length - entry.offset, length - offset);
|
|
700
|
+
data.set(entry.data.subarray(entry.offset, entry.offset + take), offset);
|
|
701
|
+
offset += take;
|
|
702
|
+
entry.offset += take;
|
|
703
|
+
if (entry.offset === entry.data.length)
|
|
704
|
+
completed.push(entry);
|
|
705
|
+
else
|
|
706
|
+
break;
|
|
707
|
+
if (offset === length && length > 0)
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
group.entries.splice(0, completed.length);
|
|
711
|
+
group.bytes -= length;
|
|
712
|
+
try {
|
|
713
|
+
await this.#write(data, group.trace);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
for (const entry of completed)
|
|
717
|
+
entry.reject(error);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
for (const entry of completed)
|
|
721
|
+
entry.resolve();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
// Reject every remaining fragment in the failed group; later FIFO
|
|
726
|
+
// controls (including recovery reseeds and close) still execute.
|
|
727
|
+
for (const entry of group.entries)
|
|
728
|
+
entry.reject(error);
|
|
729
|
+
group.entries.length = 0;
|
|
730
|
+
group.bytes = 0;
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
if (this.#pendingWrites === group)
|
|
735
|
+
this.#pendingWrites = null;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
678
738
|
this.#tail = run.catch(() => undefined);
|
|
679
|
-
void run.then(() => pending.forEach((entry) => entry.resolve()), (error) => pending.forEach((entry) => entry.reject(error)));
|
|
680
739
|
}
|
|
681
740
|
#observeMarkerBytes(data) {
|
|
682
741
|
if (this.#widgetGate)
|
|
@@ -155,6 +155,11 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
155
155
|
inputDispatchIncarnation() {
|
|
156
156
|
return this.#disposed ? null : this.#publishedIncarnation;
|
|
157
157
|
}
|
|
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. */
|
|
160
|
+
inputAdmissionIncarnation() {
|
|
161
|
+
return this.#disposed ? null : this.#publishedIncarnation;
|
|
162
|
+
}
|
|
158
163
|
qualificationSnapshot() {
|
|
159
164
|
const seed = this.#interpreter.currentSeed();
|
|
160
165
|
return Object.freeze({
|
|
@@ -275,7 +275,7 @@ export class SessionRuntimeTransportBinding {
|
|
|
275
275
|
authorize();
|
|
276
276
|
return authorize;
|
|
277
277
|
}
|
|
278
|
-
sendInput(semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult) {
|
|
278
|
+
sendInput(semanticPaneId, input, performanceTraceId, causalProbe, onCausalResult, expectedLifetime) {
|
|
279
279
|
this.assertController(semanticPaneId);
|
|
280
280
|
const lease = this.#shared.lease;
|
|
281
281
|
if (!lease) {
|
|
@@ -289,7 +289,7 @@ export class SessionRuntimeTransportBinding {
|
|
|
289
289
|
this.#causalCellProbes.delete(causalProbe.traceId);
|
|
290
290
|
onCausalResult?.(result);
|
|
291
291
|
}
|
|
292
|
-
: onCausalResult);
|
|
292
|
+
: onCausalResult, expectedLifetime);
|
|
293
293
|
}
|
|
294
294
|
catch (error) {
|
|
295
295
|
if (causalProbe)
|
|
@@ -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;
|
|
@@ -67,8 +67,23 @@ export function createApplicationAgentNavigator(options) {
|
|
|
67
67
|
return async (sessionName, paneId, source = "mouse") => {
|
|
68
68
|
const token = ++navigationToken;
|
|
69
69
|
const currentOwner = options.sessionOwner();
|
|
70
|
+
const snapshot = currentOwner.snapshot();
|
|
71
|
+
// A renderer rebind keeps its semantic control connection. Reserve this
|
|
72
|
+
// exact target synchronously; the controller fences and bounds first send.
|
|
73
|
+
const retainedControl = (() => {
|
|
74
|
+
try {
|
|
75
|
+
return (snapshot?.status === "rebinding" &&
|
|
76
|
+
snapshot.connection?.workspaceName === sessionName &&
|
|
77
|
+
snapshot.daemonGeneration &&
|
|
78
|
+
snapshot.client &&
|
|
79
|
+
Number.isSafeInteger(snapshot.client.getSnapshot().generation));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
70
85
|
if (currentOwner.sessionName() === sessionName &&
|
|
71
|
-
applicationGenerationNavigationKey(
|
|
86
|
+
(applicationGenerationNavigationKey(snapshot) !== null || retainedControl)) {
|
|
72
87
|
// Sidebar agents belong to the already-live workspace. Select their pane
|
|
73
88
|
// before returning control to OpenTUI so the very next key cannot leak to
|
|
74
89
|
// the pane that was focused before the click.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retainsAgentControl } from "./application-retained-agent.js";
|
|
1
2
|
import { createApplicationHomeAgentNavigator, } from "./application-home-agent-navigation.js";
|
|
2
3
|
import { applicationGenerationNavigationKey } from "./application-generation-starter.js";
|
|
3
4
|
import { waitForHomeAgentSemantic } from "./application-home-agents-owner.js";
|
|
@@ -16,6 +17,18 @@ export function createApplicationMachineAgentNavigator(options) {
|
|
|
16
17
|
opening = true;
|
|
17
18
|
const selected = () => options.selectedMachineId() === machineId;
|
|
18
19
|
const generation = () => selected() && options.generationMachineId() === machineId ? options.generation() : null;
|
|
20
|
+
if (selected() &&
|
|
21
|
+
options.isCurrentTarget(machineId, target) &&
|
|
22
|
+
options.sessionName() === target.sessionName &&
|
|
23
|
+
retainsAgentControl(generation(), target)) {
|
|
24
|
+
// Reserve before the next key; the terminal controller waits for a fresh
|
|
25
|
+
// coherent runtime and remains the only owner of physical input.
|
|
26
|
+
options.selectPane(target.paneId, source);
|
|
27
|
+
options.showTerminals(source);
|
|
28
|
+
options.setNote(null);
|
|
29
|
+
opening = false;
|
|
30
|
+
return Promise.resolve({ opened: true });
|
|
31
|
+
}
|
|
19
32
|
current = createApplicationHomeAgentNavigator({
|
|
20
33
|
isCurrentTarget: (row) => selected() && options.isCurrentTarget(machineId, row),
|
|
21
34
|
currentGeneration() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retainsAgentControl } from "./application-retained-agent.js";
|
|
1
2
|
import { fleetHostColor } from "./fleet-presentation.js";
|
|
2
3
|
import { createFleetTabs } from "./application-fleet-tabs.js";
|
|
3
4
|
import { saveMachineProfiles } from "../../../lib/local-fleet-request.js";
|
|
@@ -64,7 +65,7 @@ export function createApplicationMachineNavigation(options) {
|
|
|
64
65
|
const [tabRevision, setTabRevision] = createSignal(0);
|
|
65
66
|
const history = [...saved().recent].reverse();
|
|
66
67
|
let historyIndex = history.length - 1;
|
|
67
|
-
const select = (id, expectedLiveSessionId, server) => {
|
|
68
|
+
const select = (id, expectedLiveSessionId, server, retainedAgent) => {
|
|
68
69
|
if (!manager.getMachine(id))
|
|
69
70
|
return false;
|
|
70
71
|
if (manager.snapshot().selectedMachineId !== id) {
|
|
@@ -78,8 +79,15 @@ export function createApplicationMachineNavigation(options) {
|
|
|
78
79
|
manager.select(id);
|
|
79
80
|
}
|
|
80
81
|
else if (expectedLiveSessionId) {
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
const active = options.attachedGeneration?.() ?? null;
|
|
83
|
+
const retained = retainedAgent && retainsAgentControl(active, retainedAgent)
|
|
84
|
+
? {
|
|
85
|
+
liveSessionId: active.connection.liveSessionId,
|
|
86
|
+
daemonGeneration: active.daemonGeneration,
|
|
87
|
+
...(active.connection.server ? { server: active.connection.server } : {}),
|
|
88
|
+
}
|
|
89
|
+
: null;
|
|
90
|
+
const attached = options.attachedTarget?.() ?? attachedMachineNavigationTarget(active) ?? retained;
|
|
83
91
|
const sameAuthority = attached?.liveSessionId === expectedLiveSessionId &&
|
|
84
92
|
(server
|
|
85
93
|
? attached.server
|
|
@@ -302,7 +310,7 @@ export function createApplicationMachineNavigation(options) {
|
|
|
302
310
|
return;
|
|
303
311
|
}
|
|
304
312
|
options.cancelOpen();
|
|
305
|
-
if (!select(id, selectedSession?.liveSessionId, selectedSession?.server))
|
|
313
|
+
if (!select(id, selectedSession?.liveSessionId, selectedSession?.server, row))
|
|
306
314
|
return;
|
|
307
315
|
if (selectedSession)
|
|
308
316
|
setActiveSessionKey(selectedSession.id);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Selection intent may wait on this exact retained control; input cannot use it yet. */
|
|
2
|
+
export function retainsAgentControl(active, target) {
|
|
3
|
+
if (active?.status !== "rebinding" ||
|
|
4
|
+
!active.client ||
|
|
5
|
+
!target.paneId ||
|
|
6
|
+
active.daemonGeneration !== target.daemonInstanceId ||
|
|
7
|
+
active.connection?.workspaceName !== target.sessionName ||
|
|
8
|
+
active.connection.liveSessionId !== target.liveSessionId)
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
const state = active.client.getSnapshot();
|
|
12
|
+
return (Number.isSafeInteger(state.generation) &&
|
|
13
|
+
state.semantic?.sidebar.agents.some((agent) => agent.id === target.agentId && agent.paneId === target.paneId) === true);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|