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
package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rendererProbe } from "./renderer-probe.js";
|
|
2
|
+
import { createTerminalSelectionWaiter } from "./terminal-selection-waiter.js";
|
|
1
3
|
import { WorkspaceWindowLinkSelectResultSchemaZ } from "@tmux-ide/contracts";
|
|
2
4
|
import { windowLinkForPane, windowLinkTarget } from "./application-terminal-workspace-policy.js";
|
|
3
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -117,6 +119,10 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
117
119
|
left.generation === right.generation &&
|
|
118
120
|
left.workspace === right.workspace;
|
|
119
121
|
let selectedControl = null;
|
|
122
|
+
const selectionWaiter = createTerminalSelectionWaiter();
|
|
123
|
+
let observedLayoutEpoch = null;
|
|
124
|
+
let selectionGeneration = null;
|
|
125
|
+
let selectionLayout = options.layout();
|
|
120
126
|
const waitingInput = new Set();
|
|
121
127
|
let waitingInputBytes = 0;
|
|
122
128
|
let releasingInput = null;
|
|
@@ -522,8 +528,28 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
522
528
|
const paneInput = new TerminalPaneInputRouter({
|
|
523
529
|
select: async (paneId) => {
|
|
524
530
|
finishLinkSelection(false);
|
|
531
|
+
const initial = options.generation();
|
|
532
|
+
const selectionControl = controlIdentity(initial);
|
|
533
|
+
const selectionVersion = paneInput.selectionVersion;
|
|
534
|
+
const selectionCurrent = () => paneInput.selectionVersion === selectionVersion &&
|
|
535
|
+
sameControl(selectionControl, controlIdentity(selectionGeneration));
|
|
536
|
+
const panePresent = () => selectionLayout.windows.some((window) => window.panes.some((pane) => pane.pane === paneId));
|
|
537
|
+
if (initial?.status === "rebinding") {
|
|
538
|
+
// Reserve the optimistic target now, but send its FIRST mutation only
|
|
539
|
+
// after this same control connection has a coherent terminal runtime.
|
|
540
|
+
const ready = await selectionWaiter.wait(() => {
|
|
541
|
+
if (!selectionCurrent())
|
|
542
|
+
return false;
|
|
543
|
+
const active = selectionGeneration;
|
|
544
|
+
if (active?.status !== "live" || observedLayoutEpoch !== active.rendererEpoch)
|
|
545
|
+
return "pending";
|
|
546
|
+
return panePresent();
|
|
547
|
+
});
|
|
548
|
+
if (!ready || !selectionCurrent())
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
525
551
|
const expected = liveSelectionTarget();
|
|
526
|
-
|
|
552
|
+
let dispatchedRendererEpoch;
|
|
527
553
|
const selecting = pendingWindowSwitch;
|
|
528
554
|
const operationId = selecting?.traceId;
|
|
529
555
|
const selectionFailure = { current: null };
|
|
@@ -536,8 +562,36 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
536
562
|
: null;
|
|
537
563
|
}, paneId, operationId, (failure) => {
|
|
538
564
|
selectionFailure.current = failure;
|
|
539
|
-
}, windowLinkForPane(options.layout(), paneId) ?? undefined)
|
|
565
|
+
}, windowLinkForPane(options.layout(), paneId) ?? undefined, () => {
|
|
566
|
+
dispatchedRendererEpoch = options.generation()?.rendererEpoch;
|
|
567
|
+
})
|
|
540
568
|
: null;
|
|
569
|
+
if (receipt === null &&
|
|
570
|
+
selectionFailure.current?.reason === "topology-changed" &&
|
|
571
|
+
selectionCurrent()) {
|
|
572
|
+
// The daemon may have selected the pane before retiring its stream.
|
|
573
|
+
// Observe the replacement runtime; never resubmit an uncertain command
|
|
574
|
+
// or manufacture an operation receipt. Input still waits on this promise.
|
|
575
|
+
const confirmed = await selectionWaiter.wait(() => {
|
|
576
|
+
if (!selectionCurrent())
|
|
577
|
+
return false;
|
|
578
|
+
const active = selectionGeneration;
|
|
579
|
+
if (active?.status !== "live" ||
|
|
580
|
+
active.rendererEpoch === dispatchedRendererEpoch ||
|
|
581
|
+
observedLayoutEpoch !== active.rendererEpoch)
|
|
582
|
+
return "pending";
|
|
583
|
+
if (!panePresent())
|
|
584
|
+
return false;
|
|
585
|
+
const current = selectionLayout.current;
|
|
586
|
+
const activePanes = current?.panes.filter((pane) => pane.active) ?? [];
|
|
587
|
+
if (!current?.currentWindow || activePanes.length !== 1)
|
|
588
|
+
return "pending";
|
|
589
|
+
return activePanes[0]?.pane === paneId ? true : "pending";
|
|
590
|
+
});
|
|
591
|
+
if (selecting && pendingWindowSwitch === selecting)
|
|
592
|
+
pendingWindowSwitch = null;
|
|
593
|
+
return confirmed && selectionCurrent();
|
|
594
|
+
}
|
|
541
595
|
if (selecting && pendingWindowSwitch === selecting) {
|
|
542
596
|
const receiptAtMicros = options.detailedWindowSwitchTiming ? diagnosticNowMicros() : null;
|
|
543
597
|
if (selecting.timing)
|
|
@@ -685,22 +739,30 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
685
739
|
}
|
|
686
740
|
return outcome.status === "sent";
|
|
687
741
|
},
|
|
688
|
-
onFocusedPane:
|
|
742
|
+
onFocusedPane: (pane) => {
|
|
743
|
+
rendererProbe()?.cancel("selection-changed", nowMicros());
|
|
744
|
+
options.setFocusedPane(pane);
|
|
745
|
+
},
|
|
689
746
|
});
|
|
690
747
|
return {
|
|
691
748
|
cancelPendingInput() {
|
|
749
|
+
rendererProbe()?.cancel("input-cancelled", nowMicros());
|
|
750
|
+
selectionWaiter.cancel();
|
|
692
751
|
finishLinkSelection(false);
|
|
693
752
|
paneInput.invalidateSelection();
|
|
694
753
|
for (const entry of waitingInput)
|
|
695
754
|
entry.settle(false);
|
|
696
755
|
},
|
|
697
756
|
adoptGeneration(snapshot) {
|
|
757
|
+
selectionGeneration = snapshot;
|
|
698
758
|
const control = controlIdentity(snapshot);
|
|
699
759
|
if (!sameControl(selectedControl, control)) {
|
|
760
|
+
rendererProbe()?.cancel("control-changed", nowMicros());
|
|
700
761
|
finishLinkSelection(false);
|
|
701
762
|
paneInput.invalidateSelection();
|
|
702
763
|
}
|
|
703
764
|
selectedControl = control;
|
|
765
|
+
selectionWaiter.reconcile();
|
|
704
766
|
if (waitingInput.size > 0 && snapshot?.status === "live" && snapshot.fastLane) {
|
|
705
767
|
// Old waiters resume first; input admitted synchronously by the next
|
|
706
768
|
// layout publication must not overtake them in this microtask turn.
|
|
@@ -751,6 +813,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
751
813
|
next.fastLane !== inputAuthorityIdentity.fastLane ||
|
|
752
814
|
next.adapter !== inputAuthorityIdentity.adapter);
|
|
753
815
|
if (next === null || replaced) {
|
|
816
|
+
rendererProbe()?.cancel("generation-changed", nowMicros());
|
|
754
817
|
finishLinkSelection(false);
|
|
755
818
|
pendingWindowSwitch = null;
|
|
756
819
|
pendingWindowRename = null;
|
|
@@ -764,6 +827,9 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
764
827
|
inputAuthorityIdentity = next;
|
|
765
828
|
},
|
|
766
829
|
adoptLayout(snapshot) {
|
|
830
|
+
selectionLayout = snapshot;
|
|
831
|
+
observedLayoutEpoch = snapshot.rendererEpoch ?? null;
|
|
832
|
+
selectionWaiter.reconcile();
|
|
767
833
|
reconcileLinkSelection(snapshot);
|
|
768
834
|
paneInput.adoptCanonicalPane(snapshot.current ? paneForWindow(snapshot.current) : null);
|
|
769
835
|
const currentWindow = snapshot.current?.semanticWindowId ?? snapshot.current?.windowName;
|
|
@@ -850,6 +916,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
850
916
|
selectPane: (paneId) => {
|
|
851
917
|
finishLinkSelection(false);
|
|
852
918
|
paneInput.selectPane(paneId);
|
|
919
|
+
selectionWaiter.reconcile();
|
|
853
920
|
},
|
|
854
921
|
async unlinkWindowLink(target) {
|
|
855
922
|
const expected = liveSelectionTarget();
|
|
@@ -979,9 +1046,27 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
979
1046
|
}
|
|
980
1047
|
},
|
|
981
1048
|
sendInput: async (input, parserOrigin) => {
|
|
982
|
-
|
|
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;
|
|
983
1069
|
},
|
|
984
|
-
sendInputToPane: (paneId, input, parserOrigin) => paneInput.sendInputToPane(paneId, { input, parserOrigin }),
|
|
985
1070
|
beginResizePointerIngress(input) {
|
|
986
1071
|
if (!options.diagnosticsEnabled)
|
|
987
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);
|
|
@@ -371,7 +371,7 @@ export function createOpenTuiGenerationHost(sessionName, presentation, overrides
|
|
|
371
371
|
// desired state against the fresh socket on the following live publish.
|
|
372
372
|
publish({ ...snapshot, status: "rebinding" });
|
|
373
373
|
owner.stopPresentation?.();
|
|
374
|
-
owner.stopPresentation = runtime ? presentation.adopt(runtime) : null;
|
|
374
|
+
owner.stopPresentation = runtime ? presentation.adopt(runtime, rendererEpoch + 1) : null;
|
|
375
375
|
owner.runtime = runtime;
|
|
376
376
|
owner.authorityClient = runtime ? runtimeAuthorityClient(owner.bundle, runtime) : null;
|
|
377
377
|
if (!runtime)
|
|
@@ -394,7 +394,7 @@ export function createOpenTuiGenerationHost(sessionName, presentation, overrides
|
|
|
394
394
|
if (candidate !== owner)
|
|
395
395
|
return;
|
|
396
396
|
if (runtime) {
|
|
397
|
-
owner.stopPresentation = presentation.adopt(runtime);
|
|
397
|
+
owner.stopPresentation = presentation.adopt(runtime, rendererEpoch + 1);
|
|
398
398
|
diagnose?.("host-internal-snapshot-publication", {
|
|
399
399
|
publicationPhase: "presentation-adopted",
|
|
400
400
|
daemonGeneration: owner.bundle.connection.target.daemon.instanceId,
|
|
@@ -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
|
+
}
|
|
@@ -52,7 +52,7 @@ export function createOpenTuiRuntimeLayoutPresentation() {
|
|
|
52
52
|
listener(windows);
|
|
53
53
|
return () => windowListeners.delete(listener);
|
|
54
54
|
},
|
|
55
|
-
adopt(port) {
|
|
55
|
+
adopt(port, rendererEpoch) {
|
|
56
56
|
if (disposed)
|
|
57
57
|
return () => undefined;
|
|
58
58
|
let candidate = true;
|
|
@@ -60,15 +60,18 @@ export function createOpenTuiRuntimeLayoutPresentation() {
|
|
|
60
60
|
candidateStop = port.onLayout((layout) => {
|
|
61
61
|
if (disposed || !candidate || !layout.current)
|
|
62
62
|
return;
|
|
63
|
+
// A port may reuse its snapshot object. Own the provenance wrapper so
|
|
64
|
+
// a preparing runtime cannot relabel the retained coherent frame.
|
|
65
|
+
const presented = Object.freeze({ ...layout, rendererEpoch });
|
|
63
66
|
if (currentPort !== port) {
|
|
64
67
|
const previousStop = stopLayout;
|
|
65
68
|
currentPort = port;
|
|
66
69
|
stopLayout = candidateStop;
|
|
67
|
-
publish(
|
|
70
|
+
publish(presented);
|
|
68
71
|
previousStop?.();
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
|
-
publish(
|
|
74
|
+
publish(presented);
|
|
72
75
|
});
|
|
73
76
|
// `onLayout` intentionally seeds synchronously. Capture the disposer
|
|
74
77
|
// after that first callback has promoted this candidate.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Acquire controller authority before issuing the canonical pane-select intent. */
|
|
2
|
-
export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure, windowLink) {
|
|
2
|
+
export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure, windowLink, onDispatch) {
|
|
3
3
|
const fail = (failure) => {
|
|
4
4
|
try {
|
|
5
5
|
onFailure?.(failure);
|
|
@@ -20,7 +20,11 @@ export async function selectTerminalPane(expected, current, semanticPaneId, oper
|
|
|
20
20
|
: undefined;
|
|
21
21
|
return fail({
|
|
22
22
|
stage,
|
|
23
|
-
reason: code === "operation-timeout"
|
|
23
|
+
reason: code === "operation-timeout"
|
|
24
|
+
? "operation-timeout"
|
|
25
|
+
: code === "topology-changed" && stage === "dispatch"
|
|
26
|
+
? "topology-changed"
|
|
27
|
+
: "transport-rejected",
|
|
24
28
|
...(backendReason ? { backendReason } : {}),
|
|
25
29
|
});
|
|
26
30
|
};
|
|
@@ -50,6 +54,7 @@ export async function selectTerminalPane(expected, current, semanticPaneId, oper
|
|
|
50
54
|
return fail({ stage: "pre-dispatch", reason: "generation-replaced" });
|
|
51
55
|
let result;
|
|
52
56
|
try {
|
|
57
|
+
onDispatch?.();
|
|
53
58
|
result = await expected.client.dispatch({
|
|
54
59
|
kind: "semantic-intent",
|
|
55
60
|
...(operationId ? { operationId } : {}),
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One bounded selection observation; never submits or repeats a mutation. */
|
|
2
|
+
export function createTerminalSelectionWaiter() {
|
|
3
|
+
let pending = null;
|
|
4
|
+
const cancel = () => pending?.finish(false);
|
|
5
|
+
const reconcile = () => {
|
|
6
|
+
const waiting = pending;
|
|
7
|
+
if (!waiting)
|
|
8
|
+
return;
|
|
9
|
+
const result = waiting.check();
|
|
10
|
+
if (result !== "pending")
|
|
11
|
+
waiting.finish(result);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
cancel,
|
|
15
|
+
reconcile,
|
|
16
|
+
wait(check) {
|
|
17
|
+
cancel();
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const timer = setTimeout(() => waiting.finish(false), 5000);
|
|
20
|
+
timer.unref?.();
|
|
21
|
+
const waiting = {
|
|
22
|
+
check,
|
|
23
|
+
finish(selected) {
|
|
24
|
+
if (pending !== waiting)
|
|
25
|
+
return;
|
|
26
|
+
pending = null;
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
resolve(selected);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
pending = waiting;
|
|
32
|
+
reconcile();
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -95,6 +95,10 @@ export class SemanticPaneReplica {
|
|
|
95
95
|
: [...this.#dirtyRows].sort((a, b) => a - b);
|
|
96
96
|
}
|
|
97
97
|
accept(message) {
|
|
98
|
+
if (message.type === "terminal.projection") {
|
|
99
|
+
this.#fail("protocol-violation", message.transactionId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
98
102
|
if (message.type === "terminal.delivery.fault") {
|
|
99
103
|
this.#assembler = null;
|
|
100
104
|
this.#snapshot = null;
|
|
@@ -8,7 +8,8 @@ import { createGuardedNativeSplitResize } from "./guarded-native-split-resize.ts
|
|
|
8
8
|
import { WindowLinkTargetSchemaZ } from "@tmux-ide/contracts";
|
|
9
9
|
import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.ts";
|
|
10
10
|
import { requireSupportedTmuxVersion } from "./tmux-version.ts";
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { LocalImageOwner, cleanupLocalImageOwner } from "../terminal/graphics/local-image-owner.ts";
|
|
12
13
|
import { createClaudeTeamMembershipReader } from "../terminal/attachments/claude-team-names.ts";
|
|
13
14
|
import { PaneSourceDiscovery } from "./pane-source-discovery.ts";
|
|
14
15
|
import { createBackgroundNativeCapture } from "./background-native-capture.ts";
|
|
@@ -159,6 +160,7 @@ import { captureDaemonProvenance } from "./daemon-provenance.ts";
|
|
|
159
160
|
import { registerLogSecret, setLogIdentity } from "./log.ts";
|
|
160
161
|
import {
|
|
161
162
|
canonicalDaemonUrl,
|
|
163
|
+
getCanonicalDaemonInfoPath,
|
|
162
164
|
clearCanonicalDaemonInfoIfOwned,
|
|
163
165
|
clearCanonicalDaemonInfoIfUnchanged,
|
|
164
166
|
inspectCanonicalDaemonInfo,
|
|
@@ -1399,6 +1401,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1399
1401
|
};
|
|
1400
1402
|
let terminalAttachmentRuntime: NativeTerminalAttachmentRuntime | null = null;
|
|
1401
1403
|
let paneStreamRuntime: PaneStreamRuntime | null = null;
|
|
1404
|
+
let localImages: LocalImageOwner | undefined;
|
|
1402
1405
|
let serverOwners: Awaited<ReturnType<typeof createEmbeddedTmuxServerOwners>> | null = null;
|
|
1403
1406
|
let defaultEditorContextRetired = false;
|
|
1404
1407
|
const defaultDiagnosticsLifetime = new AbortController();
|
|
@@ -1732,7 +1735,34 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1732
1735
|
};
|
|
1733
1736
|
const peekTerminalAttachmentRuntime = (): NativeTerminalAttachmentRuntime | null =>
|
|
1734
1737
|
terminalAttachmentRuntime;
|
|
1738
|
+
// The held canonical claim has already proven and hardened this private root.
|
|
1739
|
+
// Local images are optional: unavailable storage retains inline graphics.
|
|
1740
|
+
if (process.platform !== "win32") {
|
|
1741
|
+
const root = dirname(getCanonicalDaemonInfoPath());
|
|
1742
|
+
try {
|
|
1743
|
+
if (existingCanonical.status === "valid") {
|
|
1744
|
+
await cleanupLocalImageOwner({
|
|
1745
|
+
root,
|
|
1746
|
+
owner: {
|
|
1747
|
+
generation: existingCanonical.info.instanceId,
|
|
1748
|
+
pid: existingCanonical.info.pid,
|
|
1749
|
+
startedAt: existingCanonical.info.startedAt,
|
|
1750
|
+
},
|
|
1751
|
+
proveDead: () => isCanonicalDaemonRecordOwnerProvenDead(existingCanonical),
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
localImages = await LocalImageOwner.create({
|
|
1755
|
+
root,
|
|
1756
|
+
generation: instanceId,
|
|
1757
|
+
pid: process.pid,
|
|
1758
|
+
startedAt,
|
|
1759
|
+
});
|
|
1760
|
+
} catch {
|
|
1761
|
+
localImages = undefined;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1735
1764
|
paneStreamRuntime = createPaneStreamRuntime({
|
|
1765
|
+
localImages,
|
|
1736
1766
|
daemonInstanceId: instanceId,
|
|
1737
1767
|
webSocketUrl: paneStreamWebSocketUrl(bindHostname, port),
|
|
1738
1768
|
sessionRuntimeRegistry,
|
|
@@ -1758,6 +1788,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
1758
1788
|
});
|
|
1759
1789
|
await externalInteractionObserver.start();
|
|
1760
1790
|
serverOwners = await createEmbeddedTmuxServerOwners({
|
|
1791
|
+
localImages,
|
|
1761
1792
|
environmentId,
|
|
1762
1793
|
onDefaultScope: async (scope) => {
|
|
1763
1794
|
interactionEvidence = new InteractionEvidenceAuthority(environmentId, scope);
|
|
@@ -2075,7 +2106,10 @@ async function startEmbeddedDaemonGeneration(
|
|
|
2075
2106
|
// The pane-stream coordinator may still hold runtime consumers while it
|
|
2076
2107
|
// drains. Preserve the normal shutdown order on startup rollback too:
|
|
2077
2108
|
// transports first, then the one session/control authority.
|
|
2078
|
-
await Promise.allSettled([
|
|
2109
|
+
await Promise.allSettled([
|
|
2110
|
+
sessionRuntimeRegistry?.dispose() ?? Promise.resolve(),
|
|
2111
|
+
localImages?.dispose() ?? Promise.resolve(),
|
|
2112
|
+
]);
|
|
2079
2113
|
throw error;
|
|
2080
2114
|
}
|
|
2081
2115
|
const {
|
|
@@ -2108,6 +2142,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
2108
2142
|
)),
|
|
2109
2143
|
...(await retirePaneStreamTransport()),
|
|
2110
2144
|
];
|
|
2145
|
+
await localImages?.dispose();
|
|
2111
2146
|
terminalInventoryRuntime.dispose();
|
|
2112
2147
|
const paneDisposal = Promise.resolve().then(() => workspacePaneCreation.dispose());
|
|
2113
2148
|
const workspaceOpenDisposal = Promise.resolve().then(() => workspaceOpen.dispose());
|
|
@@ -2375,6 +2410,7 @@ async function startEmbeddedDaemonGeneration(
|
|
|
2375
2410
|
)),
|
|
2376
2411
|
);
|
|
2377
2412
|
failures.push(...(await retirePaneStreamTransport()));
|
|
2413
|
+
await capture(() => localImages?.dispose());
|
|
2378
2414
|
await capture(() => terminalInventoryRuntime.dispose());
|
|
2379
2415
|
await capture(() => workspacePaneCreation.dispose());
|
|
2380
2416
|
await capture(() => workspaceOpen.dispose());
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LocalImageOwner } from "../terminal/graphics/local-image-owner.ts";
|
|
1
2
|
import type { Server, IncomingMessage } from "node:http";
|
|
2
3
|
import type { Socket } from "node:net";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
|
|
23
24
|
/** Adapts the established default owner without duplicating its control clients. */
|
|
24
25
|
export async function createEmbeddedTmuxServerOwners(options: {
|
|
26
|
+
readonly localImages?: LocalImageOwner;
|
|
25
27
|
readonly environmentId: string;
|
|
26
28
|
readonly onDefaultScope?: (scope: TmuxServerScope) => Promise<void>;
|
|
27
29
|
readonly defaultAuthority: WorkspacePaneTmuxAuthority;
|
|
@@ -89,6 +91,7 @@ export async function createEmbeddedTmuxServerOwners(options: {
|
|
|
89
91
|
if (registration.serverId === defaultRegistration.serverId) await retireDefault();
|
|
90
92
|
const route = tmuxServerPaneStreamPath(scope);
|
|
91
93
|
const owner = await createNativeTmuxServerOwner({
|
|
94
|
+
localImages: options.localImages,
|
|
92
95
|
environmentId: options.environmentId,
|
|
93
96
|
...scope,
|
|
94
97
|
tmuxAuthority: observation.authority,
|