tmux-ide 2.9.3 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +120 -139
  3. package/bin/cli.js +4847 -3585
  4. package/package.json +16 -9
  5. package/packages/contracts/src/session-runtime.ts +2 -0
  6. package/packages/contracts/src/terminal-delivery.ts +4 -0
  7. package/packages/core/src/terminal-compact-capability.ts +31 -1
  8. package/packages/core/src/terminal-delivery.ts +234 -32
  9. package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
  10. package/packages/core/src/terminal-replica-owned-row.ts +165 -10
  11. package/packages/core/src/terminal-replica.ts +43 -21
  12. package/packages/daemon/dist/lib/daemon-embed.js +19 -34
  13. package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
  14. package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
  15. package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
  16. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  18. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  20. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
  21. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  22. package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
  23. package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
  24. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
  25. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  26. package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
  27. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
  28. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  29. package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
  30. package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
  31. package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
  32. package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
  33. package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
  34. package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
  35. package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
  36. package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
  37. package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
  38. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
  39. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
  40. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
  41. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
  42. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  43. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
  44. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
  45. package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
  46. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
  47. package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
  49. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
  50. package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
  51. package/packages/daemon/src/lib/daemon-embed.ts +22 -34
  52. package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
  53. package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
  54. package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
  55. package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
  56. package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
  57. package/packages/daemon/src/terminal/mirror/events.ts +7 -7
  58. package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
  59. package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
  60. package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
  61. package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
  62. package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
  63. package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
  64. package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
  65. package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
  66. package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
  67. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
  68. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
  69. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
  70. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
  71. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  72. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
  73. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
  74. package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
  75. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
  76. package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
  77. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
  79. package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
  80. package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
  81. package/packages/daemon-client/src/workspace-client-types.ts +1 -0
  82. package/packages/daemon-client/src/workspace-client.ts +78 -5
  83. package/packages/tmux-bridge/src/index.ts +1 -0
  84. package/packages/tmux-bridge/src/runner.ts +25 -1
  85. package/skill/SKILL.md +30 -30
@@ -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(): { readonly pendingInputs: number; readonly droppedInputs: number };
69
- close(): { readonly pendingInputs: number; readonly droppedInputs: number };
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
- typeof value[key] === "string" ? value[key].slice(0, 64) : null;
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 snapshot = () => Object.freeze({ pendingInputs: inputs.size, droppedInputs });
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
- const startedAtMicros = nowMicros();
582
- expireInputs(inputs, startedAtMicros);
583
- // The transport carries one latest-only performance trace id. Once a
584
- // newer input is admitted, an older probe can no longer be attributed to
585
- // the exact changed-cell paint without lying. Retire it as superseded;
586
- // `droppedInputs` is reserved for actual capacity loss.
587
- inputs.clear();
588
- while (inputs.size >= MAX_PENDING_INPUTS) {
589
- inputs.delete(inputs.keys().next().value!);
590
- droppedInputs += 1;
591
- }
592
- const traceId = createTraceId();
593
- if (
594
- (detailed || options.inputOrigin) &&
595
- origin &&
596
- !closed &&
597
- typeof options.inputFingerprintKey === "string" &&
598
- options.inputFingerprintKey.length >= 32
599
- ) {
600
- const payload = Buffer.from(origin.payload);
601
- const event: TuiTerminalInputOriginEvent = {
602
- processId,
603
- clockId: "opentui-performance-now",
604
- clockKind: "performance-now",
605
- atMicros:
606
- Number.isSafeInteger(origin.ingressAtMicros) &&
607
- origin.ingressAtMicros! <= startedAtMicros
608
- ? origin.ingressAtMicros!
609
- : startedAtMicros,
610
- origin: origin.origin,
611
- payloadByteCount: payload.byteLength,
612
- payloadFingerprint: createHmac("sha256", options.inputFingerprintKey)
613
- .update(traceId)
614
- .update("\0")
615
- .update(payload)
616
- .digest("hex"),
617
- parserConsumption:
618
- origin.origin === "keyboard"
619
- ? "keyboard-event"
620
- : origin.origin === "bracketed-paste"
621
- ? "paste-event"
622
- : "pointer-event",
623
- ...(origin.gestureId ? { gestureId: origin.gestureId } : {}),
624
- ...(origin.pointerAction ? { pointerAction: origin.pointerAction } : {}),
625
- ...(Number.isSafeInteger(origin.pointerColumn)
626
- ? { pointerColumn: origin.pointerColumn }
627
- : {}),
628
- ...(Number.isSafeInteger(origin.pointerRow) ? { pointerRow: origin.pointerRow } : {}),
629
- ...(origin.pointerButton === null || Number.isSafeInteger(origin.pointerButton)
630
- ? { pointerButton: origin.pointerButton }
631
- : {}),
632
- traceId,
633
- semanticPaneId: origin.semanticPaneId,
634
- generation: origin.generation,
635
- incarnation: origin.incarnation,
636
- revision: origin.revision,
637
- stateHash: origin.stateHash,
638
- };
639
- options.append({ version: 1, type: "performance.input-origin", ...event });
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: () => inputs.delete(traceId),
687
+ cancel: () => {
688
+ if (inputs.delete(traceId)) attempts.cancelled += 1;
689
+ },
656
690
  });
657
691
  },
658
- terminalTraceSpan: (paint: TuiTerminalTraceSpanEvent) =>
659
- recordCompletedTrace(inputs, paint, options.append),
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
- ): void {
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
- ): void {
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
- setNote("terminal input queue full · wait for the session to connect");
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
- setNote(null);
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 { createRenderEffect, onCleanup, untrack, type Accessor } from "solid-js";
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
- <pane_surface
126
- ref={(renderable: PaneSurfaceRenderable) => {
127
- surface = renderable;
128
- renderable.contentVersion = props.adapter.paneVersion(props.paneId);
129
- renderable.presentationVersion = props.adapter.panePresentationVersion?.(props.paneId) ?? 0;
130
- renderable.sourceEpoch = props.sourceEpoch + props.adapter.paneSourceEpoch();
131
- renderable.presentationGeneration = currentPresentationGeneration();
132
- ensureRetainedCanonicalState(props.adapter, props.paneId);
133
- }}
134
- width={props.width}
135
- height={props.height}
136
- mirror={props.adapter.renderSource}
137
- paneId={props.paneId}
138
- defaultFg={props.defaultFg}
139
- defaultBg={props.defaultBg}
140
- terminalPalette={props.terminalPalette}
141
- searchHl={props.searchHl}
142
- searchCur={props.searchCur}
143
- scrollOffset={props.scrollOffset}
144
- viewportOrigin={props.viewportOrigin}
145
- paneFocused={props.paneFocused}
146
- contentVersion={props.adapter.paneVersion(props.paneId)}
147
- presentationVersion={props.adapter.panePresentationVersion?.(props.paneId) ?? 0}
148
- sourceEpoch={props.sourceEpoch + props.adapter.paneSourceEpoch()}
149
- rendererEpoch={props.sourceEpoch}
150
- hostFocusTransitionOwner={props.hostFocusTransitionOwner}
151
- selRange={props.selRange}
152
- copyCursor={props.copyCursor}
153
- search={props.search}
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(paneId, (publication) => {
436
- this.#publish(interest, publication);
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 offset = 0;
58
- offset < framed.length;
59
- offset += SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS
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, offset + SESSION_RUNTIME_MAX_TERMINAL_INPUT_TEXT_CHARS),
66
+ data: framed.slice(offset, end),
65
67
  }),
66
68
  );
69
+ offset = end;
67
70
  }
68
71
  return Object.freeze(inputs);
69
72
  }