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
|
@@ -709,7 +709,15 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
709
709
|
}
|
|
710
710
|
if (this._needsWalk) {
|
|
711
711
|
this._needsWalk = false;
|
|
712
|
-
|
|
712
|
+
try {
|
|
713
|
+
this.walk();
|
|
714
|
+
} catch (error) {
|
|
715
|
+
// The source may have consumed its dirty rows before a grapheme or
|
|
716
|
+
// presentation pass fails. Rebuild the retained surface on retry.
|
|
717
|
+
this._needsWalk = true;
|
|
718
|
+
this._forceFull = true;
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
713
721
|
} else if (this._needsCursorPresentation) {
|
|
714
722
|
this.applyCursorOnlyPresentation();
|
|
715
723
|
}
|
|
@@ -59,14 +59,30 @@ export interface ReferenceTraceWriterSnapshot {
|
|
|
59
59
|
readonly firstDroppedRecord: ReferenceTraceDroppedRecordKind | null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface ReferenceInputAttempts {
|
|
63
|
+
readonly begun: number;
|
|
64
|
+
readonly completed: number;
|
|
65
|
+
readonly superseded: number;
|
|
66
|
+
readonly expired: number;
|
|
67
|
+
readonly cancelled: number;
|
|
68
|
+
readonly pending: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ReferenceInputReport {
|
|
72
|
+
readonly pendingInputs: number;
|
|
73
|
+
readonly droppedInputs: number;
|
|
74
|
+
readonly inputAttempts?: ReferenceInputAttempts;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
export interface ReferenceTraceCollectorReport extends ReferenceTraceWriterSnapshot {
|
|
63
78
|
readonly pendingInputs: number;
|
|
64
79
|
readonly droppedInputs: number;
|
|
80
|
+
readonly inputAttempts?: ReferenceInputAttempts;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
interface ReferencePerformanceTraceSink extends TuiPerformanceEventSink {
|
|
68
|
-
snapshot():
|
|
69
|
-
close():
|
|
84
|
+
snapshot(): ReferenceInputReport & { readonly inputAttempts: ReferenceInputAttempts };
|
|
85
|
+
close(): ReferenceInputReport & { readonly inputAttempts: ReferenceInputAttempts };
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
interface ActiveCollector {
|
|
@@ -137,10 +153,7 @@ export function createReferenceTraceWriter(
|
|
|
137
153
|
readonly append: (value: Readonly<Record<string, unknown>>) => void;
|
|
138
154
|
readonly appendCritical: (value: Readonly<Record<string, unknown>>) => void;
|
|
139
155
|
readonly snapshot: () => ReferenceTraceWriterSnapshot;
|
|
140
|
-
readonly close: (sink:
|
|
141
|
-
readonly pendingInputs: number;
|
|
142
|
-
readonly droppedInputs: number;
|
|
143
|
-
}) => Promise<ReferenceTraceCollectorReport>;
|
|
156
|
+
readonly close: (sink: ReferenceInputReport) => Promise<ReferenceTraceCollectorReport>;
|
|
144
157
|
} {
|
|
145
158
|
let acceptedRecords = 0;
|
|
146
159
|
let droppedRecords = 0;
|
|
@@ -241,13 +254,13 @@ export function createReferenceTraceWriter(
|
|
|
241
254
|
firstDroppedRecord ??= droppedRecordKind(value);
|
|
242
255
|
return;
|
|
243
256
|
}
|
|
244
|
-
const line = `${JSON.stringify(value)}\n`;
|
|
245
|
-
const bytes = Buffer.byteLength(line);
|
|
246
|
-
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
247
|
-
oversizedRecords += 1;
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
257
|
try {
|
|
258
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
259
|
+
const bytes = Buffer.byteLength(line);
|
|
260
|
+
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
261
|
+
oversizedRecords += 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
251
264
|
if (!saturated && pendingCount() === 0) {
|
|
252
265
|
writeLine(line);
|
|
253
266
|
return;
|
|
@@ -273,10 +286,7 @@ export function createReferenceTraceWriter(
|
|
|
273
286
|
const append = (value: Readonly<Record<string, unknown>>): void => appendValue(value, false);
|
|
274
287
|
const appendCritical = (value: Readonly<Record<string, unknown>>): void =>
|
|
275
288
|
appendValue(value, true);
|
|
276
|
-
const close = (sink: {
|
|
277
|
-
readonly pendingInputs: number;
|
|
278
|
-
readonly droppedInputs: number;
|
|
279
|
-
}): Promise<ReferenceTraceCollectorReport> => {
|
|
289
|
+
const close = (sink: ReferenceInputReport): Promise<ReferenceTraceCollectorReport> => {
|
|
280
290
|
if (closePromise) return closePromise;
|
|
281
291
|
closed = true;
|
|
282
292
|
closePromise = (async () => {
|
|
@@ -322,8 +332,14 @@ export function createReferenceTraceWriter(
|
|
|
322
332
|
function droppedRecordKind(
|
|
323
333
|
value: Readonly<Record<string, unknown>>,
|
|
324
334
|
): ReferenceTraceDroppedRecordKind {
|
|
325
|
-
const field = (key: "type" | "stage" | "operation") =>
|
|
326
|
-
|
|
335
|
+
const field = (key: "type" | "stage" | "operation") => {
|
|
336
|
+
try {
|
|
337
|
+
const result = value[key];
|
|
338
|
+
return typeof result === "string" ? result.slice(0, 64) : null;
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
327
343
|
return Object.freeze({
|
|
328
344
|
type: field("type"),
|
|
329
345
|
stage: field("stage"),
|
|
@@ -374,7 +390,13 @@ export function createReferencePerformanceTraceSink(options: {
|
|
|
374
390
|
>();
|
|
375
391
|
let droppedInputs = 0;
|
|
376
392
|
let closed = false;
|
|
377
|
-
const
|
|
393
|
+
const attempts = { begun: 0, completed: 0, superseded: 0, expired: 0, cancelled: 0 };
|
|
394
|
+
const snapshot = () =>
|
|
395
|
+
Object.freeze({
|
|
396
|
+
pendingInputs: inputs.size,
|
|
397
|
+
droppedInputs,
|
|
398
|
+
inputAttempts: Object.freeze({ ...attempts, pending: inputs.size }),
|
|
399
|
+
});
|
|
378
400
|
return Object.freeze({
|
|
379
401
|
detailedWindowPresentationFrames: detailed ? true : undefined,
|
|
380
402
|
frame: (intervalMs: number, window?: TuiWindowPresentationFrameEvidence | null) => {
|
|
@@ -578,71 +600,81 @@ export function createReferencePerformanceTraceSink(options: {
|
|
|
578
600
|
}
|
|
579
601
|
: {}),
|
|
580
602
|
beginTerminalInput: (origin?: TuiTerminalInputOrigin) => {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
droppedInputs
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
? {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
603
|
+
if (closed) return Object.freeze({ traceId: "", finish: () => {}, cancel: () => {} });
|
|
604
|
+
attempts.begun += 1;
|
|
605
|
+
let traceId: string;
|
|
606
|
+
try {
|
|
607
|
+
const startedAtMicros = nowMicros();
|
|
608
|
+
attempts.expired += expireInputs(inputs, startedAtMicros);
|
|
609
|
+
// The transport carries one latest-only performance trace id. Once a
|
|
610
|
+
// newer input is admitted, an older probe can no longer be attributed to
|
|
611
|
+
// the exact changed-cell paint without lying. Retire it as superseded;
|
|
612
|
+
// `droppedInputs` is reserved for actual capacity loss.
|
|
613
|
+
attempts.superseded += inputs.size;
|
|
614
|
+
inputs.clear();
|
|
615
|
+
while (inputs.size >= MAX_PENDING_INPUTS) {
|
|
616
|
+
inputs.delete(inputs.keys().next().value!);
|
|
617
|
+
droppedInputs += 1;
|
|
618
|
+
attempts.cancelled += 1;
|
|
619
|
+
}
|
|
620
|
+
traceId = createTraceId();
|
|
621
|
+
if (
|
|
622
|
+
(detailed || options.inputOrigin) &&
|
|
623
|
+
origin &&
|
|
624
|
+
!closed &&
|
|
625
|
+
typeof options.inputFingerprintKey === "string" &&
|
|
626
|
+
options.inputFingerprintKey.length >= 32
|
|
627
|
+
) {
|
|
628
|
+
const payload = Buffer.from(origin.payload);
|
|
629
|
+
const event: TuiTerminalInputOriginEvent = {
|
|
630
|
+
processId,
|
|
631
|
+
clockId: "opentui-performance-now",
|
|
632
|
+
clockKind: "performance-now",
|
|
633
|
+
atMicros:
|
|
634
|
+
Number.isSafeInteger(origin.ingressAtMicros) &&
|
|
635
|
+
origin.ingressAtMicros! <= startedAtMicros
|
|
636
|
+
? origin.ingressAtMicros!
|
|
637
|
+
: startedAtMicros,
|
|
638
|
+
origin: origin.origin,
|
|
639
|
+
payloadByteCount: payload.byteLength,
|
|
640
|
+
payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
|
|
641
|
+
.update(traceId)
|
|
642
|
+
.update("\0")
|
|
643
|
+
.update(payload)
|
|
644
|
+
.digest("hex"),
|
|
645
|
+
parserConsumption:
|
|
646
|
+
origin.origin === "keyboard"
|
|
647
|
+
? "keyboard-event"
|
|
648
|
+
: origin.origin === "bracketed-paste"
|
|
649
|
+
? "paste-event"
|
|
650
|
+
: "pointer-event",
|
|
651
|
+
...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
|
|
652
|
+
...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
|
|
653
|
+
...(Number.isSafeInteger(origin.pointerColumn)
|
|
654
|
+
? { pointerColumn: origin.pointerColumn }
|
|
655
|
+
: {}),
|
|
656
|
+
...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
|
|
657
|
+
...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
|
|
658
|
+
? { pointerButton: origin.pointerButton }
|
|
659
|
+
: {}),
|
|
660
|
+
traceId,
|
|
661
|
+
semanticPaneId: origin.semanticPaneId,
|
|
662
|
+
generation: origin.generation,
|
|
663
|
+
incarnation: origin.incarnation,
|
|
664
|
+
revision: origin.revision,
|
|
665
|
+
stateHash: origin.stateHash,
|
|
666
|
+
};
|
|
667
|
+
options.append({ version: 1, type: "performance.input-origin", ...event });
|
|
668
|
+
}
|
|
669
|
+
inputs.set(traceId, {
|
|
670
|
+
startedAtMicros,
|
|
671
|
+
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
672
|
+
endedAtMicros: null,
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
attempts.cancelled += 1;
|
|
676
|
+
throw error;
|
|
640
677
|
}
|
|
641
|
-
inputs.set(traceId, {
|
|
642
|
-
startedAtMicros,
|
|
643
|
-
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
644
|
-
endedAtMicros: null,
|
|
645
|
-
});
|
|
646
678
|
let finished = false;
|
|
647
679
|
return Object.freeze({
|
|
648
680
|
traceId,
|
|
@@ -652,11 +684,16 @@ export function createReferencePerformanceTraceSink(options: {
|
|
|
652
684
|
const input = inputs.get(traceId);
|
|
653
685
|
if (input) input.endedAtMicros = nowMicros();
|
|
654
686
|
},
|
|
655
|
-
cancel: () =>
|
|
687
|
+
cancel: () => {
|
|
688
|
+
if (inputs.delete(traceId)) attempts.cancelled += 1;
|
|
689
|
+
},
|
|
656
690
|
});
|
|
657
691
|
},
|
|
658
|
-
terminalTraceSpan: (paint: TuiTerminalTraceSpanEvent) =>
|
|
659
|
-
|
|
692
|
+
terminalTraceSpan: (paint: TuiTerminalTraceSpanEvent) => {
|
|
693
|
+
if (closed || paint.processId !== processId) return;
|
|
694
|
+
const outcome = recordCompletedTrace(inputs, paint, options.append);
|
|
695
|
+
if (outcome) attempts[outcome] += 1;
|
|
696
|
+
},
|
|
660
697
|
terminalTraceStage: (event: TuiTerminalTraceStageEvent) => {
|
|
661
698
|
if ((detailed || options.inputDetail) && !closed)
|
|
662
699
|
options.append({ version: 1, type: "performance.stage", ...event });
|
|
@@ -675,6 +712,7 @@ export function createReferencePerformanceTraceSink(options: {
|
|
|
675
712
|
// Shutdown cancels probes that can no longer observe a future paint.
|
|
676
713
|
// They are not backpressure loss and must not survive as phantom work in
|
|
677
714
|
// a closed collector summary.
|
|
715
|
+
attempts.cancelled += inputs.size;
|
|
678
716
|
inputs.clear();
|
|
679
717
|
return snapshot();
|
|
680
718
|
},
|
|
@@ -692,12 +730,21 @@ function recordCompletedTrace(
|
|
|
692
730
|
>,
|
|
693
731
|
paint: TuiTerminalTraceSpanEvent,
|
|
694
732
|
appendRecord: (value: Readonly<Record<string, unknown>>) => void,
|
|
695
|
-
):
|
|
733
|
+
): "completed" | "expired" | null {
|
|
696
734
|
const input = inputs.get(paint.traceId);
|
|
697
|
-
if (!input || input.endedAtMicros === null) return;
|
|
735
|
+
if (!input || input.endedAtMicros === null) return null;
|
|
736
|
+
if (
|
|
737
|
+
paint.clockId !== "opentui-performance-now" ||
|
|
738
|
+
paint.clockKind !== "performance-now" ||
|
|
739
|
+
!Number.isFinite(paint.startedAtMicros) ||
|
|
740
|
+
!Number.isFinite(paint.endedAtMicros) ||
|
|
741
|
+
paint.startedAtMicros < input.endedAtMicros ||
|
|
742
|
+
paint.endedAtMicros < paint.startedAtMicros
|
|
743
|
+
)
|
|
744
|
+
return null;
|
|
698
745
|
if (paint.startedAtMicros > input.expiresAtMicros) {
|
|
699
746
|
inputs.delete(paint.traceId);
|
|
700
|
-
return;
|
|
747
|
+
return "expired";
|
|
701
748
|
}
|
|
702
749
|
inputs.delete(paint.traceId);
|
|
703
750
|
appendRecord({
|
|
@@ -714,14 +761,18 @@ function recordCompletedTrace(
|
|
|
714
761
|
authority: { generation: paint.generation, incarnation: paint.incarnation },
|
|
715
762
|
});
|
|
716
763
|
appendRecord({ version: 1, type: "performance.stage", ...paint });
|
|
764
|
+
return "completed";
|
|
717
765
|
}
|
|
718
766
|
|
|
719
767
|
function expireInputs(
|
|
720
768
|
inputs: Map<string, { readonly expiresAtMicros: number }>,
|
|
721
769
|
nowMicros: number,
|
|
722
|
-
):
|
|
770
|
+
): number {
|
|
771
|
+
let expired = 0;
|
|
723
772
|
for (const [traceId, input] of inputs) {
|
|
724
773
|
if (input.expiresAtMicros > nowMicros) break;
|
|
725
774
|
inputs.delete(traceId);
|
|
775
|
+
expired += 1;
|
|
726
776
|
}
|
|
777
|
+
return expired;
|
|
727
778
|
}
|
|
@@ -25,7 +25,7 @@ export function createApplicationTerminalInputIngress(
|
|
|
25
25
|
generation: () => OpenTuiGenerationHostSnapshot | null,
|
|
26
26
|
sessionOwner: () => OpenTuiSessionOwner | null,
|
|
27
27
|
focusedPane: () => string | null,
|
|
28
|
-
setNote: (note: string | null) => void,
|
|
28
|
+
setNote: (note: string | null | ((current: string | null) => string | null)) => void,
|
|
29
29
|
) {
|
|
30
30
|
const pending = createApplicationPendingTerminalInputOwner();
|
|
31
31
|
let ownsNote = false;
|
|
@@ -87,6 +87,20 @@ export function createApplicationTerminalInputIngress(
|
|
|
87
87
|
left.clientGeneration === right.clientGeneration &&
|
|
88
88
|
left.session === right.session &&
|
|
89
89
|
left.pane === right.pane;
|
|
90
|
+
// Rejection is an outcome for this exact terminal scope, not queue occupancy.
|
|
91
|
+
// Successful delivery of the admitted prefix must not hide lost input.
|
|
92
|
+
let rejectedRecoveryScope: RecoveryScope | null = null;
|
|
93
|
+
const rejectedRecoveryNote = "some terminal input was not sent during recovery";
|
|
94
|
+
const rejectedRecoveryAdmissionNote = `terminal input queue full · ${rejectedRecoveryNote}`;
|
|
95
|
+
const clearRejectedRecovery = () => {
|
|
96
|
+
if (!rejectedRecoveryScope) return;
|
|
97
|
+
rejectedRecoveryScope = null;
|
|
98
|
+
setNote((current) =>
|
|
99
|
+
current === rejectedRecoveryNote || current === rejectedRecoveryAdmissionNote
|
|
100
|
+
? null
|
|
101
|
+
: current,
|
|
102
|
+
);
|
|
103
|
+
};
|
|
90
104
|
let recoveryVersion = 0;
|
|
91
105
|
const cancelRecovery = () => {
|
|
92
106
|
recoveryVersion++;
|
|
@@ -105,6 +119,7 @@ export function createApplicationTerminalInputIngress(
|
|
|
105
119
|
return false;
|
|
106
120
|
const target = scope();
|
|
107
121
|
if (!target) return false;
|
|
122
|
+
if (rejectedRecoveryScope && !sameScope(rejectedRecoveryScope, target)) clearRejectedRecovery();
|
|
108
123
|
if (recovery && !sameScope(recovery.scope, target)) cancelRecovery();
|
|
109
124
|
if (!recovery) {
|
|
110
125
|
const timer = setTimeout(() => {
|
|
@@ -116,7 +131,8 @@ export function createApplicationTerminalInputIngress(
|
|
|
116
131
|
}
|
|
117
132
|
const weight = bytes.byteLength;
|
|
118
133
|
if (recovery.admitted >= 64 || recovery.weight + weight > 1024 * 1024) {
|
|
119
|
-
|
|
134
|
+
rejectedRecoveryScope = target;
|
|
135
|
+
setNote(rejectedRecoveryAdmissionNote);
|
|
120
136
|
return true;
|
|
121
137
|
}
|
|
122
138
|
recovery.weight += weight;
|
|
@@ -144,7 +160,7 @@ export function createApplicationTerminalInputIngress(
|
|
|
144
160
|
}
|
|
145
161
|
return true;
|
|
146
162
|
});
|
|
147
|
-
setNote(`${target.session} · terminal ${kind} queued`);
|
|
163
|
+
if (!rejectedRecoveryScope) setNote(`${target.session} · terminal ${kind} queued`);
|
|
148
164
|
return true;
|
|
149
165
|
};
|
|
150
166
|
const flushRecovery = () => {
|
|
@@ -173,7 +189,11 @@ export function createApplicationTerminalInputIngress(
|
|
|
173
189
|
}
|
|
174
190
|
if (version === recoveryVersion) {
|
|
175
191
|
cancelRecovery();
|
|
176
|
-
|
|
192
|
+
if (rejectedRecoveryScope) {
|
|
193
|
+
setNote((current) =>
|
|
194
|
+
current === rejectedRecoveryAdmissionNote ? rejectedRecoveryNote : current,
|
|
195
|
+
);
|
|
196
|
+
} else setNote(null);
|
|
177
197
|
}
|
|
178
198
|
})();
|
|
179
199
|
};
|
|
@@ -210,6 +230,7 @@ export function createApplicationTerminalInputIngress(
|
|
|
210
230
|
|
|
211
231
|
const cancelInteractionInput = () => {
|
|
212
232
|
cancelRecovery();
|
|
233
|
+
clearRejectedRecovery();
|
|
213
234
|
interaction.cancelPendingInput();
|
|
214
235
|
};
|
|
215
236
|
return {
|
|
@@ -236,6 +257,8 @@ export function createApplicationTerminalInputIngress(
|
|
|
236
257
|
});
|
|
237
258
|
},
|
|
238
259
|
adopt(terminalsActive = true): void {
|
|
260
|
+
if (rejectedRecoveryScope && !sameScope(rejectedRecoveryScope, scope()))
|
|
261
|
+
clearRejectedRecovery();
|
|
239
262
|
if (!terminalsActive) cancelInteractionInput();
|
|
240
263
|
flush();
|
|
241
264
|
flushRecovery();
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createRenderEffect,
|
|
4
|
+
createSignal,
|
|
5
|
+
Show,
|
|
6
|
+
onCleanup,
|
|
7
|
+
untrack,
|
|
8
|
+
type Accessor,
|
|
9
|
+
} from "solid-js";
|
|
3
10
|
|
|
4
11
|
import type {
|
|
5
12
|
PaneSearchHighlight,
|
|
@@ -13,6 +20,7 @@ import type { TerminalReplicaSnapshot } from "@tmux-ide/contracts";
|
|
|
13
20
|
export interface PaneScopedTerminalAdapter {
|
|
14
21
|
readonly renderSource: import("../pane-surface.tsx").TerminalPaneRenderSource;
|
|
15
22
|
paneVersion(paneId: string): number;
|
|
23
|
+
paneUnavailable?(paneId: string): string | null;
|
|
16
24
|
panePresentationVersion?(paneId: string): number;
|
|
17
25
|
paneSourceEpoch(): number;
|
|
18
26
|
/** Coalesced exceptional recovery when a formerly coherent snapshot is absent. */
|
|
@@ -63,6 +71,7 @@ export interface PaneScopedTerminalSurfaceProps {
|
|
|
63
71
|
/** One Solid owner per terminal pane; terminal output never wakes the root shell. */
|
|
64
72
|
export function PaneScopedTerminalSurface(props: PaneScopedTerminalSurfaceProps) {
|
|
65
73
|
let surface: PaneSurfaceRenderable | undefined;
|
|
74
|
+
const [unavailable, setUnavailable] = createSignal<string | null>(null);
|
|
66
75
|
let activationEpoch = 0;
|
|
67
76
|
let wasActive = false;
|
|
68
77
|
let hadCanonicalSnapshot = false;
|
|
@@ -74,6 +83,9 @@ export function PaneScopedTerminalSurface(props: PaneScopedTerminalSurfaceProps)
|
|
|
74
83
|
adapter: PaneScopedTerminalAdapter,
|
|
75
84
|
paneId: string,
|
|
76
85
|
): void => {
|
|
86
|
+
const message = adapter.paneUnavailable?.(paneId) ?? null;
|
|
87
|
+
setUnavailable(message);
|
|
88
|
+
if (message) return;
|
|
77
89
|
const identity = adapter.renderSource.paneCanonicalIdentity?.(paneId) ?? null;
|
|
78
90
|
if (identity) {
|
|
79
91
|
hadCanonicalSnapshot = true;
|
|
@@ -122,35 +134,53 @@ export function PaneScopedTerminalSurface(props: PaneScopedTerminalSurfaceProps)
|
|
|
122
134
|
});
|
|
123
135
|
|
|
124
136
|
return (
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
137
|
+
<box width={props.width} height={props.height}>
|
|
138
|
+
<pane_surface
|
|
139
|
+
ref={(renderable: PaneSurfaceRenderable) => {
|
|
140
|
+
surface = renderable;
|
|
141
|
+
renderable.contentVersion = props.adapter.paneVersion(props.paneId);
|
|
142
|
+
renderable.presentationVersion =
|
|
143
|
+
props.adapter.panePresentationVersion?.(props.paneId) ?? 0;
|
|
144
|
+
renderable.sourceEpoch = props.sourceEpoch + props.adapter.paneSourceEpoch();
|
|
145
|
+
renderable.presentationGeneration = currentPresentationGeneration();
|
|
146
|
+
ensureRetainedCanonicalState(props.adapter, props.paneId);
|
|
147
|
+
}}
|
|
148
|
+
width={props.width}
|
|
149
|
+
height={props.height}
|
|
150
|
+
mirror={props.adapter.renderSource}
|
|
151
|
+
paneId={props.paneId}
|
|
152
|
+
defaultFg={props.defaultFg}
|
|
153
|
+
defaultBg={props.defaultBg}
|
|
154
|
+
terminalPalette={props.terminalPalette}
|
|
155
|
+
searchHl={props.searchHl}
|
|
156
|
+
searchCur={props.searchCur}
|
|
157
|
+
scrollOffset={props.scrollOffset}
|
|
158
|
+
viewportOrigin={props.viewportOrigin}
|
|
159
|
+
paneFocused={props.paneFocused}
|
|
160
|
+
contentVersion={props.adapter.paneVersion(props.paneId)}
|
|
161
|
+
presentationVersion={props.adapter.panePresentationVersion?.(props.paneId) ?? 0}
|
|
162
|
+
sourceEpoch={props.sourceEpoch + props.adapter.paneSourceEpoch()}
|
|
163
|
+
rendererEpoch={props.sourceEpoch}
|
|
164
|
+
hostFocusTransitionOwner={props.hostFocusTransitionOwner}
|
|
165
|
+
selRange={props.selRange}
|
|
166
|
+
copyCursor={props.copyCursor}
|
|
167
|
+
search={props.search}
|
|
168
|
+
/>
|
|
169
|
+
<Show when={unavailable()}>
|
|
170
|
+
{(message) => (
|
|
171
|
+
<box
|
|
172
|
+
position="absolute"
|
|
173
|
+
left={0}
|
|
174
|
+
top={0}
|
|
175
|
+
width={props.width}
|
|
176
|
+
height={props.height}
|
|
177
|
+
backgroundColor={`#${props.defaultBg.toString(16).padStart(6, "0")}`}
|
|
178
|
+
padding={1}
|
|
179
|
+
>
|
|
180
|
+
<text fg={`#${props.defaultFg.toString(16).padStart(6, "0")}`}>{message()}</text>
|
|
181
|
+
</box>
|
|
182
|
+
)}
|
|
183
|
+
</Show>
|
|
184
|
+
</box>
|
|
155
185
|
);
|
|
156
186
|
}
|
|
@@ -155,6 +155,10 @@ export class TerminalFastLaneRendererAdapter implements PaneScopedTerminalAdapte
|
|
|
155
155
|
this.#resourceSampler = resourceSampler;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
paneUnavailable(paneId: string): string | null {
|
|
159
|
+
return this.#lane.paneUnavailable(paneId);
|
|
160
|
+
}
|
|
161
|
+
|
|
158
162
|
paneVersion(paneId: string): number {
|
|
159
163
|
return this.#panes.get(paneId)?.version ?? 0;
|
|
160
164
|
}
|
|
@@ -432,9 +436,33 @@ export class TerminalFastLaneRendererAdapter implements PaneScopedTerminalAdapte
|
|
|
432
436
|
const interest = this.#interest(paneId);
|
|
433
437
|
interest.listeners.add(listener);
|
|
434
438
|
if (!interest.release) {
|
|
435
|
-
interest.release = this.#lane.subscribePane(
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
interest.release = this.#lane.subscribePane(
|
|
440
|
+
paneId,
|
|
441
|
+
(publication) => {
|
|
442
|
+
this.#publish(interest, publication);
|
|
443
|
+
},
|
|
444
|
+
() => {
|
|
445
|
+
interest.retainedView?.capture?.abort();
|
|
446
|
+
interest.retainedView = undefined;
|
|
447
|
+
interest.state = null;
|
|
448
|
+
interest.pendingTrace = null;
|
|
449
|
+
interest.pendingHostFrame = null;
|
|
450
|
+
interest.rowProjectionCache?.clear();
|
|
451
|
+
interest.version++;
|
|
452
|
+
for (const listener of [...interest.listeners]) {
|
|
453
|
+
try {
|
|
454
|
+
listener(
|
|
455
|
+
interest.version,
|
|
456
|
+
this.#sourceEpoch,
|
|
457
|
+
interest.presentationVersion,
|
|
458
|
+
"content",
|
|
459
|
+
);
|
|
460
|
+
} catch {
|
|
461
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
);
|
|
438
466
|
}
|
|
439
467
|
// The lane owns a generation-scoped canonical replica even while a pane is
|
|
440
468
|
// off-screen. A newly mounted surface must be invalidated synchronously
|
|
@@ -53,17 +53,20 @@ export function terminalInputsForPaste(text: string): readonly SessionRuntimeTer
|
|
|
53
53
|
if (text.includes("\0")) throw new TypeError("terminal paste must not contain NUL");
|
|
54
54
|
const framed = `\u001b[200~${text}\u001b[201~`;
|
|
55
55
|
const inputs: SessionRuntimeTerminalInput[] = [];
|
|
56
|
-
for (
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
for (let offset = 0; offset < framed.length; ) {
|
|
57
|
+
let end = Math.min(offset + SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS, framed.length);
|
|
58
|
+
// Messages can be encoded independently. Keep a supplementary code point
|
|
59
|
+
// intact instead of relying on the receiver to rejoin its UTF-16 halves.
|
|
60
|
+
const last = framed.charCodeAt(end - 1);
|
|
61
|
+
const next = framed.charCodeAt(end);
|
|
62
|
+
if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) end -= 1;
|
|
61
63
|
inputs.push(
|
|
62
64
|
SessionRuntimeTerminalInputSchemaZ.parse({
|
|
63
65
|
kind: "text",
|
|
64
|
-
data: framed.slice(offset,
|
|
66
|
+
data: framed.slice(offset, end),
|
|
65
67
|
}),
|
|
66
68
|
);
|
|
69
|
+
offset = end;
|
|
67
70
|
}
|
|
68
71
|
return Object.freeze(inputs);
|
|
69
72
|
}
|