tmux-ide 2.9.3 → 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/LICENSE +1 -1
- package/README.md +120 -139
- package/bin/cli.js +4847 -3585
- package/package.json +16 -9
- 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/integrations/claude-agent-teams.js +264 -0
- 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/integrations/claude-agent-teams.ts +334 -0
- 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
- package/skill/SKILL.md +30 -30
|
@@ -21,6 +21,7 @@ import { withBundledTmuxResources } from "../../lib/bundled-tmux.js";
|
|
|
21
21
|
* chance to detach us — killing the reader first can wedge the server.
|
|
22
22
|
*/
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
24
25
|
import { shellEscape } from "../../lib/shell.js";
|
|
25
26
|
import { NativeJournalIdentitySchemaZ, } from "@tmux-ide/contracts";
|
|
26
27
|
import { nativeOperationWrapperArgs } from "../../lib/native-operation-command.js";
|
|
@@ -182,6 +183,12 @@ export class ControlChannelCore {
|
|
|
182
183
|
armAtomicPaneSnapshotCollector(spec) {
|
|
183
184
|
if (this.failed || this.atomicCollector)
|
|
184
185
|
return false;
|
|
186
|
+
if (spec.kind !== undefined && spec.kind !== "snapshot" && spec.kind !== "pause")
|
|
187
|
+
return false;
|
|
188
|
+
if (spec.kind === "pause" && (spec.observerCommandCount !== 0 || spec.dualCapture))
|
|
189
|
+
return false;
|
|
190
|
+
if (spec.dualCapture !== undefined && typeof spec.dualCapture !== "boolean")
|
|
191
|
+
return false;
|
|
185
192
|
if (!/^[0-9a-f]{32,128}$/.test(spec.nonce) || !/^%\d+$/.test(spec.runtimePaneId))
|
|
186
193
|
return false;
|
|
187
194
|
if (!Number.isSafeInteger(spec.maxCaptureBytes) ||
|
|
@@ -199,15 +206,19 @@ export class ControlChannelCore {
|
|
|
199
206
|
return false;
|
|
200
207
|
this.atomicCollector = {
|
|
201
208
|
spec,
|
|
209
|
+
retired: false,
|
|
210
|
+
settled: false,
|
|
202
211
|
started: false,
|
|
203
212
|
blockOrdinal: -1,
|
|
204
213
|
blockContentCount: 0,
|
|
205
214
|
captureLines: [],
|
|
215
|
+
ansiCaptureLines: [],
|
|
206
216
|
captureBytes: 0,
|
|
207
217
|
totalLines: 0,
|
|
208
218
|
totalBytes: 0,
|
|
209
219
|
cursorLine: null,
|
|
210
220
|
continueObserved: false,
|
|
221
|
+
pauseObserved: false,
|
|
211
222
|
statusObserved: false,
|
|
212
223
|
observerEmissionObserved: false,
|
|
213
224
|
lastCompletedOrdinal: -1,
|
|
@@ -217,25 +228,112 @@ export class ControlChannelCore {
|
|
|
217
228
|
}
|
|
218
229
|
retireAtomicPaneSnapshotCollector(nonce, reason = "retired") {
|
|
219
230
|
const collector = this.atomicCollector;
|
|
220
|
-
if (!collector || collector.spec.nonce !== nonce)
|
|
231
|
+
if (!collector || collector.spec.nonce !== nonce || collector.settled)
|
|
221
232
|
return false;
|
|
222
|
-
|
|
223
|
-
collector.
|
|
233
|
+
collector.retired = true;
|
|
234
|
+
collector.settled = true;
|
|
235
|
+
const result = Object.freeze({
|
|
224
236
|
ok: false,
|
|
225
237
|
captureLines: Object.freeze([]),
|
|
238
|
+
...(collector.spec.dualCapture ? { ansiCaptureLines: Object.freeze([]) } : {}),
|
|
226
239
|
cursorLine: null,
|
|
227
240
|
continueObserved: collector.continueObserved,
|
|
228
241
|
statusObserved: collector.statusObserved,
|
|
229
242
|
observerEmissionObserved: collector.observerEmissionObserved,
|
|
230
243
|
started: collector.started,
|
|
231
244
|
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
232
|
-
captureLineCount: collector.captureLines.length,
|
|
245
|
+
captureLineCount: collector.captureLines.length + collector.ansiCaptureLines.length,
|
|
233
246
|
captureByteCount: collector.captureBytes,
|
|
234
247
|
failureReason: collector.failureReason ?? reason,
|
|
235
|
-
|
|
248
|
+
...(collector.spec.kind === "pause" ? { pauseObserved: collector.pauseObserved } : {}),
|
|
249
|
+
});
|
|
250
|
+
// Release payload memory before callbacks can throw or reenter retirement.
|
|
251
|
+
collector.captureLines.length = 0;
|
|
252
|
+
collector.ansiCaptureLines.length = 0;
|
|
253
|
+
collector.cursorLine = null;
|
|
254
|
+
collector.totalBytes = 0;
|
|
255
|
+
collector.totalLines = 0;
|
|
256
|
+
collector.spec.onSettled(result);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
/** Caller has validated the separate ordinary command's exact fence reply. */
|
|
260
|
+
releaseRetiredCollector(nonce) {
|
|
261
|
+
const collector = this.atomicCollector;
|
|
262
|
+
if (!collector?.retired || collector.spec.nonce !== nonce)
|
|
263
|
+
return false;
|
|
264
|
+
this.atomicCollector = null;
|
|
265
|
+
this.notifyAtomicDrained(collector, "fence");
|
|
236
266
|
return true;
|
|
237
267
|
}
|
|
268
|
+
notifyAtomicDrained(collector, reason) {
|
|
269
|
+
try {
|
|
270
|
+
collector.spec.onDrained?.(reason);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
this.failRetiredDrain("snapshot drain callback failed");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
failRetiredDrain(reason) {
|
|
277
|
+
try {
|
|
278
|
+
this.fail(reason);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
this.handlers.onExit(reason);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
consumeRetiredSnapshotLine(line) {
|
|
285
|
+
const collector = this.atomicCollector;
|
|
286
|
+
if (!collector?.retired)
|
|
287
|
+
return false;
|
|
288
|
+
const event = parseControlLine(line, this.inReply);
|
|
289
|
+
if (this.inReply && this.currentReplyFlags === 0) {
|
|
290
|
+
collector.totalBytes += Buffer.byteLength(line, "latin1") + 1;
|
|
291
|
+
collector.totalLines += 1;
|
|
292
|
+
if (collector.totalBytes > collector.spec.maxCaptureBytes + 64 * 1024 ||
|
|
293
|
+
collector.totalLines > collector.spec.maxCaptureLines + 64) {
|
|
294
|
+
this.failRetiredDrain("retired snapshot drain exceeded bounds");
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
// Raw rows may look like %begin, a sentinel, or the fence payload. Only
|
|
298
|
+
// the matching raw terminator changes framing; none can redeem a FIFO slot.
|
|
299
|
+
if (event.kind === "end" || event.kind === "error") {
|
|
300
|
+
if (event.flags !== 0 || event.num !== this.currentReplyNum) {
|
|
301
|
+
this.failRetiredDrain("invalid retired snapshot framing");
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
this.inReply = false;
|
|
305
|
+
this.currentReplyNum = null;
|
|
306
|
+
this.currentReplyFlags = null;
|
|
307
|
+
this.currentReplyConsumesPending = false;
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (event.kind === "begin" &&
|
|
312
|
+
(!Number.isSafeInteger(event.num) || event.num < 0 || ![0, 1].includes(event.flags))) {
|
|
313
|
+
this.failRetiredDrain("invalid retired snapshot opening frame");
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
if (event.kind === "begin" && event.flags === 0) {
|
|
317
|
+
this.inReply = true;
|
|
318
|
+
this.currentReplyNum = event.num;
|
|
319
|
+
this.currentReplyFlags = 0;
|
|
320
|
+
this.currentReplyConsumesPending = false;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (event.kind === "end" || event.kind === "error") {
|
|
324
|
+
if (!this.inReply ||
|
|
325
|
+
event.num !== this.currentReplyNum ||
|
|
326
|
+
event.flags !== this.currentReplyFlags) {
|
|
327
|
+
this.failRetiredDrain("invalid retired snapshot fence framing");
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Ordinary flags=1 responses still own their existing pending FIFO slots.
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
238
334
|
feed(chunk, receivedAtMicros) {
|
|
335
|
+
if (this.failed)
|
|
336
|
+
return;
|
|
239
337
|
if (this.droppingOversizedReplyLine) {
|
|
240
338
|
const newline = chunk.indexOf("\n");
|
|
241
339
|
if (newline < 0)
|
|
@@ -255,6 +353,16 @@ export class ControlChannelCore {
|
|
|
255
353
|
const lineReceivedAtMicros = this.bufferReceivedAtMicros;
|
|
256
354
|
this.bufferReceivedAtMicros = this.buffer.length > 0 ? (receivedAtMicros ?? null) : null;
|
|
257
355
|
this.handleLine(line, lineReceivedAtMicros);
|
|
356
|
+
if (this.failed) {
|
|
357
|
+
this.buffer = "";
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (this.atomicCollector?.retired &&
|
|
362
|
+
this.buffer.length > this.atomicCollector.spec.maxCaptureBytes + 64 * 1024) {
|
|
363
|
+
this.buffer = "";
|
|
364
|
+
this.failRetiredDrain("retired snapshot partial line exceeded bounds");
|
|
365
|
+
return;
|
|
258
366
|
}
|
|
259
367
|
const head = this.currentBudget();
|
|
260
368
|
// Protocol terminators may themselves exceed a tiny payload budget and
|
|
@@ -275,25 +383,44 @@ export class ControlChannelCore {
|
|
|
275
383
|
if (this.failed)
|
|
276
384
|
return;
|
|
277
385
|
this.failed = true;
|
|
278
|
-
if (this.atomicCollector)
|
|
279
|
-
this.
|
|
386
|
+
if (this.atomicCollector) {
|
|
387
|
+
const collector = this.atomicCollector;
|
|
388
|
+
try {
|
|
389
|
+
this.retireAtomicPaneSnapshotCollector(collector.spec.nonce, "channel-exit");
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// A failed owner cannot prevent sibling requests from settling on exit.
|
|
393
|
+
}
|
|
394
|
+
this.atomicCollector = null;
|
|
395
|
+
try {
|
|
396
|
+
this.notifyAtomicDrained(collector, "channel-exit");
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Exit notification callbacks may also fail; the pending FIFO still drains.
|
|
400
|
+
}
|
|
401
|
+
}
|
|
280
402
|
for (const sink of this.pending.splice(0)) {
|
|
281
|
-
|
|
282
|
-
sink.
|
|
283
|
-
|
|
284
|
-
sink.
|
|
285
|
-
|
|
286
|
-
sink.state.settled
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
403
|
+
try {
|
|
404
|
+
if (sink.kind === "promise")
|
|
405
|
+
sink.reject(new Error(reason));
|
|
406
|
+
else if (sink.kind === "inline" || sink.kind === "bounded")
|
|
407
|
+
sink.onReply({ ok: false, lines: [reason] });
|
|
408
|
+
else if (sink.kind === "native-wrapper" && !sink.state.settled) {
|
|
409
|
+
sink.state.settled = true;
|
|
410
|
+
notifyNativeWrapper(sink.state, {
|
|
411
|
+
ok: false,
|
|
412
|
+
lines: [],
|
|
413
|
+
metadataStatus: sink.state.metadataStatus,
|
|
414
|
+
acknowledgement: sink.state.acknowledgement,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else if (sink.kind === "command-list" && !sink.state.settled) {
|
|
418
|
+
sink.state.settled = true;
|
|
419
|
+
sink.state.onReply({ ok: false, lines: [reason] });
|
|
420
|
+
}
|
|
293
421
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
sink.state.onReply({ ok: false, lines: [reason] });
|
|
422
|
+
catch {
|
|
423
|
+
// Keep failure fan-out independent of any one request callback.
|
|
297
424
|
}
|
|
298
425
|
}
|
|
299
426
|
}
|
|
@@ -457,6 +584,7 @@ export class ControlChannelCore {
|
|
|
457
584
|
this.handlers.onOutput(event.pane, event.data, event.ageMs, timing);
|
|
458
585
|
break;
|
|
459
586
|
case "exit":
|
|
587
|
+
this.fail(event.reason ?? "control channel exited");
|
|
460
588
|
this.handlers.onExit(event.reason);
|
|
461
589
|
break;
|
|
462
590
|
case "notify":
|
|
@@ -468,6 +596,8 @@ export class ControlChannelCore {
|
|
|
468
596
|
const collector = this.atomicCollector;
|
|
469
597
|
if (!collector)
|
|
470
598
|
return false;
|
|
599
|
+
if (collector.retired)
|
|
600
|
+
return this.consumeRetiredSnapshotLine(line);
|
|
471
601
|
const prefix = "%tmux-ide-atomic-v1 ";
|
|
472
602
|
const ownPrefix = `${prefix}${collector.spec.nonce} `;
|
|
473
603
|
if (line.startsWith(prefix) && !line.startsWith(ownPrefix)) {
|
|
@@ -481,7 +611,9 @@ export class ControlChannelCore {
|
|
|
481
611
|
if (token !== "start" ||
|
|
482
612
|
!this.inReply ||
|
|
483
613
|
this.currentReplyFlags !== 0 ||
|
|
484
|
-
this.currentReplyNum === null
|
|
614
|
+
this.currentReplyNum === null ||
|
|
615
|
+
(collector.spec.kind === "pause" &&
|
|
616
|
+
(!Number.isSafeInteger(this.currentReplyNum) || this.currentReplyNum < 0))) {
|
|
485
617
|
collector.failureReason ??= "sentinel-order";
|
|
486
618
|
return true;
|
|
487
619
|
}
|
|
@@ -497,6 +629,9 @@ export class ControlChannelCore {
|
|
|
497
629
|
collector.failureReason ??= "capture-line-cap";
|
|
498
630
|
if (collector.totalBytes > collector.spec.maxCaptureBytes + 64 * 1024)
|
|
499
631
|
collector.failureReason ??= "capture-byte-cap";
|
|
632
|
+
if (collector.spec.kind === "pause")
|
|
633
|
+
return this.consumeOwnedPauseLine(line, token);
|
|
634
|
+
const shift = collector.spec.dualCapture ? 2 : 0;
|
|
500
635
|
const guard = parseControlLine(line, this.inReply);
|
|
501
636
|
if (guard.kind === "begin") {
|
|
502
637
|
if (guard.flags !== 0 || this.inReply)
|
|
@@ -520,11 +655,20 @@ export class ControlChannelCore {
|
|
|
520
655
|
// The ownership compare-and-unset is an if-shell command plus exactly
|
|
521
656
|
// one selected branch command. Both commands retain the control client
|
|
522
657
|
// and therefore each has its own flags=0 guard block.
|
|
523
|
-
const markerBranchOrdinal = 7 + collector.spec.observerCommandCount;
|
|
524
|
-
const statusOrdinal = 8 + collector.spec.observerCommandCount;
|
|
525
|
-
const completeOrdinal = 10 + collector.spec.observerCommandCount;
|
|
526
|
-
const contentRequired = new Set([
|
|
658
|
+
const markerBranchOrdinal = 7 + shift + collector.spec.observerCommandCount;
|
|
659
|
+
const statusOrdinal = 8 + shift + collector.spec.observerCommandCount;
|
|
660
|
+
const completeOrdinal = 10 + shift + collector.spec.observerCommandCount;
|
|
661
|
+
const contentRequired = new Set([
|
|
662
|
+
0,
|
|
663
|
+
2,
|
|
664
|
+
3 + shift,
|
|
665
|
+
4 + shift,
|
|
666
|
+
...(shift ? [4] : []),
|
|
667
|
+
statusOrdinal,
|
|
668
|
+
completeOrdinal,
|
|
669
|
+
]);
|
|
527
670
|
const contentSilent = collector.blockOrdinal !== 1 &&
|
|
671
|
+
!(shift && collector.blockOrdinal === 3) &&
|
|
528
672
|
collector.blockOrdinal !== markerBranchOrdinal &&
|
|
529
673
|
!contentRequired.has(collector.blockOrdinal);
|
|
530
674
|
const markerBranchValid = collector.blockOrdinal !== markerBranchOrdinal || collector.blockContentCount <= 1;
|
|
@@ -535,7 +679,7 @@ export class ControlChannelCore {
|
|
|
535
679
|
if (guard.kind === "end" &&
|
|
536
680
|
!guardInvalid &&
|
|
537
681
|
collector.spec.observerCommandCount > 0 &&
|
|
538
|
-
collector.blockOrdinal === 5 + collector.spec.observerCommandCount)
|
|
682
|
+
collector.blockOrdinal === 5 + shift + collector.spec.observerCommandCount)
|
|
539
683
|
collector.observerEmissionObserved = true;
|
|
540
684
|
if (!guardInvalid && guard.kind === "end") {
|
|
541
685
|
collector.lastCompletedOrdinal = collector.blockOrdinal;
|
|
@@ -545,27 +689,15 @@ export class ControlChannelCore {
|
|
|
545
689
|
this.currentReplyNum = null;
|
|
546
690
|
this.currentReplyFlags = null;
|
|
547
691
|
if (collector.blockOrdinal === completeOrdinal) {
|
|
548
|
-
this.atomicCollector = null;
|
|
549
692
|
const ok = collector.failureReason === null &&
|
|
550
693
|
collector.cursorLine !== null &&
|
|
551
694
|
collector.statusObserved;
|
|
552
|
-
|
|
553
|
-
ok,
|
|
554
|
-
captureLines: Object.freeze(ok ? [...collector.captureLines] : []),
|
|
555
|
-
cursorLine: ok ? collector.cursorLine : null,
|
|
556
|
-
continueObserved: collector.continueObserved,
|
|
557
|
-
statusObserved: collector.statusObserved,
|
|
558
|
-
observerEmissionObserved: collector.observerEmissionObserved,
|
|
559
|
-
started: collector.started,
|
|
560
|
-
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
561
|
-
captureLineCount: collector.captureLines.length,
|
|
562
|
-
captureByteCount: collector.captureBytes,
|
|
563
|
-
failureReason: ok ? null : (collector.failureReason ?? "sentinel-order"),
|
|
564
|
-
}));
|
|
695
|
+
this.completeAtomicCollector(collector, ok);
|
|
565
696
|
}
|
|
566
697
|
return true;
|
|
567
698
|
}
|
|
568
|
-
if (line === `%continue ${collector.spec.runtimePaneId}` &&
|
|
699
|
+
if (line === `%continue ${collector.spec.runtimePaneId}` &&
|
|
700
|
+
collector.blockOrdinal === 5 + shift) {
|
|
569
701
|
if (collector.continueObserved)
|
|
570
702
|
collector.failureReason ??= "duplicate-sentinel";
|
|
571
703
|
collector.continueObserved = true;
|
|
@@ -579,22 +711,23 @@ export class ControlChannelCore {
|
|
|
579
711
|
collector.failureReason ??= "unexpected-post-line";
|
|
580
712
|
return true;
|
|
581
713
|
}
|
|
582
|
-
const markerBranchOrdinal = 7 + collector.spec.observerCommandCount;
|
|
583
|
-
const statusOrdinal = 8 + collector.spec.observerCommandCount;
|
|
584
|
-
const completeOrdinal = 10 + collector.spec.observerCommandCount;
|
|
714
|
+
const markerBranchOrdinal = 7 + shift + collector.spec.observerCommandCount;
|
|
715
|
+
const statusOrdinal = 8 + shift + collector.spec.observerCommandCount;
|
|
716
|
+
const completeOrdinal = 10 + shift + collector.spec.observerCommandCount;
|
|
585
717
|
collector.blockContentCount += 1;
|
|
586
|
-
if (collector.blockOrdinal === 1) {
|
|
718
|
+
if (collector.blockOrdinal === 1 || (shift && collector.blockOrdinal === 3)) {
|
|
587
719
|
collector.captureBytes += Buffer.byteLength(line, "latin1") + 1;
|
|
588
720
|
if (collector.captureBytes > collector.spec.maxCaptureBytes)
|
|
589
721
|
collector.failureReason ??= "capture-byte-cap";
|
|
590
|
-
if (collector.captureLines.length
|
|
722
|
+
if (collector.captureLines.length + collector.ansiCaptureLines.length >=
|
|
723
|
+
collector.spec.maxCaptureLines)
|
|
591
724
|
collector.failureReason ??= "capture-line-cap";
|
|
592
725
|
else
|
|
593
|
-
collector.captureLines.push(line);
|
|
726
|
+
(collector.blockOrdinal === 1 ? collector.captureLines : collector.ansiCaptureLines).push(line);
|
|
594
727
|
this.reportAtomicPaneSnapshotProgress(collector);
|
|
595
728
|
return true;
|
|
596
729
|
}
|
|
597
|
-
if (collector.blockOrdinal === 3) {
|
|
730
|
+
if (collector.blockOrdinal === 3 + shift) {
|
|
598
731
|
if (collector.cursorLine !== null)
|
|
599
732
|
collector.failureReason ??= "cursor-cardinality";
|
|
600
733
|
else if (Buffer.byteLength(line, "latin1") > collector.spec.maxCursorBytes)
|
|
@@ -610,13 +743,15 @@ export class ControlChannelCore {
|
|
|
610
743
|
}
|
|
611
744
|
const expectedToken = collector.blockOrdinal === 2
|
|
612
745
|
? "capture-end"
|
|
613
|
-
: collector.blockOrdinal === 4
|
|
614
|
-
? "
|
|
615
|
-
: collector.blockOrdinal ===
|
|
616
|
-
? "
|
|
617
|
-
: collector.blockOrdinal ===
|
|
618
|
-
? "
|
|
619
|
-
:
|
|
746
|
+
: shift && collector.blockOrdinal === 4
|
|
747
|
+
? "ansi-capture-end"
|
|
748
|
+
: collector.blockOrdinal === 4 + shift
|
|
749
|
+
? "cursor-end"
|
|
750
|
+
: collector.blockOrdinal === statusOrdinal
|
|
751
|
+
? "status-ok"
|
|
752
|
+
: collector.blockOrdinal === completeOrdinal
|
|
753
|
+
? "complete"
|
|
754
|
+
: null;
|
|
620
755
|
if (expectedToken === null || token !== expectedToken)
|
|
621
756
|
collector.failureReason ??= token === "start" ? "duplicate-sentinel" : "sentinel-order";
|
|
622
757
|
if (collector.blockOrdinal === statusOrdinal && token === "status-ok") {
|
|
@@ -626,13 +761,106 @@ export class ControlChannelCore {
|
|
|
626
761
|
}
|
|
627
762
|
return true;
|
|
628
763
|
}
|
|
764
|
+
completeAtomicCollector(collector, ok) {
|
|
765
|
+
if (!ok) {
|
|
766
|
+
this.retireAtomicPaneSnapshotCollector(collector.spec.nonce, collector.failureReason ?? "sentinel-order");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const result = Object.freeze({
|
|
770
|
+
ok: true,
|
|
771
|
+
captureLines: Object.freeze([...collector.captureLines]),
|
|
772
|
+
...(collector.spec.dualCapture
|
|
773
|
+
? { ansiCaptureLines: Object.freeze([...collector.ansiCaptureLines]) }
|
|
774
|
+
: {}),
|
|
775
|
+
cursorLine: collector.cursorLine,
|
|
776
|
+
continueObserved: collector.continueObserved,
|
|
777
|
+
statusObserved: collector.statusObserved,
|
|
778
|
+
observerEmissionObserved: collector.observerEmissionObserved,
|
|
779
|
+
started: collector.started,
|
|
780
|
+
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
781
|
+
captureLineCount: collector.captureLines.length + collector.ansiCaptureLines.length,
|
|
782
|
+
captureByteCount: collector.captureBytes,
|
|
783
|
+
failureReason: null,
|
|
784
|
+
...(collector.spec.kind === "pause" ? { pauseObserved: collector.pauseObserved } : {}),
|
|
785
|
+
});
|
|
786
|
+
// Settlement cannot admit another collector before terminal framing drains.
|
|
787
|
+
collector.settled = true;
|
|
788
|
+
try {
|
|
789
|
+
collector.spec.onSettled(result);
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
this.failRetiredDrain("snapshot settlement callback failed");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (this.atomicCollector === collector) {
|
|
796
|
+
this.atomicCollector = null;
|
|
797
|
+
this.notifyAtomicDrained(collector, "complete");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
consumeOwnedPauseLine(line, token) {
|
|
801
|
+
const collector = this.atomicCollector;
|
|
802
|
+
const guard = parseControlLine(line, this.inReply);
|
|
803
|
+
if (guard.kind === "begin") {
|
|
804
|
+
if (guard.flags !== 0 || this.inReply || !Number.isSafeInteger(guard.num) || guard.num < 0)
|
|
805
|
+
collector.failureReason ??= "sentinel-order";
|
|
806
|
+
this.inReply = true;
|
|
807
|
+
this.currentReplyNum = guard.num;
|
|
808
|
+
this.currentReplyFlags = guard.flags;
|
|
809
|
+
collector.blockOrdinal += 1;
|
|
810
|
+
collector.blockContentCount = 0;
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
if (guard.kind === "end" || guard.kind === "error") {
|
|
814
|
+
const valid = this.inReply &&
|
|
815
|
+
guard.kind === "end" &&
|
|
816
|
+
guard.flags === 0 &&
|
|
817
|
+
this.currentReplyFlags === 0 &&
|
|
818
|
+
guard.num === this.currentReplyNum;
|
|
819
|
+
if (!valid ||
|
|
820
|
+
(collector.blockOrdinal === 1
|
|
821
|
+
? collector.blockContentCount > 1
|
|
822
|
+
: collector.blockContentCount !== 1))
|
|
823
|
+
collector.failureReason ??= "sentinel-order";
|
|
824
|
+
if (valid)
|
|
825
|
+
collector.lastCompletedOrdinal = collector.blockOrdinal;
|
|
826
|
+
this.inReply = false;
|
|
827
|
+
this.currentReplyNum = null;
|
|
828
|
+
this.currentReplyFlags = null;
|
|
829
|
+
this.currentReplyConsumesPending = false;
|
|
830
|
+
if (collector.blockOrdinal === 2)
|
|
831
|
+
this.completeAtomicCollector(collector, collector.failureReason === null && collector.statusObserved);
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
if (!this.inReply && (line === "%exit" || line.startsWith("%exit "))) {
|
|
835
|
+
this.retireAtomicPaneSnapshotCollector(collector.spec.nonce, "channel-exit");
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
if (!this.inReply) {
|
|
839
|
+
collector.failureReason ??= "unexpected-post-line";
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
collector.blockContentCount += 1;
|
|
843
|
+
if (collector.blockOrdinal === 1 && line === `%pause ${collector.spec.runtimePaneId}`) {
|
|
844
|
+
if (collector.pauseObserved)
|
|
845
|
+
collector.failureReason ??= "duplicate-sentinel";
|
|
846
|
+
collector.pauseObserved = true;
|
|
847
|
+
}
|
|
848
|
+
else if (collector.blockOrdinal === 2 && token === "complete") {
|
|
849
|
+
if (collector.statusObserved)
|
|
850
|
+
collector.failureReason ??= "duplicate-sentinel";
|
|
851
|
+
collector.statusObserved = true;
|
|
852
|
+
}
|
|
853
|
+
else
|
|
854
|
+
collector.failureReason ??= "sentinel-order";
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
629
857
|
reportAtomicPaneSnapshotProgress(collector) {
|
|
630
858
|
if (collector.failureReason !== null)
|
|
631
859
|
return;
|
|
632
860
|
collector.spec.onProgress?.(Object.freeze({
|
|
633
861
|
started: collector.started,
|
|
634
862
|
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
635
|
-
captureLineCount: Math.min(collector.captureLines.length, ATOMIC_CAPTURE_LINE_HARD_CAP),
|
|
863
|
+
captureLineCount: Math.min(collector.captureLines.length + collector.ansiCaptureLines.length, ATOMIC_CAPTURE_LINE_HARD_CAP),
|
|
636
864
|
captureByteCount: Math.min(collector.captureBytes, ATOMIC_CAPTURE_BYTE_HARD_CAP),
|
|
637
865
|
continueObserved: collector.continueObserved,
|
|
638
866
|
statusObserved: collector.statusObserved,
|
|
@@ -684,7 +912,7 @@ export class MirrorControlChannel {
|
|
|
684
912
|
viewerPending = 0;
|
|
685
913
|
verifiedAttach = false;
|
|
686
914
|
guardedStart = null;
|
|
687
|
-
|
|
915
|
+
atomicCollectorOwner = null;
|
|
688
916
|
constructor(opts) {
|
|
689
917
|
this.opts = opts;
|
|
690
918
|
this.core = new ControlChannelCore({
|
|
@@ -701,7 +929,14 @@ export class MirrorControlChannel {
|
|
|
701
929
|
if (!opts.nativeServerIdentity || this.verifiedAttach)
|
|
702
930
|
opts.handlers.onNotify(name, rest);
|
|
703
931
|
},
|
|
704
|
-
onExit: (reason) =>
|
|
932
|
+
onExit: (reason) => {
|
|
933
|
+
try {
|
|
934
|
+
this.noteExit(reason);
|
|
935
|
+
}
|
|
936
|
+
finally {
|
|
937
|
+
void this.dispose();
|
|
938
|
+
}
|
|
939
|
+
},
|
|
705
940
|
}, opts.nowMicros);
|
|
706
941
|
}
|
|
707
942
|
start() {
|
|
@@ -966,32 +1201,99 @@ export class MirrorControlChannel {
|
|
|
966
1201
|
proc.stdin.write(`${cmd}\n`);
|
|
967
1202
|
}
|
|
968
1203
|
armAtomicPaneSnapshotCollector(spec, timeoutMs) {
|
|
1204
|
+
if (this.exited || !this.proc?.stdin?.writable)
|
|
1205
|
+
return false;
|
|
969
1206
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 5_000)
|
|
970
1207
|
return false;
|
|
1208
|
+
const owner = {
|
|
1209
|
+
nonce: spec.nonce,
|
|
1210
|
+
timer: null,
|
|
1211
|
+
drainTimer: null,
|
|
1212
|
+
retiring: false,
|
|
1213
|
+
completed: false,
|
|
1214
|
+
};
|
|
1215
|
+
const clear = () => {
|
|
1216
|
+
if (owner.timer)
|
|
1217
|
+
clearTimeout(owner.timer);
|
|
1218
|
+
if (owner.drainTimer)
|
|
1219
|
+
clearTimeout(owner.drainTimer);
|
|
1220
|
+
owner.timer = owner.drainTimer = null;
|
|
1221
|
+
if (this.atomicCollectorOwner === owner)
|
|
1222
|
+
this.atomicCollectorOwner = null;
|
|
1223
|
+
};
|
|
971
1224
|
const wrapped = {
|
|
972
1225
|
...spec,
|
|
973
1226
|
onSettled: (result) => {
|
|
974
|
-
if (
|
|
975
|
-
clearTimeout(
|
|
976
|
-
|
|
1227
|
+
if (owner.timer)
|
|
1228
|
+
clearTimeout(owner.timer);
|
|
1229
|
+
owner.timer = null;
|
|
1230
|
+
owner.completed = result.ok;
|
|
1231
|
+
if (!result.ok && result.failureReason !== "channel-exit" && !owner.retiring) {
|
|
1232
|
+
this.beginCollectorDrain(spec.nonce, result.failureReason ?? "retired", () => spec.onSettled(result));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
977
1235
|
spec.onSettled(result);
|
|
978
1236
|
},
|
|
1237
|
+
onDrained: (reason) => {
|
|
1238
|
+
clear();
|
|
1239
|
+
spec.onDrained?.(reason);
|
|
1240
|
+
},
|
|
979
1241
|
};
|
|
980
1242
|
if (!this.core.armAtomicPaneSnapshotCollector(wrapped))
|
|
981
1243
|
return false;
|
|
982
|
-
this.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}, timeoutMs);
|
|
986
|
-
this.atomicCollectorTimer.unref?.();
|
|
1244
|
+
this.atomicCollectorOwner = owner;
|
|
1245
|
+
owner.timer = setTimeout(() => this.retireAtomicPaneSnapshotCollector(spec.nonce, "timeout"), timeoutMs);
|
|
1246
|
+
owner.timer.unref?.();
|
|
987
1247
|
return true;
|
|
988
1248
|
}
|
|
989
1249
|
retireAtomicPaneSnapshotCollector(nonce, reason = "retired") {
|
|
990
|
-
|
|
1250
|
+
this.beginCollectorDrain(nonce, reason);
|
|
1251
|
+
}
|
|
1252
|
+
beginCollectorDrain(nonce, reason, settle) {
|
|
1253
|
+
const owner = this.atomicCollectorOwner;
|
|
1254
|
+
if (!owner || owner.nonce !== nonce || owner.retiring || owner.completed)
|
|
991
1255
|
return;
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1256
|
+
owner.retiring = true;
|
|
1257
|
+
if (owner.timer)
|
|
1258
|
+
clearTimeout(owner.timer);
|
|
1259
|
+
owner.timer = null;
|
|
1260
|
+
const abort = (message) => {
|
|
1261
|
+
if (this.atomicCollectorOwner !== owner)
|
|
1262
|
+
return;
|
|
1263
|
+
try {
|
|
1264
|
+
this.core.fail(message);
|
|
1265
|
+
this.noteExit(message);
|
|
1266
|
+
}
|
|
1267
|
+
finally {
|
|
1268
|
+
void this.dispose();
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
// Establish drain ownership before onSettled can reenter and try another arm.
|
|
1272
|
+
owner.drainTimer = setTimeout(() => abort("retired snapshot drain timed out"), 5_000);
|
|
1273
|
+
owner.drainTimer.unref?.();
|
|
1274
|
+
try {
|
|
1275
|
+
this.core.retireAtomicPaneSnapshotCollector(nonce, reason);
|
|
1276
|
+
// Natural failed completion already settled the core tombstone. Retire
|
|
1277
|
+
// application ownership before queuing a fence behind dispatched work.
|
|
1278
|
+
settle?.();
|
|
1279
|
+
}
|
|
1280
|
+
catch {
|
|
1281
|
+
abort("retired snapshot settlement callback failed");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (this.atomicCollectorOwner !== owner)
|
|
1285
|
+
return;
|
|
1286
|
+
const fence = `tmux-ide-collector-drain-v1:${randomBytes(24).toString("hex")}`;
|
|
1287
|
+
this.commandBoundedInline(`display-message -p -l ${fence}`, { maxBytes: 256, maxLines: 1 }, (reply) => {
|
|
1288
|
+
if (this.atomicCollectorOwner !== owner)
|
|
1289
|
+
return;
|
|
1290
|
+
if (!reply.ok || reply.lines.length !== 1 || reply.lines[0] !== fence) {
|
|
1291
|
+
abort("invalid retired snapshot drain fence");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (!this.core.releaseRetiredCollector(nonce))
|
|
1295
|
+
abort("retired snapshot owner changed");
|
|
1296
|
+
});
|
|
995
1297
|
}
|
|
996
1298
|
send(cmd, onReply) {
|
|
997
1299
|
const proc = this.proc;
|
|
@@ -1013,9 +1315,11 @@ export class MirrorControlChannel {
|
|
|
1013
1315
|
*/
|
|
1014
1316
|
async dispose() {
|
|
1015
1317
|
this.retireNativeViewer();
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1318
|
+
const collectorOwner = this.atomicCollectorOwner;
|
|
1319
|
+
if (collectorOwner?.timer)
|
|
1320
|
+
clearTimeout(collectorOwner.timer);
|
|
1321
|
+
if (collectorOwner?.drainTimer)
|
|
1322
|
+
clearTimeout(collectorOwner.drainTimer);
|
|
1019
1323
|
const proc = this.proc;
|
|
1020
1324
|
this.proc = null;
|
|
1021
1325
|
if (!proc)
|