tmux-ide 2.9.2 → 2.9.4

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 (267) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +121 -140
  3. package/bin/cli.js +2457 -727
  4. package/package.json +23 -8
  5. package/packages/contracts/src/desktop-host.ts +3 -0
  6. package/packages/contracts/src/saved-machines.ts +19 -0
  7. package/packages/contracts/src/terminal-replica.ts +1 -1
  8. package/packages/core/package.json +3 -2
  9. package/packages/core/src/terminal-delivery.ts +51 -3
  10. package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
  11. package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
  12. package/packages/core/src/terminal-replica-owned-row.ts +27 -0
  13. package/packages/core/src/terminal-replica.ts +13 -28
  14. package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
  15. package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
  16. package/packages/daemon/dist/command-center/ws-events.js +5 -24
  17. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
  18. package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
  19. package/packages/daemon/dist/lib/daemon-embed.js +1 -1
  20. package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
  21. package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
  22. package/packages/daemon/dist/lib/daemon-service.js +259 -0
  23. package/packages/daemon/dist/lib/install-origin.js +2 -0
  24. package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
  25. package/packages/daemon/dist/lib/log.js +6 -10
  26. package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
  27. package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
  28. package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
  29. package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
  30. package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
  31. package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
  32. package/packages/daemon/dist/lib/saved-machines.js +40 -2
  33. package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
  34. package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
  35. package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
  36. package/packages/daemon/dist/lib/tui-binary.js +2 -2
  37. package/packages/daemon/dist/lib/update.js +18 -0
  38. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
  39. package/packages/daemon/dist/machines.js +40 -4
  40. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  41. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  42. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  43. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  44. package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
  45. package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
  46. package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
  47. package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
  48. package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
  49. package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
  50. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
  51. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
  52. package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
  53. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  54. package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
  55. package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
  56. package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
  57. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
  58. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
  59. package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
  60. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
  61. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
  62. package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
  63. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
  64. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
  65. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
  66. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
  67. package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
  68. package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
  69. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
  70. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
  71. package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
  72. package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
  73. package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
  74. package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
  75. package/packages/daemon/src/command-center/ws-events.ts +5 -25
  76. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
  78. package/packages/daemon/src/lib/daemon-embed.ts +1 -0
  79. package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
  80. package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
  81. package/packages/daemon/src/lib/daemon-service.ts +301 -0
  82. package/packages/daemon/src/lib/install-origin.ts +5 -0
  83. package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
  84. package/packages/daemon/src/lib/log.ts +13 -9
  85. package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
  86. package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
  87. package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
  88. package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
  89. package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
  90. package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
  91. package/packages/daemon/src/lib/saved-machines.ts +40 -1
  92. package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
  93. package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
  94. package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
  95. package/packages/daemon/src/lib/tui-binary.ts +2 -2
  96. package/packages/daemon/src/lib/update.ts +24 -0
  97. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
  98. package/packages/daemon/src/machines.ts +53 -5
  99. package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
  100. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  101. package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
  102. package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
  103. package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
  104. package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
  105. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
  106. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
  107. package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
  108. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
  109. package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
  110. package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
  111. package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
  112. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
  113. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
  114. package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
  115. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
  116. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
  117. package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
  118. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
  119. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
  120. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
  121. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
  122. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
  123. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
  124. package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
  125. package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
  126. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
  127. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
  128. package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
  129. package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
  130. package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
  131. package/packages/daemon-client/src/workspace-client.ts +4 -1
  132. package/skill/SKILL.md +30 -30
  133. package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
  134. package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
  135. package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
  136. package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
  137. package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
  138. package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
  139. package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
  140. package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
  141. package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
  142. package/packages/contracts/src/__tests__/commands.test.ts +0 -86
  143. package/packages/contracts/src/__tests__/control.test.ts +0 -154
  144. package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
  145. package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
  146. package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
  147. package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
  148. package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
  149. package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
  150. package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
  151. package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
  152. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
  153. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
  154. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
  155. package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
  156. package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
  157. package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
  158. package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
  159. package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
  160. package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
  161. package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
  162. package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
  163. package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
  164. package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
  165. package/packages/contracts/src/__tests__/missions.test.ts +0 -272
  166. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
  167. package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
  168. package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
  169. package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
  170. package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
  171. package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
  172. package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
  173. package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
  174. package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
  175. package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
  176. package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
  177. package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
  178. package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
  179. package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
  180. package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
  181. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
  182. package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
  183. package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
  184. package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
  185. package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
  186. package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
  187. package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
  188. package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
  189. package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
  190. package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
  191. package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
  192. package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
  193. package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
  194. package/packages/contracts/src/native-operation-identity.test.ts +0 -23
  195. package/packages/contracts/src/workspace-admission.test.ts +0 -21
  196. package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
  197. package/packages/core/src/agent-provisioning.test.ts +0 -42
  198. package/packages/core/src/application-shell-session.test.ts +0 -105
  199. package/packages/core/src/interaction-evidence.test.ts +0 -134
  200. package/packages/core/src/interaction-flow.test.ts +0 -66
  201. package/packages/core/src/interaction-receipts.test.ts +0 -671
  202. package/packages/core/src/navigator.test.ts +0 -34
  203. package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
  204. package/packages/core/src/optimistic-projection.test.ts +0 -118
  205. package/packages/core/src/performance-metrics.test.ts +0 -121
  206. package/packages/core/src/performance-qualification.test.ts +0 -283
  207. package/packages/core/src/saved-machines.test.ts +0 -80
  208. package/packages/core/src/terminal-compact-cold.test.ts +0 -87
  209. package/packages/core/src/terminal-conformance.test.ts +0 -48
  210. package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
  211. package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
  212. package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
  213. package/packages/core/src/terminal-delivery.test.ts +0 -1471
  214. package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
  215. package/packages/core/src/terminal-replica-cold.test.ts +0 -31
  216. package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
  217. package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
  218. package/packages/core/src/terminal-replica.test.ts +0 -347
  219. package/packages/core/src/workspace-selection.test.ts +0 -63
  220. package/packages/daemon/dist/__tests__/support.js +0 -33
  221. package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
  222. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
  223. package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
  224. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
  225. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
  226. package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
  227. package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
  228. package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
  229. package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
  230. package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
  231. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
  232. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
  233. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
  234. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
  235. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
  236. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
  237. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
  238. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
  239. package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
  240. package/packages/daemon-client/src/automation-client.test.ts +0 -214
  241. package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
  242. package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
  243. package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
  244. package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
  245. package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
  246. package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
  247. package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
  248. package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
  249. package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
  250. package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
  251. package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
  252. package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
  253. package/packages/daemon-client/src/resource-replica.test.ts +0 -186
  254. package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
  255. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
  256. package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
  257. package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
  258. package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
  259. package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
  260. package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
  261. package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
  262. package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
  263. package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
  264. package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
  265. package/packages/presentation/src/presentation-contract.test.ts +0 -63
  266. package/packages/tmux-bridge/src/runner.test.ts +0 -614
  267. package/packages/tmux-bridge/src/targeting.test.ts +0 -107
@@ -1,835 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import type {
3
- CanonicalTerminalReplicaPatch,
4
- CanonicalTerminalReplicaSeed,
5
- CanonicalTerminalReplicaUpdate,
6
- SessionRuntimeAuthorityKind,
7
- SessionRuntimeTerminalInput,
8
- TerminalReplicaDeliveryMetadata,
9
- TerminalReplicaPatchPayload,
10
- TerminalReplicaSnapshot,
11
- } from "@tmux-ide/contracts";
12
- import {
13
- applyTerminalReplicaPatch,
14
- blankTerminalReplicaSnapshot,
15
- hashTerminalReplicaSnapshot,
16
- } from "@tmux-ide/core";
17
-
18
- import {
19
- createTerminalFastLane,
20
- createWorkspaceClientTerminalSource,
21
- type TerminalFastLaneControlPort,
22
- type TerminalFastLaneGenerationAddress,
23
- type TerminalFastLanePaneAddress,
24
- type TerminalFastLaneSourcePort,
25
- type TerminalFastLaneViewport,
26
- } from "./terminal-fast-lane.ts";
27
-
28
- const GENERATION_A = "11111111-1111-4111-8111-111111111111";
29
- const GENERATION_B = "22222222-2222-4222-8222-222222222222";
30
- const WORKSPACE = "workspace";
31
-
32
- function address(generation = GENERATION_A): TerminalFastLaneGenerationAddress {
33
- return { workspaceName: WORKSPACE, generation };
34
- }
35
-
36
- function seed(
37
- snapshot: TerminalReplicaSnapshot,
38
- revision = 0,
39
- generation = GENERATION_A,
40
- semanticPaneId = "pane-a",
41
- incarnation = `${generation}:0`,
42
- ): CanonicalTerminalReplicaSeed {
43
- return {
44
- type: "terminal.seed",
45
- workspaceName: WORKSPACE,
46
- semanticPaneId,
47
- generation,
48
- incarnation,
49
- revision,
50
- cols: snapshot.cols,
51
- rows: snapshot.rows,
52
- stateHash: hashTerminalReplicaSnapshot(snapshot),
53
- hashAlgorithm: "fnv1a64-v1",
54
- snapshot,
55
- };
56
- }
57
-
58
- function patch(
59
- current: TerminalReplicaSnapshot,
60
- payload: TerminalReplicaPatchPayload,
61
- revision: number,
62
- generation = GENERATION_A,
63
- semanticPaneId = "pane-a",
64
- incarnation = `${generation}:0`,
65
- ): CanonicalTerminalReplicaPatch {
66
- const next = applyTerminalReplicaPatch(current, payload);
67
- return {
68
- type: "terminal.patch",
69
- workspaceName: WORKSPACE,
70
- semanticPaneId,
71
- generation,
72
- incarnation,
73
- baseRevision: revision - 1,
74
- revision,
75
- cols: next.cols,
76
- rows: next.rows,
77
- stateHash: hashTerminalReplicaSnapshot(next),
78
- hashAlgorithm: "fnv1a64-v1",
79
- patch: payload,
80
- };
81
- }
82
-
83
- function deferred<T>() {
84
- let resolve!: (value: T) => void;
85
- let reject!: (error: unknown) => void;
86
- const promise = new Promise<T>((accept, deny) => {
87
- resolve = accept;
88
- reject = deny;
89
- });
90
- return { promise, resolve, reject };
91
- }
92
-
93
- async function settle(): Promise<void> {
94
- await Promise.resolve();
95
- await Promise.resolve();
96
- await Promise.resolve();
97
- }
98
-
99
- class FakeSource implements TerminalFastLaneSourcePort {
100
- readonly listeners = new Map<
101
- string,
102
- Set<
103
- (update: CanonicalTerminalReplicaUpdate, metadata?: TerminalReplicaDeliveryMetadata) => void
104
- >
105
- >();
106
- subscribe(
107
- pane: TerminalFastLanePaneAddress,
108
- listener: (update: CanonicalTerminalReplicaUpdate) => void,
109
- ): () => void {
110
- const key = `${pane.generation}:${pane.semanticPaneId}`;
111
- const listeners = this.listeners.get(key) ?? new Set();
112
- listeners.add(listener);
113
- this.listeners.set(key, listeners);
114
- return () => listeners.delete(listener);
115
- }
116
- emit(
117
- update: CanonicalTerminalReplicaUpdate,
118
- generation = update.generation,
119
- pane = update.semanticPaneId,
120
- metadata?: TerminalReplicaDeliveryMetadata,
121
- ): void {
122
- for (const listener of [...(this.listeners.get(`${generation}:${pane}`) ?? [])])
123
- listener(update, metadata);
124
- }
125
- capture(generation: string, pane: string): (update: CanonicalTerminalReplicaUpdate) => void {
126
- return [...(this.listeners.get(`${generation}:${pane}`) ?? [])][0]!;
127
- }
128
- }
129
-
130
- class FakeControl implements TerminalFastLaneControlPort {
131
- readonly owned = new Set<SessionRuntimeAuthorityKind>();
132
- readonly writes: Array<{
133
- address: TerminalFastLanePaneAddress;
134
- input: SessionRuntimeTerminalInput;
135
- performanceTraceId?: string;
136
- }> = [];
137
- readonly resizes: Array<{
138
- address: TerminalFastLaneGenerationAddress;
139
- viewport: TerminalFastLaneViewport;
140
- }> = [];
141
- readonly requests: SessionRuntimeAuthorityKind[] = [];
142
- requestResult = true;
143
- writeResult: "ok" | "authority-lost" = "ok";
144
- writeResults: Array<"ok" | "authority-lost"> = [];
145
- resizeResult: "ok" | "authority-lost" | "geometry-authority-conflict" = "ok";
146
- writeGate: Promise<void> | null = null;
147
- writeGates: Promise<void>[] = [];
148
- resizeGates: Promise<void>[] = [];
149
-
150
- owns(authority: SessionRuntimeAuthorityKind): boolean {
151
- return this.owned.has(authority);
152
- }
153
- async request(authority: SessionRuntimeAuthorityKind): Promise<boolean> {
154
- this.requests.push(authority);
155
- if (this.requestResult) this.owned.add(authority);
156
- return this.requestResult;
157
- }
158
- async write(
159
- address: TerminalFastLanePaneAddress,
160
- input: SessionRuntimeTerminalInput,
161
- performanceTraceId?: string,
162
- ) {
163
- const result = this.writeResults.shift() ?? this.writeResult;
164
- this.writes.push({
165
- address,
166
- input: { ...input },
167
- ...(performanceTraceId ? { performanceTraceId } : {}),
168
- });
169
- const gate = this.writeGates.shift() ?? this.writeGate;
170
- if (gate) await gate;
171
- return result;
172
- }
173
- async resize(address: TerminalFastLaneGenerationAddress, viewport: TerminalFastLaneViewport) {
174
- const gate = this.resizeGates.shift();
175
- this.resizes.push({ address, viewport });
176
- if (gate) await gate;
177
- return this.resizeResult;
178
- }
179
- }
180
-
181
- function rig(
182
- overrides: {
183
- maxPendingInputs?: number;
184
- maxPendingInputBytes?: number;
185
- maxInFlightInputs?: number;
186
- } = {},
187
- ) {
188
- const source = new FakeSource();
189
- const control = new FakeControl();
190
- const repairs: unknown[] = [];
191
- const lane = createTerminalFastLane({
192
- address: address(),
193
- source,
194
- control,
195
- repair: { request: (request) => repairs.push(request) },
196
- ...overrides,
197
- });
198
- return { lane, source, control, repairs };
199
- }
200
-
201
- describe("terminal fast lane", () => {
202
- it("separates slow or throwing observers from exact canonical apply timing", () => {
203
- const source = new FakeSource();
204
- const control = new FakeControl();
205
- let nowMicros = 1_000;
206
- const stages: Array<{
207
- operation: string;
208
- atMicros: number;
209
- semanticPaneId?: string;
210
- generation?: string;
211
- incarnation?: string;
212
- revision?: number;
213
- stateHash?: string;
214
- }> = [];
215
- const lane = createTerminalFastLane({
216
- address: address(),
217
- source,
218
- control,
219
- repair: { request: () => undefined },
220
- diagnosticNowMicros: () => nowMicros,
221
- onTraceStage: (stage) => {
222
- stages.push(stage);
223
- if (stage.operation === "delivery-received") nowMicros += 40_000;
224
- if (stage.operation === "canonical-apply-begin") throw new Error("observer failed");
225
- },
226
- });
227
- const snapshot = blankTerminalReplicaSnapshot(132, 41);
228
- let publications = 0;
229
- lane.subscribePane("pane-a", () => (publications += 1));
230
- source.emit(seed(snapshot, 0));
231
- const changedRow = {
232
- ...snapshot.grid[0]!,
233
- cells: [
234
- { ...snapshot.grid[0]!.cells[0]!, grapheme: "x" },
235
- ...snapshot.grid[0]!.cells.slice(1),
236
- ],
237
- };
238
- const update = patch(snapshot, { rows: [{ index: 0, row: changedRow }] }, 1);
239
-
240
- expect(() =>
241
- source.emit(update, update.generation, update.semanticPaneId, {
242
- performanceTraceId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
243
- }),
244
- ).not.toThrow();
245
- expect(publications).toBe(2);
246
- expect(stages.map(({ operation }) => operation)).toEqual([
247
- "delivery-received",
248
- "delivery-observer-returned",
249
- "canonical-apply-begin",
250
- "canonical-apply-end",
251
- "lane-published",
252
- ]);
253
- expect(stages.find(({ operation }) => operation === "delivery-received")?.atMicros).toBe(1_000);
254
- expect(
255
- stages.find(({ operation }) => operation === "delivery-observer-returned")?.atMicros,
256
- ).toBe(41_000);
257
- expect(stages.find(({ operation }) => operation === "canonical-apply-begin")?.atMicros).toBe(
258
- 41_000,
259
- );
260
- expect(stages.find(({ operation }) => operation === "canonical-apply-end")?.atMicros).toBe(
261
- 41_000,
262
- );
263
- for (const stage of stages) {
264
- expect(stage).toMatchObject({
265
- semanticPaneId: "pane-a",
266
- generation: GENERATION_A,
267
- incarnation: `${GENERATION_A}:0`,
268
- revision: 1,
269
- stateHash: update.stateHash,
270
- });
271
- }
272
- });
273
-
274
- it("does no diagnostic clock work when canonical tracing is disabled", () => {
275
- const source = new FakeSource();
276
- const lane = createTerminalFastLane({
277
- address: address(),
278
- source,
279
- control: new FakeControl(),
280
- repair: { request: () => undefined },
281
- diagnosticNowMicros: () => {
282
- throw new Error("disabled clock must not run");
283
- },
284
- });
285
- lane.subscribePane("pane-a", () => undefined);
286
- const update = seed(blankTerminalReplicaSnapshot(2, 1));
287
- expect(() =>
288
- source.emit(update, update.generation, update.semanticPaneId, {
289
- performanceTraceId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
290
- }),
291
- ).not.toThrow();
292
- expect(lane.paneState("pane-a")?.revision).toBe(0);
293
- });
294
-
295
- it("keeps canonical apply live when the enabled diagnostic clock throws", () => {
296
- const source = new FakeSource();
297
- const lane = createTerminalFastLane({
298
- address: address(),
299
- source,
300
- control: new FakeControl(),
301
- repair: { request: () => undefined },
302
- onTraceStage: () => undefined,
303
- diagnosticNowMicros: () => {
304
- throw new Error("diagnostic clock failed");
305
- },
306
- });
307
- lane.subscribePane("pane-a", () => undefined);
308
- const update = seed(blankTerminalReplicaSnapshot(2, 1));
309
- expect(() =>
310
- source.emit(update, update.generation, update.semanticPaneId, {
311
- performanceTraceId: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
312
- }),
313
- ).not.toThrow();
314
- expect(lane.paneState("pane-a")?.revision).toBe(0);
315
- });
316
-
317
- it("publishes ordered pane-local state once and coalesces repair until a valid seed", () => {
318
- const { lane, source, repairs } = rig();
319
- const snapshot = blankTerminalReplicaSnapshot(4, 2);
320
- const publications: number[] = [];
321
- let isolatedListenerCalls = 0;
322
- lane.subscribePane("pane-a", (publication) => publications.push(publication.state.revision));
323
- lane.subscribePane("pane-a", () => {
324
- isolatedListenerCalls += 1;
325
- throw new Error("observer failure");
326
- });
327
- let siblingCalls = 0;
328
- lane.subscribePane("pane-b", () => (siblingCalls += 1));
329
-
330
- const initial = seed(snapshot);
331
- source.emit(initial);
332
- source.emit(initial);
333
- source.emit(patch(snapshot, { rows: [] }, 2));
334
- source.emit(patch(snapshot, { rows: [] }, 3));
335
- source.emit(
336
- { ...patch(snapshot, { rows: [] }, 1), semanticPaneId: "pane-b" },
337
- GENERATION_A,
338
- "pane-a",
339
- );
340
- expect(publications).toEqual([0]);
341
- expect(isolatedListenerCalls).toBe(1);
342
- expect(siblingCalls).toBe(0);
343
- expect(repairs).toHaveLength(1);
344
-
345
- source.emit(seed(snapshot, 2, GENERATION_A, "pane-a", `${GENERATION_A}:1`));
346
- source.emit(patch(snapshot, { rows: [] }, 4, GENERATION_A, "pane-a", `${GENERATION_A}:1`));
347
- expect(publications).toEqual([0, 2]);
348
- expect(repairs).toHaveLength(2);
349
- expect(lane.counters()).toMatchObject({
350
- published: 2,
351
- duplicateOrStale: 1,
352
- repairs: 2,
353
- });
354
- });
355
-
356
- it("coalesces an explicit missing-state repair until a canonical seed arrives", () => {
357
- const { lane, source, repairs } = rig();
358
- lane.subscribePane("pane-a", () => undefined);
359
-
360
- expect(lane.requestRepair("pane-a", "missing-state")).toBe(true);
361
- expect(lane.requestRepair("pane-a", "missing-state")).toBe(true);
362
- expect(repairs).toHaveLength(1);
363
- expect(repairs[0]).toMatchObject({
364
- reason: "missing-state",
365
- expectedRevision: 0,
366
- receivedRevision: 0,
367
- });
368
-
369
- source.emit(seed(blankTerminalReplicaSnapshot(4, 2)));
370
- expect(lane.requestRepair("pane-a", "missing-state")).toBe(true);
371
- expect(repairs).toHaveLength(2);
372
- expect(lane.counters()).toMatchObject({ repairs: 2 });
373
- });
374
-
375
- it("routes 10k updates to exactly one pane and never asks for semantic publication", () => {
376
- const listeners = new Map<string, (update: CanonicalTerminalReplicaUpdate) => void>();
377
- let semanticCallbacks = 0;
378
- const client = {
379
- subscribe: () => {
380
- semanticCallbacks += 1;
381
- return () => undefined;
382
- },
383
- subscribeTerminal: (
384
- target: { workspaceName: string; semanticPaneId: string },
385
- listener: (update: CanonicalTerminalReplicaUpdate) => void,
386
- ) => {
387
- listeners.set(target.semanticPaneId, listener);
388
- return () => listeners.delete(target.semanticPaneId);
389
- },
390
- };
391
- const control = new FakeControl();
392
- const lane = createTerminalFastLane({
393
- address: address(),
394
- source: createWorkspaceClientTerminalSource(client),
395
- control,
396
- repair: { request: () => undefined },
397
- });
398
- const snapshot = blankTerminalReplicaSnapshot(2, 1);
399
- let addressed = 0;
400
- let sibling = 0;
401
- lane.subscribePane("pane-a", () => (addressed += 1));
402
- lane.subscribePane("pane-b", () => (sibling += 1));
403
- listeners.get("pane-a")!(seed(snapshot));
404
- for (let revision = 1; revision < 10_000; revision += 1) {
405
- listeners.get("pane-a")!(patch(snapshot, { rows: [] }, revision));
406
- }
407
- expect(addressed).toBe(10_000);
408
- expect(sibling).toBe(0);
409
- expect(semanticCallbacks).toBe(0);
410
- expect(lane.counters()).toMatchObject({ accepted: 10_000, published: 10_000 });
411
- });
412
-
413
- it("preserves text/key order with one authority request and UTF-8-bounded overflow", async () => {
414
- const { lane, control } = rig({ maxPendingInputs: 3, maxPendingInputBytes: 10 });
415
- const authority = deferred<boolean>();
416
- control.request = async (kind) => {
417
- control.requests.push(kind);
418
- const granted = await authority.promise;
419
- if (granted) control.owned.add(kind);
420
- return granted;
421
- };
422
- const first = lane.sendInput("pane-a", { kind: "text", data: "é" });
423
- const second = lane.sendInput("pane-a", { kind: "key", data: "Enter" });
424
- const third = lane.sendInput("pane-b", { kind: "text", data: "界" });
425
- const overflow = await lane.sendInput("pane-b", { kind: "key", data: "Up" });
426
- expect(overflow).toEqual({ status: "rejected", reason: "queue-full" });
427
- expect(control.requests).toEqual(["input"]);
428
-
429
- authority.resolve(true);
430
- await settle();
431
- expect(await Promise.all([first, second, third])).toEqual([
432
- { status: "sent" },
433
- { status: "sent" },
434
- { status: "sent" },
435
- ]);
436
- expect(control.writes.map((write) => write.input)).toEqual([
437
- { kind: "text", data: "é" },
438
- { kind: "key", data: "Enter" },
439
- { kind: "text", data: "界" },
440
- ]);
441
- expect(lane.counters()).toMatchObject({
442
- inputAccepted: 3,
443
- inputRejected: 1,
444
- inputWrites: 3,
445
- authorityRequests: 1,
446
- });
447
- });
448
-
449
- it("forwards trace identity only for accepted ordered input and fences retired work", async () => {
450
- const { lane, control } = rig({ maxPendingInputs: 2 });
451
- const authority = deferred<boolean>();
452
- control.request = async (kind) => {
453
- control.requests.push(kind);
454
- const granted = await authority.promise;
455
- if (granted) control.owned.add(kind);
456
- return granted;
457
- };
458
- const traceA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
459
- const traceB = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
460
- const traceRejected = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
461
- const first = lane.sendInput("pane-a", { kind: "text", data: "a" }, traceA);
462
- const second = lane.sendInput("pane-a", { kind: "text", data: "b" }, traceB);
463
- expect(await lane.sendInput("pane-a", { kind: "text", data: "c" }, traceRejected)).toEqual({
464
- status: "rejected",
465
- reason: "queue-full",
466
- });
467
- authority.resolve(true);
468
- expect(await Promise.all([first, second])).toEqual([{ status: "sent" }, { status: "sent" }]);
469
- expect(control.writes.map(({ performanceTraceId }) => performanceTraceId)).toEqual([
470
- traceA,
471
- traceB,
472
- ]);
473
-
474
- const gate = deferred<boolean>();
475
- control.owned.delete("input");
476
- control.request = async () => await gate.promise;
477
- const retired = lane.sendInput(
478
- "pane-a",
479
- { kind: "text", data: "old" },
480
- "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
481
- );
482
- lane.replaceGeneration(address(GENERATION_B));
483
- expect(await retired).toEqual({ status: "rejected", reason: "retired" });
484
- gate.resolve(true);
485
- await settle();
486
- expect(control.writes).toHaveLength(2);
487
- });
488
-
489
- it("reports settled queue bytes on the final transport acknowledgement", async () => {
490
- const source = new FakeSource();
491
- const control = new FakeControl();
492
- control.owned.add("input");
493
- const stages: Array<{
494
- operation: string;
495
- inputPending: number;
496
- inputInFlight: number;
497
- inputPendingBytes: number;
498
- }> = [];
499
- const lane = createTerminalFastLane({
500
- address: address(),
501
- source,
502
- control,
503
- repair: { request: () => undefined },
504
- onTraceStage: (stage) => stages.push(stage),
505
- });
506
-
507
- await expect(
508
- lane.sendInput(
509
- "pane-a",
510
- { kind: "key", data: "Enter" },
511
- "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
512
- ),
513
- ).resolves.toEqual({ status: "sent" });
514
- expect(stages.at(-1)).toMatchObject({
515
- operation: "transport-ack",
516
- inputPending: 0,
517
- inputInFlight: 0,
518
- inputPendingBytes: 0,
519
- });
520
- expect(lane.counters()).toMatchObject({
521
- inputPending: 0,
522
- inputInFlight: 0,
523
- inputPendingBytes: 0,
524
- });
525
- });
526
-
527
- it("dispatches a bounded FIFO window without waiting for each acknowledgement", async () => {
528
- const { lane, control } = rig({ maxInFlightInputs: 2 });
529
- control.owned.add("input");
530
- const firstGate = deferred<void>();
531
- const secondGate = deferred<void>();
532
- control.writeGates.push(firstGate.promise, secondGate.promise, Promise.resolve());
533
- const first = lane.sendInput("pane-a", { kind: "text", data: "a" });
534
- const second = lane.sendInput("pane-a", { kind: "text", data: "b" });
535
- const third = lane.sendInput("pane-a", { kind: "text", data: "c" });
536
- await settle();
537
- expect(control.writes.map(({ input }) => input.data)).toEqual(["a", "b"]);
538
-
539
- secondGate.resolve();
540
- await settle();
541
- expect(await second).toEqual({ status: "sent" });
542
- expect(control.writes.map(({ input }) => input.data)).toEqual(["a", "b", "c"]);
543
- expect(await third).toEqual({ status: "sent" });
544
- firstGate.resolve();
545
- expect(await first).toEqual({ status: "sent" });
546
- expect(lane.counters()).toMatchObject({ inputAccepted: 3, inputWrites: 3 });
547
- });
548
-
549
- it("retires an out-of-order authority loss exactly once and releases every queue byte", async () => {
550
- const { lane, control } = rig({
551
- maxPendingInputs: 3,
552
- maxPendingInputBytes: 3,
553
- maxInFlightInputs: 3,
554
- });
555
- control.owned.add("input");
556
- const firstGate = deferred<void>();
557
- const lostGate = deferred<void>();
558
- const thirdGate = deferred<void>();
559
- control.writeGates.push(firstGate.promise, lostGate.promise, thirdGate.promise);
560
- control.writeResults.push("ok", "authority-lost", "ok");
561
- const first = lane.sendInput("pane-a", { kind: "text", data: "a" });
562
- const lost = lane.sendInput("pane-a", { kind: "text", data: "b" });
563
- const third = lane.sendInput("pane-a", { kind: "text", data: "c" });
564
- await settle();
565
- expect(control.writes.map(({ input }) => input.data)).toEqual(["a", "b", "c"]);
566
-
567
- lostGate.resolve();
568
- await expect(lost).resolves.toEqual({ status: "rejected", reason: "authority-lost" });
569
- await expect(first).resolves.toEqual({ status: "rejected", reason: "authority-lost" });
570
- await expect(third).resolves.toEqual({ status: "rejected", reason: "authority-lost" });
571
- expect(lane.counters()).toMatchObject({
572
- inputRejected: 3,
573
- inputPending: 0,
574
- inputInFlight: 0,
575
- inputPendingBytes: 0,
576
- });
577
-
578
- firstGate.resolve();
579
- thirdGate.resolve();
580
- await settle();
581
- expect(lane.counters()).toMatchObject({ inputRejected: 3, inputWrites: 0 });
582
- control.owned.delete("input");
583
- control.writeGates.length = 0;
584
- control.writeResults.length = 0;
585
- expect(await lane.sendInput("pane-a", { kind: "text", data: "z" })).toEqual({
586
- status: "sent",
587
- });
588
- expect(lane.counters().inputPendingBytes).toBe(0);
589
- });
590
-
591
- it("fences late authority grants and write acknowledgements on generation replacement", async () => {
592
- const { lane, control } = rig();
593
- const authority = deferred<boolean>();
594
- control.request = async (kind) => {
595
- control.requests.push(kind);
596
- return await authority.promise;
597
- };
598
- const pendingGrant = lane.sendInput("pane-a", { kind: "key", data: "a" });
599
- lane.replaceGeneration(address(GENERATION_B));
600
- expect(await pendingGrant).toEqual({ status: "rejected", reason: "retired" });
601
- control.request = async (kind) => {
602
- control.requests.push(kind);
603
- control.owned.add(kind);
604
- return true;
605
- };
606
- const replacement = lane.sendInput("pane-a", { kind: "key", data: "b" });
607
- await settle();
608
- expect(await replacement).toEqual({ status: "sent" });
609
- expect(control.writes.map(({ input }) => input)).toEqual([{ kind: "key", data: "b" }]);
610
- authority.resolve(true);
611
- await settle();
612
- expect(control.writes).toHaveLength(1);
613
-
614
- control.owned.add("input");
615
- const write = deferred<void>();
616
- control.writeGate = write.promise;
617
- const pendingAck = lane.sendInput("pane-a", { kind: "key", data: "c" });
618
- await settle();
619
- lane.dispose();
620
- expect(await pendingAck).toEqual({ status: "rejected", reason: "disposed" });
621
- write.resolve();
622
- await settle();
623
- expect(lane.counters().inputWrites).toBe(1);
624
- });
625
-
626
- it("coalesces a resize burst to first+latest and never drops final size", async () => {
627
- const { lane, control } = rig();
628
- control.owned.add("geometry");
629
- const firstGate = deferred<void>();
630
- control.resizeGates.push(firstGate.promise, Promise.resolve());
631
- const first = lane.resize({ cols: 80, rows: 24 });
632
- const middle = lane.resize({ cols: 90, rows: 30 });
633
- const latest = lane.resize({ cols: 120, rows: 40 });
634
- expect(await middle).toEqual({ status: "superseded" });
635
- expect(control.resizes.map(({ viewport }) => viewport)).toEqual([{ cols: 80, rows: 24 }]);
636
- firstGate.resolve();
637
- await settle();
638
- expect(await first).toEqual({ status: "applied" });
639
- expect(await latest).toEqual({ status: "applied" });
640
- expect(control.resizes.map(({ viewport }) => viewport)).toEqual([
641
- { cols: 80, rows: 24 },
642
- { cols: 120, rows: 40 },
643
- ]);
644
- expect(lane.counters()).toMatchObject({
645
- resizeAccepted: 3,
646
- resizeTransports: 2,
647
- resizeSuperseded: 1,
648
- });
649
- });
650
-
651
- it("keeps equal-sized scoped targets distinct across an in-flight resize", async () => {
652
- const { lane, control } = rig();
653
- control.owned.add("geometry");
654
- const gate = deferred<void>();
655
- control.resizeGates.push(gate.promise, Promise.resolve());
656
- const first = lane.resize({ semanticWindowId: "window.a", cols: 80, rows: 24 });
657
- const second = lane.resize({ semanticWindowId: "window.b", cols: 80, rows: 24 });
658
- gate.resolve();
659
- await settle();
660
- expect(await first).toEqual({ status: "applied" });
661
- expect(await second).toEqual({ status: "applied" });
662
- expect(control.resizes.map(({ viewport }) => viewport.semanticWindowId)).toEqual([
663
- "window.a",
664
- "window.b",
665
- ]);
666
- lane.dispose();
667
- });
668
-
669
- it("preserves an exact geometry authority conflict without treating it as applied", async () => {
670
- const { lane, control } = rig();
671
- control.owned.add("geometry");
672
- control.resizeResult = "geometry-authority-conflict";
673
-
674
- await expect(lane.resize({ cols: 140, rows: 46 })).resolves.toEqual({
675
- status: "geometry-authority-conflict",
676
- });
677
- expect(control.resizes).toHaveLength(1);
678
- expect(lane.counters()).toMatchObject({ resizeAccepted: 1, resizeTransports: 1 });
679
- });
680
-
681
- it("fences an old pane subscription and accepts a fresh generation seed", () => {
682
- const { lane, source } = rig();
683
- const snapshot = blankTerminalReplicaSnapshot(2, 1);
684
- const revisions: string[] = [];
685
- lane.subscribePane("pane-a", (publication) =>
686
- revisions.push(`${publication.state.generation}:${publication.state.revision}`),
687
- );
688
- const oldListener = source.capture(GENERATION_A, "pane-a");
689
- source.emit(seed(snapshot));
690
- lane.replaceGeneration(address(GENERATION_B));
691
- oldListener(seed(snapshot, 1));
692
- source.emit(seed(snapshot, 0, GENERATION_B));
693
- expect(revisions).toEqual([`${GENERATION_A}:0`, `${GENERATION_B}:0`]);
694
- });
695
-
696
- it("retains canonical seed and patches while a pane has no renderer observer", () => {
697
- const { lane, source } = rig();
698
- const initial = blankTerminalReplicaSnapshot(2, 1);
699
- const stop = lane.subscribePane("pane-a", () => undefined);
700
- source.emit(seed(initial));
701
- stop();
702
-
703
- const changedRow = {
704
- ...initial.grid[0]!,
705
- cells: initial.grid[0]!.cells.map((cell, index) =>
706
- index === 0 ? { ...cell, grapheme: "R" } : cell,
707
- ),
708
- };
709
- source.emit(patch(initial, { rows: [{ index: 0, row: changedRow }] }, 1));
710
-
711
- const retained = lane.paneState("pane-a");
712
- expect(retained?.generation).toBe(GENERATION_A);
713
- expect(retained?.revision).toBe(1);
714
- expect(retained?.snapshot?.grid[0]?.cells[0]?.grapheme).toBe("R");
715
- expect(source.listeners.get(`${GENERATION_A}:pane-a`)?.size).toBe(1);
716
-
717
- const revisions: number[] = [];
718
- lane.subscribePane("pane-a", ({ state }) => revisions.push(state.revision));
719
- expect(revisions).toEqual([]);
720
- lane.dispose();
721
- expect(source.listeners.get(`${GENERATION_A}:pane-a`)?.size).toBe(0);
722
- });
723
-
724
- it("retains exactly the generation inventory without renderer observers", () => {
725
- const { lane, source } = rig();
726
- lane.retainPanes(["pane-a", "pane-b"]);
727
- expect(source.listeners.get(`${GENERATION_A}:pane-a`)?.size).toBe(1);
728
- expect(source.listeners.get(`${GENERATION_A}:pane-b`)?.size).toBe(1);
729
-
730
- const outside: number[] = [];
731
- lane.subscribePane("pane-outside", ({ state }) => outside.push(state.revision));
732
- expect(source.listeners.has(`${GENERATION_A}:pane-outside`)).toBe(false);
733
-
734
- const removed: number[] = [];
735
- lane.subscribePane("pane-a", ({ state }) => removed.push(state.revision));
736
-
737
- lane.retainPanes(["pane-b"]);
738
- expect(source.listeners.get(`${GENERATION_A}:pane-a`)?.size ?? 0).toBe(0);
739
- expect(source.listeners.get(`${GENERATION_A}:pane-b`)?.size).toBe(1);
740
- source.emit(seed(blankTerminalReplicaSnapshot(2, 1), 0, GENERATION_A, "pane-a"));
741
- expect(removed).toEqual([]);
742
- expect(outside).toEqual([]);
743
- lane.dispose();
744
- expect(source.listeners.get(`${GENERATION_A}:pane-b`)?.size ?? 0).toBe(0);
745
- });
746
-
747
- it("stages candidate panes additively and trims only after commit", () => {
748
- const { lane, source } = rig();
749
- lane.retainPanes(["pane-incumbent"]);
750
- const initial = blankTerminalReplicaSnapshot(2, 1);
751
- source.emit(seed(initial, 0, GENERATION_A, "pane-incumbent"));
752
- expect(lane.paneState("pane-incumbent")?.hash).toBe(hashTerminalReplicaSnapshot(initial));
753
-
754
- const releaseCandidate = lane.stagePanes(["pane-candidate"]);
755
- expect(source.listeners.get(`${GENERATION_A}:pane-incumbent`)?.size).toBe(1);
756
- expect(source.listeners.get(`${GENERATION_A}:pane-candidate`)?.size).toBe(1);
757
- expect(lane.paneState("pane-incumbent")?.hash).toBe(hashTerminalReplicaSnapshot(initial));
758
-
759
- lane.retainPanes(["pane-candidate"]);
760
- // The candidate stage overlaps the committed inventory, so releasing it
761
- // cannot close the newly active source. The incumbent trims at commit.
762
- releaseCandidate();
763
- expect(source.listeners.get(`${GENERATION_A}:pane-incumbent`)?.size ?? 0).toBe(0);
764
- expect(source.listeners.get(`${GENERATION_A}:pane-candidate`)?.size).toBe(1);
765
- lane.dispose();
766
- });
767
-
768
- it("releases an abandoned candidate stage without disturbing the incumbent", () => {
769
- const { lane, source } = rig();
770
- lane.retainPanes(["pane-incumbent"]);
771
- const releaseCandidate = lane.stagePanes(["pane-candidate"]);
772
- releaseCandidate();
773
- releaseCandidate();
774
- expect(source.listeners.get(`${GENERATION_A}:pane-candidate`)?.size ?? 0).toBe(0);
775
- expect(source.listeners.get(`${GENERATION_A}:pane-incumbent`)?.size).toBe(1);
776
- lane.dispose();
777
- });
778
- });
779
-
780
- function fnv1a64(value: string): string {
781
- let hash = 0xcbf29ce484222325n;
782
- for (const byte of new TextEncoder().encode(value)) {
783
- hash ^= BigInt(byte);
784
- hash = BigInt.asUintN(64, hash * 0x100000001b3n);
785
- }
786
- return hash.toString(16).padStart(16, "0");
787
- }
788
-
789
- describe("terminal fast-lane shared conformance", () => {
790
- it("produces identical transitions and counters for core/OpenTUI/Web/SDK-labelled clients", async () => {
791
- const surfaces = ["core", "opentui", "web", "sdk"] as const;
792
- const traces: Array<{ surface: (typeof surfaces)[number]; hash: string; counters: unknown }> =
793
- [];
794
- for (const surface of surfaces) {
795
- const { lane, source, control, repairs } = rig();
796
- const snapshot = blankTerminalReplicaSnapshot(2, 1);
797
- const transitions: string[] = [];
798
- lane.subscribePane("pane-a", ({ state }) =>
799
- transitions.push(
800
- `${state.generation}:${state.incarnation}:${state.revision}:${state.hash}`,
801
- ),
802
- );
803
- source.emit(seed(snapshot));
804
- source.emit(seed(snapshot));
805
- source.emit(patch(snapshot, { rows: [] }, 2));
806
- source.emit(seed(snapshot, 2, GENERATION_A, "pane-a", `${GENERATION_A}:1`));
807
- control.owned.add("input");
808
- await Promise.all([
809
- lane.sendInput("pane-a", { kind: "text", data: "paste" }),
810
- lane.sendInput("pane-a", { kind: "key", data: "Enter" }),
811
- ]);
812
- control.owned.delete("input");
813
- control.requestResult = false;
814
- await lane.sendInput("pane-a", { kind: "key", data: "C-c" });
815
- control.owned.add("geometry");
816
- const gate = deferred<void>();
817
- control.resizeGates.push(gate.promise, Promise.resolve());
818
- const first = lane.resize({ cols: 80, rows: 24 });
819
- const middle = lane.resize({ cols: 90, rows: 30 });
820
- const latest = lane.resize({ cols: 100, rows: 40 });
821
- await middle;
822
- gate.resolve();
823
- await Promise.all([first, latest]);
824
- lane.replaceGeneration(address(GENERATION_B));
825
- source.emit(seed(snapshot, 0, GENERATION_B));
826
- transitions.push(`repairs:${repairs.length}`);
827
- const counters = lane.counters();
828
- traces.push({ surface, hash: fnv1a64(JSON.stringify(transitions)), counters });
829
- lane.dispose();
830
- }
831
- expect(new Set(traces.map(({ hash }) => hash)).size).toBe(1);
832
- expect(new Set(traces.map(({ counters }) => JSON.stringify(counters))).size).toBe(1);
833
- expect(new Set(traces.map(({ surface }) => surface)).size).toBe(4);
834
- });
835
- });