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
|
@@ -154,13 +154,13 @@ export function createReferenceTraceWriter(stream, options = {}) {
|
|
|
154
154
|
firstDroppedRecord ??= droppedRecordKind(value);
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
|
-
const line = `${JSON.stringify(value)}\n`;
|
|
158
|
-
const bytes = Buffer.byteLength(line);
|
|
159
|
-
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
160
|
-
oversizedRecords += 1;
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
157
|
try {
|
|
158
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
159
|
+
const bytes = Buffer.byteLength(line);
|
|
160
|
+
if (bytes > MAX_TRACE_RECORD_BYTES) {
|
|
161
|
+
oversizedRecords += 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
164
|
if (!saturated && pendingCount() === 0) {
|
|
165
165
|
writeLine(line);
|
|
166
166
|
return;
|
|
@@ -233,7 +233,15 @@ export function createReferenceTraceWriter(stream, options = {}) {
|
|
|
233
233
|
return Object.freeze({ append, appendCritical, snapshot, close });
|
|
234
234
|
}
|
|
235
235
|
function droppedRecordKind(value) {
|
|
236
|
-
const field = (key) =>
|
|
236
|
+
const field = (key) => {
|
|
237
|
+
try {
|
|
238
|
+
const result = value[key];
|
|
239
|
+
return typeof result === "string" ? result.slice(0, 64) : null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
237
245
|
return Object.freeze({
|
|
238
246
|
type: field("type"),
|
|
239
247
|
stage: field("stage"),
|
|
@@ -258,7 +266,12 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
258
266
|
const inputs = new Map();
|
|
259
267
|
let droppedInputs = 0;
|
|
260
268
|
let closed = false;
|
|
261
|
-
const
|
|
269
|
+
const attempts = { begun: 0, completed: 0, superseded: 0, expired: 0, cancelled: 0 };
|
|
270
|
+
const snapshot = () => Object.freeze({
|
|
271
|
+
pendingInputs: inputs.size,
|
|
272
|
+
droppedInputs,
|
|
273
|
+
inputAttempts: Object.freeze({ ...attempts, pending: inputs.size }),
|
|
274
|
+
});
|
|
262
275
|
return Object.freeze({
|
|
263
276
|
detailedWindowPresentationFrames: detailed ? true : undefined,
|
|
264
277
|
frame: (intervalMs, window) => {
|
|
@@ -464,67 +477,79 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
464
477
|
}
|
|
465
478
|
: {}),
|
|
466
479
|
beginTerminalInput: (origin) => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
480
|
+
if (closed)
|
|
481
|
+
return Object.freeze({ traceId: "", finish: () => { }, cancel: () => { } });
|
|
482
|
+
attempts.begun += 1;
|
|
483
|
+
let traceId;
|
|
484
|
+
try {
|
|
485
|
+
const startedAtMicros = nowMicros();
|
|
486
|
+
attempts.expired += expireInputs(inputs, startedAtMicros);
|
|
487
|
+
// The transport carries one latest-only performance trace id. Once a
|
|
488
|
+
// newer input is admitted, an older probe can no longer be attributed to
|
|
489
|
+
// the exact changed-cell paint without lying. Retire it as superseded;
|
|
490
|
+
// `droppedInputs` is reserved for actual capacity loss.
|
|
491
|
+
attempts.superseded += inputs.size;
|
|
492
|
+
inputs.clear();
|
|
493
|
+
while (inputs.size >= MAX_PENDING_INPUTS) {
|
|
494
|
+
inputs.delete(inputs.keys().next().value);
|
|
495
|
+
droppedInputs += 1;
|
|
496
|
+
attempts.cancelled += 1;
|
|
497
|
+
}
|
|
498
|
+
traceId = createTraceId();
|
|
499
|
+
if ((detailed || options.inputOrigin) &&
|
|
500
|
+
origin &&
|
|
501
|
+
!closed &&
|
|
502
|
+
typeof options.inputFingerprintKey === "string" &&
|
|
503
|
+
options.inputFingerprintKey.length >= 32) {
|
|
504
|
+
const payload = Buffer.from(origin.payload);
|
|
505
|
+
const event = {
|
|
506
|
+
processId,
|
|
507
|
+
clockId: "opentui-performance-now",
|
|
508
|
+
clockKind: "performance-now",
|
|
509
|
+
atMicros: Number.isSafeInteger(origin.ingressAtMicros) &&
|
|
510
|
+
origin.ingressAtMicros <= startedAtMicros
|
|
511
|
+
? origin.ingressAtMicros
|
|
512
|
+
: startedAtMicros,
|
|
513
|
+
origin: origin.origin,
|
|
514
|
+
payloadByteCount: payload.byteLength,
|
|
515
|
+
payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
|
|
516
|
+
.update(traceId)
|
|
517
|
+
.update("\0")
|
|
518
|
+
.update(payload)
|
|
519
|
+
.digest("hex"),
|
|
520
|
+
parserConsumption: origin.origin === "keyboard"
|
|
521
|
+
? "keyboard-event"
|
|
522
|
+
: origin.origin === "bracketed-paste"
|
|
523
|
+
? "paste-event"
|
|
524
|
+
: "pointer-event",
|
|
525
|
+
...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
|
|
526
|
+
...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
|
|
527
|
+
...(Number.isSafeInteger(origin.pointerColumn)
|
|
528
|
+
? { pointerColumn: origin.pointerColumn }
|
|
529
|
+
: {}),
|
|
530
|
+
...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
|
|
531
|
+
...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
|
|
532
|
+
? { pointerButton: origin.pointerButton }
|
|
533
|
+
: {}),
|
|
534
|
+
traceId,
|
|
535
|
+
semanticPaneId: origin.semanticPaneId,
|
|
536
|
+
generation: origin.generation,
|
|
537
|
+
incarnation: origin.incarnation,
|
|
538
|
+
revision: origin.revision,
|
|
539
|
+
stateHash: origin.stateHash,
|
|
540
|
+
};
|
|
541
|
+
options.append({ version: 1, type: "performance.input-origin", ...event });
|
|
542
|
+
}
|
|
543
|
+
inputs.set(traceId, {
|
|
544
|
+
startedAtMicros,
|
|
545
|
+
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
546
|
+
endedAtMicros: null,
|
|
547
|
+
});
|
|
477
548
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
!closed &&
|
|
482
|
-
typeof options.inputFingerprintKey === "string" &&
|
|
483
|
-
options.inputFingerprintKey.length >= 32) {
|
|
484
|
-
const payload = Buffer.from(origin.payload);
|
|
485
|
-
const event = {
|
|
486
|
-
processId,
|
|
487
|
-
clockId: "opentui-performance-now",
|
|
488
|
-
clockKind: "performance-now",
|
|
489
|
-
atMicros: Number.isSafeInteger(origin.ingressAtMicros) &&
|
|
490
|
-
origin.ingressAtMicros <= startedAtMicros
|
|
491
|
-
? origin.ingressAtMicros
|
|
492
|
-
: startedAtMicros,
|
|
493
|
-
origin: origin.origin,
|
|
494
|
-
payloadByteCount: payload.byteLength,
|
|
495
|
-
payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
|
|
496
|
-
.update(traceId)
|
|
497
|
-
.update("\0")
|
|
498
|
-
.update(payload)
|
|
499
|
-
.digest("hex"),
|
|
500
|
-
parserConsumption: origin.origin === "keyboard"
|
|
501
|
-
? "keyboard-event"
|
|
502
|
-
: origin.origin === "bracketed-paste"
|
|
503
|
-
? "paste-event"
|
|
504
|
-
: "pointer-event",
|
|
505
|
-
...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
|
|
506
|
-
...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
|
|
507
|
-
...(Number.isSafeInteger(origin.pointerColumn)
|
|
508
|
-
? { pointerColumn: origin.pointerColumn }
|
|
509
|
-
: {}),
|
|
510
|
-
...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
|
|
511
|
-
...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
|
|
512
|
-
? { pointerButton: origin.pointerButton }
|
|
513
|
-
: {}),
|
|
514
|
-
traceId,
|
|
515
|
-
semanticPaneId: origin.semanticPaneId,
|
|
516
|
-
generation: origin.generation,
|
|
517
|
-
incarnation: origin.incarnation,
|
|
518
|
-
revision: origin.revision,
|
|
519
|
-
stateHash: origin.stateHash,
|
|
520
|
-
};
|
|
521
|
-
options.append({ version: 1, type: "performance.input-origin", ...event });
|
|
549
|
+
catch (error) {
|
|
550
|
+
attempts.cancelled += 1;
|
|
551
|
+
throw error;
|
|
522
552
|
}
|
|
523
|
-
inputs.set(traceId, {
|
|
524
|
-
startedAtMicros,
|
|
525
|
-
expiresAtMicros: startedAtMicros + INPUT_EXPIRY_MICROS,
|
|
526
|
-
endedAtMicros: null,
|
|
527
|
-
});
|
|
528
553
|
let finished = false;
|
|
529
554
|
return Object.freeze({
|
|
530
555
|
traceId,
|
|
@@ -536,10 +561,19 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
536
561
|
if (input)
|
|
537
562
|
input.endedAtMicros = nowMicros();
|
|
538
563
|
},
|
|
539
|
-
cancel: () =>
|
|
564
|
+
cancel: () => {
|
|
565
|
+
if (inputs.delete(traceId))
|
|
566
|
+
attempts.cancelled += 1;
|
|
567
|
+
},
|
|
540
568
|
});
|
|
541
569
|
},
|
|
542
|
-
terminalTraceSpan: (paint) =>
|
|
570
|
+
terminalTraceSpan: (paint) => {
|
|
571
|
+
if (closed || paint.processId !== processId)
|
|
572
|
+
return;
|
|
573
|
+
const outcome = recordCompletedTrace(inputs, paint, options.append);
|
|
574
|
+
if (outcome)
|
|
575
|
+
attempts[outcome] += 1;
|
|
576
|
+
},
|
|
543
577
|
terminalTraceStage: (event) => {
|
|
544
578
|
if ((detailed || options.inputDetail) && !closed)
|
|
545
579
|
options.append({ version: 1, type: "performance.stage", ...event });
|
|
@@ -560,6 +594,7 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
560
594
|
// Shutdown cancels probes that can no longer observe a future paint.
|
|
561
595
|
// They are not backpressure loss and must not survive as phantom work in
|
|
562
596
|
// a closed collector summary.
|
|
597
|
+
attempts.cancelled += inputs.size;
|
|
563
598
|
inputs.clear();
|
|
564
599
|
return snapshot();
|
|
565
600
|
},
|
|
@@ -568,10 +603,17 @@ export function createReferencePerformanceTraceSink(options) {
|
|
|
568
603
|
function recordCompletedTrace(inputs, paint, appendRecord) {
|
|
569
604
|
const input = inputs.get(paint.traceId);
|
|
570
605
|
if (!input || input.endedAtMicros === null)
|
|
571
|
-
return;
|
|
606
|
+
return null;
|
|
607
|
+
if (paint.clockId !== "opentui-performance-now" ||
|
|
608
|
+
paint.clockKind !== "performance-now" ||
|
|
609
|
+
!Number.isFinite(paint.startedAtMicros) ||
|
|
610
|
+
!Number.isFinite(paint.endedAtMicros) ||
|
|
611
|
+
paint.startedAtMicros < input.endedAtMicros ||
|
|
612
|
+
paint.endedAtMicros < paint.startedAtMicros)
|
|
613
|
+
return null;
|
|
572
614
|
if (paint.startedAtMicros > input.expiresAtMicros) {
|
|
573
615
|
inputs.delete(paint.traceId);
|
|
574
|
-
return;
|
|
616
|
+
return "expired";
|
|
575
617
|
}
|
|
576
618
|
inputs.delete(paint.traceId);
|
|
577
619
|
appendRecord({
|
|
@@ -588,11 +630,15 @@ function recordCompletedTrace(inputs, paint, appendRecord) {
|
|
|
588
630
|
authority: { generation: paint.generation, incarnation: paint.incarnation },
|
|
589
631
|
});
|
|
590
632
|
appendRecord({ version: 1, type: "performance.stage", ...paint });
|
|
633
|
+
return "completed";
|
|
591
634
|
}
|
|
592
635
|
function expireInputs(inputs, nowMicros) {
|
|
636
|
+
let expired = 0;
|
|
593
637
|
for (const [traceId, input] of inputs) {
|
|
594
638
|
if (input.expiresAtMicros > nowMicros)
|
|
595
639
|
break;
|
|
596
640
|
inputs.delete(traceId);
|
|
641
|
+
expired += 1;
|
|
597
642
|
}
|
|
643
|
+
return expired;
|
|
598
644
|
}
|
|
@@ -48,6 +48,19 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
48
48
|
left.clientGeneration === right.clientGeneration &&
|
|
49
49
|
left.session === right.session &&
|
|
50
50
|
left.pane === right.pane;
|
|
51
|
+
// Rejection is an outcome for this exact terminal scope, not queue occupancy.
|
|
52
|
+
// Successful delivery of the admitted prefix must not hide lost input.
|
|
53
|
+
let rejectedRecoveryScope = null;
|
|
54
|
+
const rejectedRecoveryNote = "some terminal input was not sent during recovery";
|
|
55
|
+
const rejectedRecoveryAdmissionNote = `terminal input queue full · ${rejectedRecoveryNote}`;
|
|
56
|
+
const clearRejectedRecovery = () => {
|
|
57
|
+
if (!rejectedRecoveryScope)
|
|
58
|
+
return;
|
|
59
|
+
rejectedRecoveryScope = null;
|
|
60
|
+
setNote((current) => current === rejectedRecoveryNote || current === rejectedRecoveryAdmissionNote
|
|
61
|
+
? null
|
|
62
|
+
: current);
|
|
63
|
+
};
|
|
51
64
|
let recoveryVersion = 0;
|
|
52
65
|
const cancelRecovery = () => {
|
|
53
66
|
recoveryVersion++;
|
|
@@ -62,6 +75,8 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
62
75
|
const target = scope();
|
|
63
76
|
if (!target)
|
|
64
77
|
return false;
|
|
78
|
+
if (rejectedRecoveryScope && !sameScope(rejectedRecoveryScope, target))
|
|
79
|
+
clearRejectedRecovery();
|
|
65
80
|
if (recovery && !sameScope(recovery.scope, target))
|
|
66
81
|
cancelRecovery();
|
|
67
82
|
if (!recovery) {
|
|
@@ -74,7 +89,8 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
74
89
|
}
|
|
75
90
|
const weight = bytes.byteLength;
|
|
76
91
|
if (recovery.admitted >= 64 || recovery.weight + weight > 1024 * 1024) {
|
|
77
|
-
|
|
92
|
+
rejectedRecoveryScope = target;
|
|
93
|
+
setNote(rejectedRecoveryAdmissionNote);
|
|
78
94
|
return true;
|
|
79
95
|
}
|
|
80
96
|
recovery.weight += weight;
|
|
@@ -96,7 +112,8 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
96
112
|
}
|
|
97
113
|
return true;
|
|
98
114
|
});
|
|
99
|
-
|
|
115
|
+
if (!rejectedRecoveryScope)
|
|
116
|
+
setNote(`${target.session} · terminal ${kind} queued`);
|
|
100
117
|
return true;
|
|
101
118
|
};
|
|
102
119
|
const flushRecovery = () => {
|
|
@@ -130,7 +147,11 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
130
147
|
}
|
|
131
148
|
if (version === recoveryVersion) {
|
|
132
149
|
cancelRecovery();
|
|
133
|
-
|
|
150
|
+
if (rejectedRecoveryScope) {
|
|
151
|
+
setNote((current) => current === rejectedRecoveryAdmissionNote ? rejectedRecoveryNote : current);
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
setNote(null);
|
|
134
155
|
}
|
|
135
156
|
})();
|
|
136
157
|
};
|
|
@@ -165,6 +186,7 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
165
186
|
};
|
|
166
187
|
const cancelInteractionInput = () => {
|
|
167
188
|
cancelRecovery();
|
|
189
|
+
clearRejectedRecovery();
|
|
168
190
|
interaction.cancelPendingInput();
|
|
169
191
|
};
|
|
170
192
|
return {
|
|
@@ -191,6 +213,8 @@ export function createApplicationTerminalInputIngress(interaction, generation, s
|
|
|
191
213
|
});
|
|
192
214
|
},
|
|
193
215
|
adopt(terminalsActive = true) {
|
|
216
|
+
if (rejectedRecoveryScope && !sameScope(rejectedRecoveryScope, scope()))
|
|
217
|
+
clearRejectedRecovery();
|
|
194
218
|
if (!terminalsActive)
|
|
195
219
|
cancelInteractionInput();
|
|
196
220
|
flush();
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/* @jsxImportSource @opentui/solid */
|
|
2
|
-
import { createRenderEffect, onCleanup, untrack } from "solid-js";
|
|
2
|
+
import { createRenderEffect, createSignal, Show, onCleanup, untrack, } from "solid-js";
|
|
3
3
|
/** One Solid owner per terminal pane; terminal output never wakes the root shell. */
|
|
4
4
|
export function PaneScopedTerminalSurface(props) {
|
|
5
5
|
let surface;
|
|
6
|
+
const [unavailable, setUnavailable] = createSignal(null);
|
|
6
7
|
let activationEpoch = 0;
|
|
7
8
|
let wasActive = false;
|
|
8
9
|
let hadCanonicalSnapshot = false;
|
|
9
10
|
const isActive = () => typeof props.active === "function" ? props.active() : props.active !== false;
|
|
10
11
|
const currentPresentationGeneration = () => `${props.presentationGeneration ?? "stable"}:activation-${activationEpoch}`;
|
|
11
12
|
const ensureRetainedCanonicalState = (adapter, paneId) => {
|
|
13
|
+
const message = adapter.paneUnavailable?.(paneId) ?? null;
|
|
14
|
+
setUnavailable(message);
|
|
15
|
+
if (message)
|
|
16
|
+
return;
|
|
12
17
|
const identity = adapter.renderSource.paneCanonicalIdentity?.(paneId) ?? null;
|
|
13
18
|
if (identity) {
|
|
14
19
|
hadCanonicalSnapshot = true;
|
|
@@ -56,12 +61,20 @@ export function PaneScopedTerminalSurface(props) {
|
|
|
56
61
|
surface.sourceEpoch = props.sourceEpoch + props.adapter.paneSourceEpoch();
|
|
57
62
|
ensureRetainedCanonicalState(props.adapter, props.paneId);
|
|
58
63
|
});
|
|
59
|
-
return (<
|
|
64
|
+
return (<box width={props.width} height={props.height}>
|
|
65
|
+
<pane_surface ref={(renderable) => {
|
|
60
66
|
surface = renderable;
|
|
61
67
|
renderable.contentVersion = props.adapter.paneVersion(props.paneId);
|
|
62
|
-
renderable.presentationVersion =
|
|
68
|
+
renderable.presentationVersion =
|
|
69
|
+
props.adapter.panePresentationVersion?.(props.paneId) ?? 0;
|
|
63
70
|
renderable.sourceEpoch = props.sourceEpoch + props.adapter.paneSourceEpoch();
|
|
64
71
|
renderable.presentationGeneration = currentPresentationGeneration();
|
|
65
72
|
ensureRetainedCanonicalState(props.adapter, props.paneId);
|
|
66
|
-
}} width={props.width} height={props.height} mirror={props.adapter.renderSource} paneId={props.paneId} defaultFg={props.defaultFg} defaultBg={props.defaultBg} terminalPalette={props.terminalPalette} searchHl={props.searchHl} searchCur={props.searchCur} scrollOffset={props.scrollOffset} viewportOrigin={props.viewportOrigin} paneFocused={props.paneFocused} contentVersion={props.adapter.paneVersion(props.paneId)} presentationVersion={props.adapter.panePresentationVersion?.(props.paneId) ?? 0} sourceEpoch={props.sourceEpoch + props.adapter.paneSourceEpoch()} rendererEpoch={props.sourceEpoch} hostFocusTransitionOwner={props.hostFocusTransitionOwner} selRange={props.selRange} copyCursor={props.copyCursor} search={props.search}/>
|
|
73
|
+
}} width={props.width} height={props.height} mirror={props.adapter.renderSource} paneId={props.paneId} defaultFg={props.defaultFg} defaultBg={props.defaultBg} terminalPalette={props.terminalPalette} searchHl={props.searchHl} searchCur={props.searchCur} scrollOffset={props.scrollOffset} viewportOrigin={props.viewportOrigin} paneFocused={props.paneFocused} contentVersion={props.adapter.paneVersion(props.paneId)} presentationVersion={props.adapter.panePresentationVersion?.(props.paneId) ?? 0} sourceEpoch={props.sourceEpoch + props.adapter.paneSourceEpoch()} rendererEpoch={props.sourceEpoch} hostFocusTransitionOwner={props.hostFocusTransitionOwner} selRange={props.selRange} copyCursor={props.copyCursor} search={props.search}/>
|
|
74
|
+
<Show when={unavailable()}>
|
|
75
|
+
{(message) => (<box position="absolute" left={0} top={0} width={props.width} height={props.height} backgroundColor={`#${props.defaultBg.toString(16).padStart(6, "0")}`} padding={1}>
|
|
76
|
+
<text fg={`#${props.defaultFg.toString(16).padStart(6, "0")}`}>{message()}</text>
|
|
77
|
+
</box>)}
|
|
78
|
+
</Show>
|
|
79
|
+
</box>);
|
|
67
80
|
}
|
|
@@ -63,6 +63,9 @@ export class TerminalFastLaneRendererAdapter {
|
|
|
63
63
|
this.#causalCellLedger = causalCellLedger;
|
|
64
64
|
this.#resourceSampler = resourceSampler;
|
|
65
65
|
}
|
|
66
|
+
paneUnavailable(paneId) {
|
|
67
|
+
return this.#lane.paneUnavailable(paneId);
|
|
68
|
+
}
|
|
66
69
|
paneVersion(paneId) {
|
|
67
70
|
return this.#panes.get(paneId)?.version ?? 0;
|
|
68
71
|
}
|
|
@@ -295,6 +298,22 @@ export class TerminalFastLaneRendererAdapter {
|
|
|
295
298
|
if (!interest.release) {
|
|
296
299
|
interest.release = this.#lane.subscribePane(paneId, (publication) => {
|
|
297
300
|
this.#publish(interest, publication);
|
|
301
|
+
}, () => {
|
|
302
|
+
interest.retainedView?.capture?.abort();
|
|
303
|
+
interest.retainedView = undefined;
|
|
304
|
+
interest.state = null;
|
|
305
|
+
interest.pendingTrace = null;
|
|
306
|
+
interest.pendingHostFrame = null;
|
|
307
|
+
interest.rowProjectionCache?.clear();
|
|
308
|
+
interest.version++;
|
|
309
|
+
for (const listener of [...interest.listeners]) {
|
|
310
|
+
try {
|
|
311
|
+
listener(interest.version, this.#sourceEpoch, interest.presentationVersion, "content");
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
/* Availability observers cannot interrupt sibling delivery. */
|
|
315
|
+
}
|
|
316
|
+
}
|
|
298
317
|
});
|
|
299
318
|
}
|
|
300
319
|
// The lane owns a generation-scoped canonical replica even while a pane is
|
|
@@ -40,11 +40,19 @@ export function terminalInputsForPaste(text) {
|
|
|
40
40
|
throw new TypeError("terminal paste must not contain NUL");
|
|
41
41
|
const framed = `\u001b[200~${text}\u001b[201~`;
|
|
42
42
|
const inputs = [];
|
|
43
|
-
for (let offset = 0; offset < framed.length;
|
|
43
|
+
for (let offset = 0; offset < framed.length;) {
|
|
44
|
+
let end = Math.min(offset + SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS, framed.length);
|
|
45
|
+
// Messages can be encoded independently. Keep a supplementary code point
|
|
46
|
+
// intact instead of relying on the receiver to rejoin its UTF-16 halves.
|
|
47
|
+
const last = framed.charCodeAt(end - 1);
|
|
48
|
+
const next = framed.charCodeAt(end);
|
|
49
|
+
if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff)
|
|
50
|
+
end -= 1;
|
|
44
51
|
inputs.push(SessionRuntimeTerminalInputSchemaZ.parse({
|
|
45
52
|
kind: "text",
|
|
46
|
-
data: framed.slice(offset,
|
|
53
|
+
data: framed.slice(offset, end),
|
|
47
54
|
}));
|
|
55
|
+
offset = end;
|
|
48
56
|
}
|
|
49
57
|
return Object.freeze(inputs);
|
|
50
58
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
function check(condition, message) {
|
|
2
|
+
if (!condition)
|
|
3
|
+
throw Error(message);
|
|
4
|
+
}
|
|
5
|
+
const blank = () => ({ text: "", width: 1, fg: "ffffff", bg: "000000", bold: false });
|
|
6
|
+
export function literalStyledFrame(marker, cols = 40) {
|
|
7
|
+
check(cols === 24 || cols === 40, "literal geometry");
|
|
8
|
+
check(marker.length <= cols, "marker width");
|
|
9
|
+
const cells = Array.from({ length: 8 }, () => Array.from({ length: cols }, blank));
|
|
10
|
+
[...marker].forEach((text, x) => (cells[0][x] = { ...blank(), text }));
|
|
11
|
+
const initial = marker === "BEFORE_SSH";
|
|
12
|
+
const glyphs = initial ? ["X", "界", "", "é", "Z"] : ["Y", "q", "", "é", "R"];
|
|
13
|
+
glyphs.forEach((text, x) => (cells[1][x] = {
|
|
14
|
+
text,
|
|
15
|
+
width: initial ? (x === 1 ? 2 : x === 2 ? 0 : 1) : 1,
|
|
16
|
+
fg: text ? "123456" : "ffffff",
|
|
17
|
+
bg: "214365",
|
|
18
|
+
bold: !!text,
|
|
19
|
+
}));
|
|
20
|
+
for (let x = 0; x < cols; x++)
|
|
21
|
+
cells[3][x] = { ...blank(), text: x < 2 ? String(x + 1) : "", bg: "445566" };
|
|
22
|
+
return { cells, cursor: { x: 4, y: 2, visible: true } };
|
|
23
|
+
}
|
|
24
|
+
export function styledBytes(marker) {
|
|
25
|
+
return `\x1b[0m\x1b[2J\x1b[H${marker}\x1b[2;1H\x1b[1;38;2;18;52;86;48;2;33;67;101m${marker === "BEFORE_SSH" ? "X界éZ" : "Yq éR"}\x1b[0m\x1b[4;1H\x1b[48;2;68;85;102m123456\x1b[4;3H\x1b[K\x1b[0m\x1b[3;5H\x1b]2;${marker}\x07`;
|
|
26
|
+
}
|
|
27
|
+
export function readCompletedFrame(raw) {
|
|
28
|
+
check((raw.cols === 24 || raw.cols === 40) && raw.rows === 8, "geometry");
|
|
29
|
+
const size = raw.cols * raw.rows;
|
|
30
|
+
check(raw.char.length === size &&
|
|
31
|
+
raw.attributes.length === size &&
|
|
32
|
+
raw.fg.length === size * 4 &&
|
|
33
|
+
raw.bg.length === size * 4, "buffer lengths");
|
|
34
|
+
const lines = raw.text.replace(/\n$/u, "").split("\n");
|
|
35
|
+
check(lines.length === 8, "line count");
|
|
36
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
37
|
+
const rgb = (a, i) => {
|
|
38
|
+
const c = a.slice(i * 4, i * 4 + 4);
|
|
39
|
+
check(c.length === 4 && c.every((v) => Number.isInteger(v) && v >= 0 && v <= 255) && c[3] === 255, "RGBA");
|
|
40
|
+
return c
|
|
41
|
+
.slice(0, 3)
|
|
42
|
+
.map((v) => v.toString(16).padStart(2, "0"))
|
|
43
|
+
.join("");
|
|
44
|
+
};
|
|
45
|
+
const continuation = (n) => n === 0 || n >>> 30 === 3;
|
|
46
|
+
const cells = lines.map((line, y) => {
|
|
47
|
+
const text = [...segmenter.segment(line)].map((s) => s.segment);
|
|
48
|
+
let pos = 0;
|
|
49
|
+
const row = Array.from({ length: raw.cols }, (_, x) => {
|
|
50
|
+
const i = y * raw.cols + x, cp = raw.char[i];
|
|
51
|
+
const cont = continuation(cp) && x > 0 && !continuation(raw.char[i - 1]) && raw.char[i - 1] !== 32;
|
|
52
|
+
let glyph = "";
|
|
53
|
+
if (cont) {
|
|
54
|
+
if (cp === 0) {
|
|
55
|
+
check(text[pos] === " ", "direct continuation text");
|
|
56
|
+
pos++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
glyph = text[pos++] ?? "";
|
|
61
|
+
check(/^(?:[\x20-\x7e]|界|é)$/u.test(glyph), "unsupported grapheme");
|
|
62
|
+
}
|
|
63
|
+
const width = cont ? 0 : x < raw.cols - 1 && continuation(raw.char[i + 1]) ? 2 : 1;
|
|
64
|
+
const kind = cp >>> 30;
|
|
65
|
+
if (cont) {
|
|
66
|
+
const prev = raw.char[i - 1];
|
|
67
|
+
check(prev === 0x754c || (prev >>> 30 === 2 && ((prev >>> 28) & 3) === 1), "continuation owner");
|
|
68
|
+
if (kind === 3)
|
|
69
|
+
check(((cp >>> 26) & 3) === 1 &&
|
|
70
|
+
((cp >>> 28) & 3) === 0 &&
|
|
71
|
+
(cp & 0x03ffffff) === (prev & 0x03ffffff), "interned continuation");
|
|
72
|
+
}
|
|
73
|
+
else if (kind === 0) {
|
|
74
|
+
check(cp !== 0 && glyph.codePointAt(0) === cp && [...glyph].length === 1, "direct codepoint mismatch");
|
|
75
|
+
check(width === (glyph === "界" ? 2 : 1), "direct width");
|
|
76
|
+
}
|
|
77
|
+
else if (kind === 2) {
|
|
78
|
+
check(glyph === "é" && ((cp >>> 26) & 3) === 0 && ((cp >>> 28) & 3) === 0 && width === 1, "interned grapheme shape");
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
check(false, "unsupported raw char flags");
|
|
82
|
+
const visible = glyph !== " " && glyph !== "";
|
|
83
|
+
check((raw.attributes[i] & ~1) === 0, "unsupported attributes");
|
|
84
|
+
return {
|
|
85
|
+
text: visible ? glyph : "",
|
|
86
|
+
width,
|
|
87
|
+
fg: visible ? rgb(raw.fg, i) : "ffffff",
|
|
88
|
+
bg: rgb(raw.bg, i),
|
|
89
|
+
bold: visible && (raw.attributes[i] & 1) !== 0,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
check(pos === text.length, "unconsumed text");
|
|
93
|
+
return row;
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
cells,
|
|
97
|
+
cursor: { x: raw.cursor.x - 1, y: raw.cursor.y - 1, visible: raw.cursor.visible },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function compareVisual(actual, expected) {
|
|
101
|
+
check(JSON.stringify(actual.cursor) === JSON.stringify(expected.cursor), "cursor mismatch");
|
|
102
|
+
check(actual.cells.length === expected.cells.length, "rows mismatch");
|
|
103
|
+
actual.cells.forEach((row, y) => {
|
|
104
|
+
check(row.length === expected.cells[y].length, "cols mismatch");
|
|
105
|
+
row.forEach((cell, x) => {
|
|
106
|
+
for (const key of ["text", "width", "fg", "bg", "bold"])
|
|
107
|
+
check(cell[key] === expected.cells[y][x][key], `${y}:${x}.${key} mismatch`);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export function nativeVisualFrame(frame) {
|
|
112
|
+
check((frame.cols === 24 || frame.cols === 40) && frame.rows === 8, "native geometry");
|
|
113
|
+
const color = (value, defaultValue) => {
|
|
114
|
+
if (value === "default")
|
|
115
|
+
return defaultValue;
|
|
116
|
+
check(/^rgb:[0-9a-f]{6}$/u.test(value), "native color vocabulary");
|
|
117
|
+
return value.slice(4);
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
cells: frame.cells.map((row) => row.map((c) => {
|
|
121
|
+
const visible = c.text !== "" && c.text !== " ";
|
|
122
|
+
check(c.attributes.every((a) => a === "bold"), "native attribute vocabulary");
|
|
123
|
+
return {
|
|
124
|
+
text: visible ? c.text : "",
|
|
125
|
+
width: c.width,
|
|
126
|
+
fg: visible ? color(c.foreground, "ffffff") : "ffffff",
|
|
127
|
+
bg: color(c.background, "000000"),
|
|
128
|
+
bold: visible && c.attributes.includes("bold"),
|
|
129
|
+
};
|
|
130
|
+
})),
|
|
131
|
+
cursor: { x: frame.cursor[0], y: frame.cursor[1], visible: true },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Declared workspace chrome: one tab row plus one title row, then eight native rows. */
|
|
135
|
+
export function cropWorkspaceCompletedFrame(raw) {
|
|
136
|
+
check((raw.cols === 24 || raw.cols === 40) && raw.rows === 10, "workspace geometry");
|
|
137
|
+
const size = raw.cols * raw.rows;
|
|
138
|
+
check(raw.char.length === size &&
|
|
139
|
+
raw.attributes.length === size &&
|
|
140
|
+
raw.fg.length === size * 4 &&
|
|
141
|
+
raw.bg.length === size * 4, "workspace buffer lengths");
|
|
142
|
+
const lines = raw.text.replace(/\n$/u, "").split("\n");
|
|
143
|
+
check(lines.length === 10, "workspace line count");
|
|
144
|
+
check(raw.cursor.y >= 3 && raw.cursor.y <= 10, "workspace cursor outside content");
|
|
145
|
+
const offset = 2 * raw.cols;
|
|
146
|
+
return {
|
|
147
|
+
cols: raw.cols,
|
|
148
|
+
rows: 8,
|
|
149
|
+
char: raw.char.slice(offset),
|
|
150
|
+
attributes: raw.attributes.slice(offset),
|
|
151
|
+
fg: raw.fg.slice(offset * 4),
|
|
152
|
+
bg: raw.bg.slice(offset * 4),
|
|
153
|
+
text: lines.slice(2).join("\n"),
|
|
154
|
+
cursor: { ...raw.cursor, y: raw.cursor.y - 2 },
|
|
155
|
+
};
|
|
156
|
+
}
|