tmux-ide 2.9.4 → 2.9.5
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 +4354 -3376
- package/package.json +7 -6
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- 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/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stock control-mode output may overtake a capture reply behind sibling output.
|
|
3
|
+
* An observed pause is the lower fence for a NOHOOKS capture/cursor/continue
|
|
4
|
+
* transaction: every subsequent target byte must follow snapshot metadata.
|
|
5
|
+
*
|
|
6
|
+
* This object owns only that attempt's delivery contract. It neither sends tmux
|
|
7
|
+
* commands nor authenticates the snapshot collector. Its caller must prove the
|
|
8
|
+
* uninterrupted command transaction and retain the connection's collector lease
|
|
9
|
+
* until cancelled wire work has drained. SessionChannel owns this lifecycle.
|
|
10
|
+
*/
|
|
11
|
+
export class StockPaneSnapshot {
|
|
12
|
+
static MAX_BYTES = 1024 * 1024;
|
|
13
|
+
static MAX_CHUNKS = 512;
|
|
14
|
+
phase = "awaiting-pause";
|
|
15
|
+
failure = null;
|
|
16
|
+
context;
|
|
17
|
+
participants;
|
|
18
|
+
maxBytes;
|
|
19
|
+
maxChunks;
|
|
20
|
+
chunks = [];
|
|
21
|
+
bytes = 0;
|
|
22
|
+
constructor(context, limits = {}) {
|
|
23
|
+
if (!/^%(?:0|[1-9][0-9]*)$/u.test(context.paneId) ||
|
|
24
|
+
!Number.isSafeInteger(context.incarnation) ||
|
|
25
|
+
context.incarnation < 0 ||
|
|
26
|
+
!Number.isSafeInteger(context.layoutGeneration) ||
|
|
27
|
+
context.layoutGeneration < 0 ||
|
|
28
|
+
context.participants.length === 0 ||
|
|
29
|
+
new Set(context.participants).size !== context.participants.length ||
|
|
30
|
+
context.participants.some((participant) => participant === null || typeof participant !== "object"))
|
|
31
|
+
throw new TypeError("stock snapshot requires exact pane and participant identities");
|
|
32
|
+
this.maxBytes = limits.maxBytes ?? StockPaneSnapshot.MAX_BYTES;
|
|
33
|
+
this.maxChunks = limits.maxChunks ?? StockPaneSnapshot.MAX_CHUNKS;
|
|
34
|
+
if (!Number.isSafeInteger(this.maxBytes) ||
|
|
35
|
+
this.maxBytes < 1 ||
|
|
36
|
+
this.maxBytes > StockPaneSnapshot.MAX_BYTES ||
|
|
37
|
+
!Number.isSafeInteger(this.maxChunks) ||
|
|
38
|
+
this.maxChunks < 1 ||
|
|
39
|
+
this.maxChunks > StockPaneSnapshot.MAX_CHUNKS)
|
|
40
|
+
throw new RangeError("stock snapshot buffer limits must be positive and bounded");
|
|
41
|
+
this.context = { ...context, participants: [...context.participants] };
|
|
42
|
+
this.participants = new Set(this.context.participants);
|
|
43
|
+
}
|
|
44
|
+
get state() {
|
|
45
|
+
return this.phase;
|
|
46
|
+
}
|
|
47
|
+
get failureReason() {
|
|
48
|
+
return this.failure;
|
|
49
|
+
}
|
|
50
|
+
get bufferedBytes() {
|
|
51
|
+
return this.bytes;
|
|
52
|
+
}
|
|
53
|
+
get bufferedChunks() {
|
|
54
|
+
return this.chunks.length;
|
|
55
|
+
}
|
|
56
|
+
matchesContext(context) {
|
|
57
|
+
if (this.phase === "invalidated")
|
|
58
|
+
return false;
|
|
59
|
+
if (context.paneId !== this.context.paneId ||
|
|
60
|
+
context.incarnation !== this.context.incarnation ||
|
|
61
|
+
context.layoutGeneration !== this.context.layoutGeneration ||
|
|
62
|
+
context.participants.length !== this.participants.size ||
|
|
63
|
+
new Set(context.participants).size !== this.participants.size ||
|
|
64
|
+
context.participants.some((participant) => !this.participants.has(participant))) {
|
|
65
|
+
this.invalidate("context-changed");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
current(context) {
|
|
71
|
+
return this.phase !== "committed" && this.matchesContext(context);
|
|
72
|
+
}
|
|
73
|
+
readCurrentContext(read) {
|
|
74
|
+
try {
|
|
75
|
+
return this.current(read());
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
this.invalidate("context-unavailable");
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Only an actual current connection's pause notification establishes the fence. */
|
|
83
|
+
observePause(context) {
|
|
84
|
+
if (!this.current(context))
|
|
85
|
+
return false;
|
|
86
|
+
if (this.phase !== "awaiting-pause") {
|
|
87
|
+
this.invalidate("repaused");
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
this.phase = "buffering";
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
acceptOutput(paneId, data, context) {
|
|
94
|
+
if (paneId !== this.context.paneId)
|
|
95
|
+
return "unrelated";
|
|
96
|
+
if (!this.matchesContext(context))
|
|
97
|
+
return "invalid";
|
|
98
|
+
if (this.phase === "committed")
|
|
99
|
+
return "live";
|
|
100
|
+
// The caller fences all participating feeds before requesting pause. These
|
|
101
|
+
// bytes precede the observed pause and are represented by the future seed.
|
|
102
|
+
if (this.phase === "awaiting-pause")
|
|
103
|
+
return "discarded";
|
|
104
|
+
if (data.byteLength === 0)
|
|
105
|
+
return "held";
|
|
106
|
+
if (this.chunks.length >= this.maxChunks || this.bytes + data.byteLength > this.maxBytes) {
|
|
107
|
+
this.invalidate("overflow");
|
|
108
|
+
return "invalid";
|
|
109
|
+
}
|
|
110
|
+
this.chunks.push(data.slice());
|
|
111
|
+
this.bytes += data.byteLength;
|
|
112
|
+
return "held";
|
|
113
|
+
}
|
|
114
|
+
/** Revoke future routing even after commit; delivered events cannot be retracted. */
|
|
115
|
+
invalidate(reason = "cancelled") {
|
|
116
|
+
if (this.phase === "invalidated")
|
|
117
|
+
return;
|
|
118
|
+
this.phase = "invalidated";
|
|
119
|
+
this.failure = reason;
|
|
120
|
+
this.chunks = [];
|
|
121
|
+
this.bytes = 0;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Publish an authenticated snapshot, then replay each retained chunk to every
|
|
125
|
+
* exact participant. Context is rechecked around callbacks. A callback may
|
|
126
|
+
* append more output, which joins this same bounded replay before commit.
|
|
127
|
+
* Failure cannot retract already delivered events: the caller must retire or
|
|
128
|
+
* reseed participants when false is returned, never release their live feeds.
|
|
129
|
+
*/
|
|
130
|
+
publish(batches, currentContext, emit) {
|
|
131
|
+
if (!this.readCurrentContext(currentContext))
|
|
132
|
+
return false;
|
|
133
|
+
if (this.phase !== "buffering") {
|
|
134
|
+
this.invalidate(this.phase === "awaiting-pause" ? "pause-not-observed" : "invalid-snapshot");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (batches.length !== this.participants.size ||
|
|
138
|
+
new Set(batches.map((batch) => batch.participant)).size !== this.participants.size ||
|
|
139
|
+
batches.some(({ participant, events }) => !this.participants.has(participant) ||
|
|
140
|
+
events.length !== 3 ||
|
|
141
|
+
events[0]?.type !== "reset" ||
|
|
142
|
+
events[1]?.type !== "seed" ||
|
|
143
|
+
events[2]?.type !== "cursor")) {
|
|
144
|
+
this.invalidate("invalid-snapshot");
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
this.phase = "publishing";
|
|
148
|
+
const deliver = (participant, event) => {
|
|
149
|
+
if (!this.readCurrentContext(currentContext))
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
emit(participant, event);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
this.invalidate("consumer-error");
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return this.readCurrentContext(currentContext);
|
|
159
|
+
};
|
|
160
|
+
for (const { participant, events } of batches)
|
|
161
|
+
for (const event of events)
|
|
162
|
+
if (!deliver(participant, event))
|
|
163
|
+
return false;
|
|
164
|
+
for (let index = 0; index < this.chunks.length; index += 1) {
|
|
165
|
+
const data = this.chunks[index];
|
|
166
|
+
for (const participant of this.context.participants) {
|
|
167
|
+
// A subscriber cannot mutate the bytes replayed to its siblings.
|
|
168
|
+
if (!deliver(participant, { type: "delta", data: data.slice() }))
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.chunks = [];
|
|
173
|
+
this.bytes = 0;
|
|
174
|
+
this.phase = "committed";
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -25,7 +25,7 @@ function colorsEqual(left, right) {
|
|
|
25
25
|
(left.kind === "indexed" && right.kind === "indexed" && left.index === right.index) ||
|
|
26
26
|
(left.kind === "rgb" && right.kind === "rgb" && left.value === right.value)));
|
|
27
27
|
}
|
|
28
|
-
function cellsEqual(left, right) {
|
|
28
|
+
export function cellsEqual(left, right) {
|
|
29
29
|
return (left === right ||
|
|
30
30
|
(left !== undefined &&
|
|
31
31
|
right !== undefined &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
|
|
1
2
|
import { SessionRuntimeClientIdSchemaZ, SessionRuntimeAuthorityLeaseSchemaZ, SessionRuntimeControllerLeaseSchemaZ, SessionRuntimeGenerationSchemaZ, SessionRuntimeSemanticIntentSchemaZ, SessionRuntimeTerminalInputSchemaZ, CausalCellProbeV1SchemaZ, } from "@tmux-ide/contracts";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -19,6 +20,21 @@ async function abortable(promise, signal) {
|
|
|
19
20
|
void promise.then(resolve, reject).then(() => signal.removeEventListener("abort", onAbort), () => signal.removeEventListener("abort", onAbort));
|
|
20
21
|
});
|
|
21
22
|
}
|
|
23
|
+
const emptyListenerDisposal = () => ({
|
|
24
|
+
sessions: 0,
|
|
25
|
+
owners: 0,
|
|
26
|
+
failures: 0,
|
|
27
|
+
canonical: 0,
|
|
28
|
+
raw: 0,
|
|
29
|
+
upstream: 0,
|
|
30
|
+
authority: 0,
|
|
31
|
+
consumerHandles: 0,
|
|
32
|
+
deliveryConnections: 0,
|
|
33
|
+
deliverySources: 0,
|
|
34
|
+
pendingDeliverySources: 0,
|
|
35
|
+
pendingSourceCloses: 0,
|
|
36
|
+
pendingDeliveryClients: 0,
|
|
37
|
+
});
|
|
22
38
|
function authoredOriginForSurface(surface) {
|
|
23
39
|
if (surface === "opentui")
|
|
24
40
|
return "tui";
|
|
@@ -56,6 +72,7 @@ export class SessionRuntimeRegistry {
|
|
|
56
72
|
#observability;
|
|
57
73
|
#createTraceCorrelator;
|
|
58
74
|
#sessions = new Map();
|
|
75
|
+
#listenerDisposal = null;
|
|
59
76
|
#proofPrewarmOwnership = new Map();
|
|
60
77
|
#trustedInventoryTokens = new WeakMap();
|
|
61
78
|
#executionHandles = new WeakMap();
|
|
@@ -81,14 +98,30 @@ export class SessionRuntimeRegistry {
|
|
|
81
98
|
...(this.#observability.enabled
|
|
82
99
|
? {
|
|
83
100
|
nowMicros: () => this.#observability.nowMicros(),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.#
|
|
89
|
-
?
|
|
90
|
-
:
|
|
91
|
-
|
|
101
|
+
captureInputWrite: (session, action, semanticPaneId) => {
|
|
102
|
+
const runtime = this.#sessions.get(session);
|
|
103
|
+
const binding = runtime?.captureInputDispatch(semanticPaneId);
|
|
104
|
+
return (startedAtMicros, endedAtMicros, pendingBeforeSend, paneCurrent) => {
|
|
105
|
+
const incarnation = paneCurrent && this.#sessions.get(session) === runtime
|
|
106
|
+
? (binding?.() ?? null)
|
|
107
|
+
: null;
|
|
108
|
+
for (const traceId of action.traceIds ?? []) {
|
|
109
|
+
const trace = this.#observability.beginTrace("terminal-input-to-paint", {
|
|
110
|
+
generation: this.generation,
|
|
111
|
+
incarnation,
|
|
112
|
+
controlDispatch: Object.freeze({
|
|
113
|
+
workspaceName: session,
|
|
114
|
+
nativePaneId: action.pane,
|
|
115
|
+
semanticPaneId: paneCurrent ? semanticPaneId : null,
|
|
116
|
+
scope: "dispatch-time",
|
|
117
|
+
}),
|
|
118
|
+
}, traceId);
|
|
119
|
+
this.#observability.recordSpan("tmux", "control-write", startedAtMicros, endedAtMicros, trace);
|
|
120
|
+
this.#observability.recordSpan("tmux", pendingBeforeSend === 0
|
|
121
|
+
? "control-queue-empty-at-send"
|
|
122
|
+
: "control-queue-nonempty-at-send", endedAtMicros, endedAtMicros, trace);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
92
125
|
},
|
|
93
126
|
}
|
|
94
127
|
: {}),
|
|
@@ -572,6 +605,8 @@ export class SessionRuntimeRegistry {
|
|
|
572
605
|
return Object.freeze({
|
|
573
606
|
generation: this.generation,
|
|
574
607
|
controlChannels: this.activeControlChannelCount(),
|
|
608
|
+
mirrorListeners: this.#mirror.qualificationListeners(),
|
|
609
|
+
listenerDisposal: this.#listenerDisposal,
|
|
575
610
|
controllerLeases: this.activeControllerLeaseCount(),
|
|
576
611
|
sessions: Object.freeze([...this.#sessions.values()].map((runtime) => runtime.qualificationSnapshot())),
|
|
577
612
|
mutations: this.#semanticMutations?.metrics() ?? null,
|
|
@@ -584,7 +619,31 @@ export class SessionRuntimeRegistry {
|
|
|
584
619
|
this.#stopExitObserver();
|
|
585
620
|
this.#disposePromise = (async () => {
|
|
586
621
|
await this.#semanticMutations?.dispose();
|
|
587
|
-
|
|
622
|
+
const runtimes = [...this.#sessions.values()];
|
|
623
|
+
const results = await Promise.allSettled(runtimes.map((runtime) => runtime.dispose()));
|
|
624
|
+
const final = { ...emptyListenerDisposal(), sessions: runtimes.length };
|
|
625
|
+
runtimes.forEach((runtime, index) => {
|
|
626
|
+
const counts = runtime.listenerDisposal;
|
|
627
|
+
if (!counts || results[index]?.status === "rejected")
|
|
628
|
+
final.failures += 1;
|
|
629
|
+
if (counts)
|
|
630
|
+
for (const key of [
|
|
631
|
+
"owners",
|
|
632
|
+
"failures",
|
|
633
|
+
"canonical",
|
|
634
|
+
"raw",
|
|
635
|
+
"upstream",
|
|
636
|
+
"authority",
|
|
637
|
+
"consumerHandles",
|
|
638
|
+
"deliveryConnections",
|
|
639
|
+
"deliverySources",
|
|
640
|
+
"pendingDeliverySources",
|
|
641
|
+
"pendingSourceCloses",
|
|
642
|
+
"pendingDeliveryClients",
|
|
643
|
+
])
|
|
644
|
+
final[key] += counts[key];
|
|
645
|
+
});
|
|
646
|
+
this.#listenerDisposal = Object.freeze(final);
|
|
588
647
|
this.#sessions.clear();
|
|
589
648
|
this.#proofPrewarmOwnership.clear();
|
|
590
649
|
await this.#mirror.dispose();
|
|
@@ -661,6 +720,7 @@ class SessionRuntime {
|
|
|
661
720
|
session;
|
|
662
721
|
#mirror;
|
|
663
722
|
#consumers = new Set();
|
|
723
|
+
listenerDisposal = null;
|
|
664
724
|
#consumersByClientId = new Map();
|
|
665
725
|
#terminalReplicas = new Map();
|
|
666
726
|
#terminalReplicaClocks = new Map();
|
|
@@ -786,11 +846,40 @@ class SessionRuntime {
|
|
|
786
846
|
hasConsumers() {
|
|
787
847
|
return this.#consumers.size > 0;
|
|
788
848
|
}
|
|
849
|
+
listenerCounts() {
|
|
850
|
+
const counts = {
|
|
851
|
+
authority: this.#authorityListeners.size,
|
|
852
|
+
mirrorSubscriptions: 0,
|
|
853
|
+
replicaSubscriptions: 0,
|
|
854
|
+
deliveryConnections: 0,
|
|
855
|
+
};
|
|
856
|
+
for (const consumer of this.#consumers) {
|
|
857
|
+
const handles = consumer.qualificationListeners();
|
|
858
|
+
counts.mirrorSubscriptions += handles.mirrorSubscriptions;
|
|
859
|
+
counts.replicaSubscriptions += handles.replicaSubscriptions;
|
|
860
|
+
counts.deliveryConnections += handles.deliveryConnections;
|
|
861
|
+
}
|
|
862
|
+
return Object.freeze(counts);
|
|
863
|
+
}
|
|
864
|
+
captureInputDispatch(semanticPaneId) {
|
|
865
|
+
const owner = this.#disposed || semanticPaneId === null
|
|
866
|
+
? undefined
|
|
867
|
+
: this.#terminalReplicas.get(semanticPaneId);
|
|
868
|
+
const incarnation = owner?.inputDispatchIncarnation() ?? null;
|
|
869
|
+
return () => !this.#disposed &&
|
|
870
|
+
semanticPaneId !== null &&
|
|
871
|
+
owner !== undefined &&
|
|
872
|
+
this.#terminalReplicas.get(semanticPaneId) === owner &&
|
|
873
|
+
owner.inputDispatchIncarnation() === incarnation
|
|
874
|
+
? incarnation
|
|
875
|
+
: null;
|
|
876
|
+
}
|
|
789
877
|
qualificationSnapshot() {
|
|
790
878
|
return Object.freeze({
|
|
791
879
|
session: this.session,
|
|
792
880
|
consumers: this.#consumers.size,
|
|
793
881
|
retained: this.#retention !== null,
|
|
882
|
+
listeners: this.listenerCounts(),
|
|
794
883
|
delivery: this.#terminalDeliveryHub.metrics(),
|
|
795
884
|
convergence: this.#terminalDeliveryHub.convergenceSnapshot(),
|
|
796
885
|
replicas: Object.freeze(Object.fromEntries([...this.#terminalReplicas.entries()].map(([paneId, owner]) => [
|
|
@@ -1148,9 +1237,11 @@ class SessionRuntime {
|
|
|
1148
1237
|
current.epoch += 1;
|
|
1149
1238
|
this.#terminalReplicaClocks.set(semanticPaneId, current);
|
|
1150
1239
|
},
|
|
1151
|
-
onFault: () => {
|
|
1240
|
+
onFault: (error) => {
|
|
1152
1241
|
if (this.#terminalReplicas.get(semanticPaneId) !== candidate)
|
|
1153
1242
|
return;
|
|
1243
|
+
if (error instanceof StockCaptureTabUnavailableError)
|
|
1244
|
+
this.#terminalDeliveryHub.failPaneCapture(semanticPaneId, error);
|
|
1154
1245
|
this.#outputTraces?.clearPane(semanticPaneId);
|
|
1155
1246
|
this.#terminalReplicas.delete(semanticPaneId);
|
|
1156
1247
|
this.#restartBarrier = this.#restartBarrier.then(async () => {
|
|
@@ -1248,20 +1339,49 @@ class SessionRuntime {
|
|
|
1248
1339
|
this.#trustedInventoryRuntimeSessionId = null;
|
|
1249
1340
|
this.#trustedInventoryEpoch += 1;
|
|
1250
1341
|
const consumers = [...this.#consumers];
|
|
1251
|
-
await Promise.allSettled(consumers.map((consumer) => consumer.close()));
|
|
1342
|
+
const consumerResults = await Promise.allSettled(consumers.map((consumer) => consumer.close()));
|
|
1252
1343
|
this.#clearController();
|
|
1253
1344
|
this.#authority.dispose();
|
|
1254
1345
|
this.#authorityListeners.clear();
|
|
1255
1346
|
this.#completedHandoffs.clear();
|
|
1256
1347
|
this.#releasedLeases.clear();
|
|
1257
1348
|
this.#outputTraces?.clear();
|
|
1258
|
-
|
|
1349
|
+
const owners = [...this.#terminalReplicas.values()];
|
|
1350
|
+
const results = await Promise.allSettled(owners.map((owner) => owner.dispose("runtime-disposed")));
|
|
1351
|
+
const final = {
|
|
1352
|
+
...emptyListenerDisposal(),
|
|
1353
|
+
sessions: 1,
|
|
1354
|
+
owners: owners.length,
|
|
1355
|
+
failures: [...results, ...consumerResults].filter((result) => result.status === "rejected")
|
|
1356
|
+
.length,
|
|
1357
|
+
};
|
|
1358
|
+
for (const owner of owners) {
|
|
1359
|
+
const counts = owner.qualificationSnapshot().listeners;
|
|
1360
|
+
final.canonical += counts.canonical;
|
|
1361
|
+
final.raw += counts.raw;
|
|
1362
|
+
final.upstream += counts.upstream;
|
|
1363
|
+
}
|
|
1364
|
+
for (const consumer of consumers) {
|
|
1365
|
+
const counts = consumer.qualificationListeners();
|
|
1366
|
+
final.failures += counts.closeFailures;
|
|
1367
|
+
final.consumerHandles +=
|
|
1368
|
+
counts.mirrorSubscriptions + counts.replicaSubscriptions + counts.deliveryConnections;
|
|
1369
|
+
}
|
|
1370
|
+
final.authority = this.#authorityListeners.size;
|
|
1259
1371
|
this.#terminalReplicas.clear();
|
|
1260
1372
|
await this.#terminalDeliveryHub.close();
|
|
1261
1373
|
await this.#startPromise?.catch(() => undefined);
|
|
1262
1374
|
await this.#restartBarrier;
|
|
1263
1375
|
await this.#retention?.close();
|
|
1264
1376
|
this.#retention = null;
|
|
1377
|
+
const delivery = this.#terminalDeliveryHub.metrics();
|
|
1378
|
+
final.deliveryConnections = delivery.connections;
|
|
1379
|
+
final.deliverySources = delivery.sourceSubscriptions;
|
|
1380
|
+
final.pendingDeliverySources = delivery.pendingSourceSubscriptions;
|
|
1381
|
+
final.pendingSourceCloses = delivery.pendingSourceCloses;
|
|
1382
|
+
final.pendingDeliveryClients = delivery.pendingClients;
|
|
1383
|
+
final.failures += delivery.sourceCloseFailures;
|
|
1384
|
+
this.listenerDisposal = Object.freeze(final);
|
|
1265
1385
|
}
|
|
1266
1386
|
#assertConnected(clientId) {
|
|
1267
1387
|
if (!this.#consumersByClientId.has(clientId)) {
|
|
@@ -1472,6 +1592,15 @@ class SessionRuntimeConsumerImpl {
|
|
|
1472
1592
|
this.#deliveryConnections.add(connection);
|
|
1473
1593
|
return connection;
|
|
1474
1594
|
}
|
|
1595
|
+
#closeFailures = 0;
|
|
1596
|
+
qualificationListeners() {
|
|
1597
|
+
return Object.freeze({
|
|
1598
|
+
closeFailures: this.#closeFailures,
|
|
1599
|
+
mirrorSubscriptions: this.#subscriptions.size,
|
|
1600
|
+
replicaSubscriptions: this.#replicaSubscriptions.size,
|
|
1601
|
+
deliveryConnections: this.#deliveryConnections.size,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1475
1604
|
async close() {
|
|
1476
1605
|
if (this.#closed)
|
|
1477
1606
|
return;
|
|
@@ -1485,9 +1614,10 @@ class SessionRuntimeConsumerImpl {
|
|
|
1485
1614
|
// Authority retirement is synchronous. A slow/frozen mirror subscription
|
|
1486
1615
|
// must never keep controller leases or previously issued handles alive.
|
|
1487
1616
|
this.#runtime.release(this);
|
|
1488
|
-
await Promise.allSettled(subscriptions.map((subscription) => subscription.close()));
|
|
1489
|
-
await Promise.allSettled(replicaSubscriptions.map((subscription) => subscription.close()));
|
|
1490
|
-
await Promise.allSettled(deliveryConnections.map((connection) => connection.close()));
|
|
1617
|
+
const mirrorResults = await Promise.allSettled(subscriptions.map((subscription) => subscription.close()));
|
|
1618
|
+
const replicaResults = await Promise.allSettled(replicaSubscriptions.map((subscription) => subscription.close()));
|
|
1619
|
+
const deliveryResults = await Promise.allSettled(deliveryConnections.map((connection) => connection.close()));
|
|
1620
|
+
this.#closeFailures += [...mirrorResults, ...replicaResults, ...deliveryResults].filter((result) => result.status === "rejected").length;
|
|
1491
1621
|
}
|
|
1492
1622
|
#assertOpen() {
|
|
1493
1623
|
if (this.#closed)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { STOCK_CAPTURE_TAB_UNAVAILABLE } from "@tmux-ide/contracts";
|
|
2
|
+
export class StockCaptureTabUnavailableError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(STOCK_CAPTURE_TAB_UNAVAILABLE);
|
|
5
|
+
this.name = "StockCaptureTabUnavailableError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** ANSI capture drops stored tab widths. Live HT is cursor motion, not capture paint. */
|
|
9
|
+
export function assertStockCaptureRepresentable(chunks) {
|
|
10
|
+
if (chunks.some((chunk) => chunk.includes(9)))
|
|
11
|
+
throw new StockCaptureTabUnavailableError();
|
|
12
|
+
}
|