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.
Files changed (80) hide show
  1. package/bin/cli.js +4354 -3376
  2. package/package.json +7 -6
  3. package/packages/contracts/src/session-runtime.ts +2 -0
  4. package/packages/contracts/src/terminal-delivery.ts +4 -0
  5. package/packages/core/src/terminal-compact-capability.ts +31 -1
  6. package/packages/core/src/terminal-delivery.ts +234 -32
  7. package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
  8. package/packages/core/src/terminal-replica-owned-row.ts +165 -10
  9. package/packages/core/src/terminal-replica.ts +43 -21
  10. package/packages/daemon/dist/lib/daemon-embed.js +19 -34
  11. package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
  12. package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
  13. package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
  14. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  15. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  16. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  18. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
  19. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  20. package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
  21. package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
  22. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
  23. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  24. package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
  25. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
  26. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  27. package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
  28. package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
  29. package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
  30. package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
  31. package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
  32. package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
  33. package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
  34. package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
  35. package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
  36. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
  37. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
  38. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
  39. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
  40. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
  41. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
  42. package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
  43. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
  44. package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
  45. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
  46. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
  47. package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
  48. package/packages/daemon/src/lib/daemon-embed.ts +22 -34
  49. package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
  50. package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
  51. package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
  52. package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
  53. package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
  54. package/packages/daemon/src/terminal/mirror/events.ts +7 -7
  55. package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
  56. package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
  57. package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
  58. package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
  59. package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
  60. package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
  61. package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
  62. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
  63. package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
  64. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
  65. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
  66. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
  67. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
  68. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
  69. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
  70. package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
  71. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
  72. package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
  73. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
  74. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
  75. package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
  76. package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
  77. package/packages/daemon-client/src/workspace-client-types.ts +1 -0
  78. package/packages/daemon-client/src/workspace-client.ts +78 -5
  79. package/packages/tmux-bridge/src/index.ts +1 -0
  80. 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) => typeof value[key] === "string" ? value[key].slice(0, 64) : null;
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 snapshot = () => Object.freeze({ pendingInputs: inputs.size, droppedInputs });
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
- const startedAtMicros = nowMicros();
468
- expireInputs(inputs, startedAtMicros);
469
- // The transport carries one latest-only performance trace id. Once a
470
- // newer input is admitted, an older probe can no longer be attributed to
471
- // the exact changed-cell paint without lying. Retire it as superseded;
472
- // `droppedInputs` is reserved for actual capacity loss.
473
- inputs.clear();
474
- while (inputs.size >= MAX_PENDING_INPUTS) {
475
- inputs.delete(inputs.keys().next().value);
476
- droppedInputs += 1;
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
- const traceId = createTraceId();
479
- if ((detailed || options.inputOrigin) &&
480
- origin &&
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: () => inputs.delete(traceId),
564
+ cancel: () => {
565
+ if (inputs.delete(traceId))
566
+ attempts.cancelled += 1;
567
+ },
540
568
  });
541
569
  },
542
- terminalTraceSpan: (paint) => recordCompletedTrace(inputs, paint, options.append),
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
- setNote("terminal input queue full · wait for the session to connect");
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
- setNote(`${target.session} · terminal ${kind} queued`);
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
- setNote(null);
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 (<pane_surface ref={(renderable) => {
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 = props.adapter.panePresentationVersion?.(props.paneId) ?? 0;
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; offset += SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS) {
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, offset + SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS),
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
+ }