tmux-ide 2.9.0-beta.21 → 2.9.0-beta.26

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 (209) hide show
  1. package/bin/cli.js +46791 -43589
  2. package/package.json +2 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/fleet-lifecycle.ts +5 -0
  15. package/packages/contracts/src/index.ts +2 -0
  16. package/packages/contracts/src/interaction-receipts.ts +39 -0
  17. package/packages/contracts/src/issue-error.ts +1 -1
  18. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  19. package/packages/contracts/src/pane-stream.ts +51 -8
  20. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  21. package/packages/contracts/src/window-links.ts +76 -0
  22. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  23. package/packages/core/src/interaction-receipts.ts +4 -0
  24. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  25. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  26. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  27. package/packages/daemon/dist/command-center/discovery.js +2 -9
  28. package/packages/daemon/dist/command-center/server.js +21 -1
  29. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  30. package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
  31. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  32. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  33. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  34. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  35. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
  36. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  37. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  38. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  39. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  40. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  41. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  42. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  43. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  44. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  46. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  47. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  48. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  49. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  50. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  51. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  52. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  53. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  54. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  55. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  56. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  57. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  58. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  59. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  60. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  61. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  62. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  63. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  64. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  65. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  66. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  67. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  68. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  70. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  71. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  72. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  73. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  74. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  75. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  76. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
  77. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
  78. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
  79. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
  80. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
  81. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
  82. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  83. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  84. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
  85. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
  86. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
  87. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  88. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
  89. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  90. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
  91. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  92. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  93. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
  94. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  95. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  96. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  97. package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
  98. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
  99. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
  100. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
  101. package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
  102. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  103. package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
  104. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  105. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
  106. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  107. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  108. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  109. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  110. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  111. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  112. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  113. package/packages/daemon/src/command-center/discovery.ts +2 -10
  114. package/packages/daemon/src/command-center/server.ts +29 -1
  115. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  116. package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
  117. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  118. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  119. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  120. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  121. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
  122. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  123. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  124. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  125. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  126. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  127. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  128. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  129. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  130. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  131. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  132. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  133. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  134. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  135. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  136. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  137. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  138. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  139. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  140. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  141. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  142. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  143. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  144. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  145. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  146. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  147. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  148. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  149. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  150. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  151. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  152. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  153. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  154. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  155. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  156. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  157. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  158. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  159. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  160. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  161. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  162. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  163. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  164. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
  165. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
  166. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
  167. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
  168. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
  169. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
  170. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  171. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  172. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
  173. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
  174. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
  175. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  176. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
  177. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  178. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  179. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  180. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
  181. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  182. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
  183. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  184. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  185. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
  186. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
  187. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  188. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  189. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  190. package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
  191. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
  192. package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
  193. package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
  194. package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
  195. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  196. package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
  197. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  198. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
  199. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  200. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  201. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  202. package/packages/daemon-client/package.json +3 -1
  203. package/packages/daemon-client/src/index.ts +2 -0
  204. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  205. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  206. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  207. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  208. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  209. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -0,0 +1,160 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { WINDOW_LINK_MAX_LINKS, WindowLinkTargetSchemaZ, WindowLinkTopologySchemaZ, WorkspaceCatalogLiveSessionIdSchemaZ, TerminalAttachmentSemanticWindowIdSchemaZ, } from "@tmux-ide/contracts";
3
+ export class WindowLinkResolutionError extends Error {
4
+ reason;
5
+ constructor(reason) {
6
+ super(reason);
7
+ this.reason = reason;
8
+ this.name = "WindowLinkResolutionError";
9
+ }
10
+ }
11
+ /**
12
+ * One authority per daemon/server/session incarnation. This is an observation
13
+ * model, not an immutable native winlink identity: unobserved unlink/relink ABA
14
+ * is unknowable. Resolved targets still require an execution-time native guard.
15
+ * Not installed into the v1 stream; C2 activates it with the complete link-aware
16
+ * producer/consumer boundary.
17
+ */
18
+ export class WindowLinkAuthority {
19
+ liveSessionId;
20
+ runtimeSessionId;
21
+ records = new Map();
22
+ revision = 0;
23
+ topology = null;
24
+ disposed = false;
25
+ constructor(liveSessionId, runtimeSessionId) {
26
+ this.liveSessionId = liveSessionId;
27
+ this.runtimeSessionId = runtimeSessionId;
28
+ WorkspaceCatalogLiveSessionIdSchemaZ.parse(liveSessionId);
29
+ if (!/^\$(?:0|[1-9]\d{0,15})$/u.test(runtimeSessionId) ||
30
+ !Number.isSafeInteger(Number(runtimeSessionId.slice(1))))
31
+ throw new TypeError("Invalid native session id");
32
+ }
33
+ /** A malformed fresh observation revokes old actions; no partial topology is published. */
34
+ reconcile(observations) {
35
+ try {
36
+ return this.stageAndPublish(observations);
37
+ }
38
+ catch (error) {
39
+ this.invalidate();
40
+ throw error;
41
+ }
42
+ }
43
+ stageAndPublish(observations) {
44
+ if (this.disposed)
45
+ throw new WindowLinkResolutionError("window_link_stale");
46
+ if (observations.length === 0 || observations.length > WINDOW_LINK_MAX_LINKS)
47
+ throw new TypeError("Invalid window link count");
48
+ const indexes = new Set();
49
+ const semanticByRuntime = new Map();
50
+ const runtimeBySemantic = new Map();
51
+ let activeCount = 0;
52
+ for (const row of observations) {
53
+ if (!Number.isSafeInteger(row.index) ||
54
+ row.index < 0 ||
55
+ indexes.has(row.index) ||
56
+ !/^@(?:0|[1-9]\d{0,15})$/u.test(row.runtimeWindowId) ||
57
+ !Number.isSafeInteger(Number(row.runtimeWindowId.slice(1))) ||
58
+ typeof row.active !== "boolean")
59
+ throw new TypeError("Malformed native window link observation");
60
+ TerminalAttachmentSemanticWindowIdSchemaZ.parse(row.semanticWindowId);
61
+ indexes.add(row.index);
62
+ if (row.active)
63
+ activeCount++;
64
+ const semantic = semanticByRuntime.get(row.runtimeWindowId);
65
+ const runtime = runtimeBySemantic.get(row.semanticWindowId);
66
+ if ((semantic !== undefined && semantic !== row.semanticWindowId) ||
67
+ (runtime !== undefined && runtime !== row.runtimeWindowId))
68
+ throw new TypeError("Window backing identity is inconsistent");
69
+ semanticByRuntime.set(row.runtimeWindowId, row.semanticWindowId);
70
+ runtimeBySemantic.set(row.semanticWindowId, row.runtimeWindowId);
71
+ }
72
+ if (activeCount !== 1)
73
+ throw new TypeError("Expected one active window link");
74
+ const rows = [...observations].sort((left, right) => left.index - right.index);
75
+ const changed = rows.length !== this.records.size ||
76
+ rows.some((row) => {
77
+ const prior = this.records.get(row.index);
78
+ return (prior?.runtimeWindowId !== row.runtimeWindowId ||
79
+ prior?.semanticWindowId !== row.semanticWindowId);
80
+ });
81
+ const revision = this.revision + (changed ? 1 : 0);
82
+ if (!Number.isSafeInteger(revision))
83
+ throw new RangeError("Window link revision exhausted");
84
+ const next = new Map();
85
+ for (const row of rows) {
86
+ const prior = this.records.get(row.index);
87
+ const retained = prior?.runtimeWindowId === row.runtimeWindowId &&
88
+ prior.semanticWindowId === row.semanticWindowId;
89
+ next.set(row.index, {
90
+ ...row,
91
+ linkId: retained ? prior.linkId : `window-link.${randomUUID().replaceAll("-", "")}`,
92
+ });
93
+ }
94
+ const records = [...next.values()];
95
+ const topology = WindowLinkTopologySchemaZ.parse({
96
+ liveSessionId: this.liveSessionId,
97
+ linkRevision: revision,
98
+ activeLinkId: records.find((row) => row.active).linkId,
99
+ links: records.map((row) => ({
100
+ linkId: row.linkId,
101
+ semanticWindowId: row.semanticWindowId,
102
+ displayIndex: row.index,
103
+ })),
104
+ });
105
+ this.records = next;
106
+ this.revision = revision;
107
+ this.topology = topology;
108
+ return this.snapshot();
109
+ }
110
+ snapshot() {
111
+ return this.topology
112
+ ? { ...this.topology, links: this.topology.links.map((link) => ({ ...link })) }
113
+ : null;
114
+ }
115
+ /** Reconnect/gap/discontinuous observation must retire all previously issued handles. */
116
+ invalidate() {
117
+ this.records.clear();
118
+ this.topology = null;
119
+ this.revision++;
120
+ if (!Number.isSafeInteger(this.revision))
121
+ this.disposed = true;
122
+ }
123
+ dispose() {
124
+ this.invalidate();
125
+ this.disposed = true;
126
+ }
127
+ resolve(target) {
128
+ WindowLinkTargetSchemaZ.parse(target);
129
+ if (target.liveSessionId !== this.liveSessionId)
130
+ throw new WindowLinkResolutionError("window_link_session_mismatch");
131
+ if (!this.topology || this.disposed || target.linkRevision !== this.revision)
132
+ throw new WindowLinkResolutionError("window_link_stale");
133
+ const record = [...this.records.values()].find((row) => row.linkId === target.linkId);
134
+ if (!record)
135
+ throw new WindowLinkResolutionError("window_link_stale");
136
+ if (record.semanticWindowId !== target.expectedSemanticWindowId)
137
+ throw new WindowLinkResolutionError("window_link_backing_mismatch");
138
+ return {
139
+ runtimeSessionId: this.runtimeSessionId,
140
+ runtimeWindowId: record.runtimeWindowId,
141
+ index: record.index,
142
+ };
143
+ }
144
+ /** Legacy backing-only selection is safe only when exactly one link applies. */
145
+ uniqueTargetForBacking(semanticWindowId) {
146
+ if (!this.topology || this.disposed)
147
+ throw new WindowLinkResolutionError("window_link_stale");
148
+ const links = [...this.records.values()].filter((row) => row.semanticWindowId === semanticWindowId);
149
+ if (links.length === 0)
150
+ throw new WindowLinkResolutionError("window_link_stale");
151
+ if (links.length !== 1)
152
+ throw new WindowLinkResolutionError("window_link_ambiguous");
153
+ return {
154
+ liveSessionId: this.liveSessionId,
155
+ linkId: links[0].linkId,
156
+ expectedSemanticWindowId: semanticWindowId,
157
+ linkRevision: this.revision,
158
+ };
159
+ }
160
+ }
@@ -5,7 +5,7 @@ import { PaneStreamLeaseRequestSchemaZ, } from "@tmux-ide/contracts";
5
5
  * PaneStreamLeaseManager — in-memory authority for pane-stream leases (m43
6
6
  * card 2), following the terminal-attachment lease discipline:
7
7
  *
8
- * - a one-time `ps1_` bearer ticket whose SHA-256 digest is all the manager
8
+ * - a one-time `ps2_` bearer ticket whose SHA-256 digest is all the manager
9
9
  * retains, burned before activation so no concurrent caller can replay it;
10
10
  * - the ticket TTL bounds credential DELIVERY (the authenticated redemption
11
11
  * frame must arrive in time); execution after delivery gets its own bounded
@@ -30,7 +30,7 @@ const SessionNameSchemaZ = z
30
30
  .min(1)
31
31
  .max(256)
32
32
  .refine((value) => !/[\0\r\n]/u.test(value));
33
- const TicketPattern = /^ps1_[A-Za-z0-9_-]{43}$/u;
33
+ const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
34
34
  export class PaneStreamLeaseError extends Error {
35
35
  code;
36
36
  constructor(code, message) {
@@ -95,7 +95,7 @@ export class PaneStreamLeaseManager {
95
95
  if (ticketBytes.byteLength !== 32) {
96
96
  throw new PaneStreamLeaseError("identity-generation-failed", "The secure random source returned an invalid ticket.");
97
97
  }
98
- const redemptionTicket = `ps1_${Buffer.from(ticketBytes).toString("base64url")}`;
98
+ const redemptionTicket = `ps2_${Buffer.from(ticketBytes).toString("base64url")}`;
99
99
  const state = {
100
100
  leaseId,
101
101
  requestId,
@@ -1,3 +1,4 @@
1
+ import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
1
2
  import { z } from "zod";
2
3
  import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
3
4
  import { canonicalOriginOrNull, digestSecret, digestsEqual, rawDataByteLength, rawDataToBuffer, safeCloseSocket, strictJsonParse, } from "../attachments/admission-util.js";
@@ -6,7 +7,7 @@ import { terminalDeliveryObservationOrdinal } from "../session-runtime/terminal-
6
7
  import { PaneStreamLeaseError, } from "./lease-manager.js";
7
8
  import { DEFAULT_PANE_STREAM_FLOW_BUDGETS, PaneStreamWireLedger, } from "./wire-ledger.js";
8
9
  /**
9
- * The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `ps1_`
10
+ * The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `ps2_`
10
11
  * ticket and bridges MirrorService subscriptions to wire frames. Admission is
11
12
  * the direct-websocket discipline verbatim — Origin-gated upgrade reservation,
12
13
  * digest-only pending tickets, one text redemption frame, delivery-bound TTL —
@@ -53,7 +54,7 @@ function hasCanonicalInputFramePrefix(raw) {
53
54
  startsWithBuffer(raw, CANONICAL_TEXT_INPUT_FRAME_PREFIX) ||
54
55
  startsWithBuffer(raw, TYPE_FIRST_INPUT_FRAME_PREFIX));
55
56
  }
56
- const TicketPattern = /^ps1_[A-Za-z0-9_-]{43}$/u;
57
+ const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
57
58
  const BindingIdSchemaZ = z
58
59
  .string()
59
60
  .min(1)
@@ -270,7 +271,7 @@ export class PaneStreamAdmissionCoordinator {
270
271
  if (this.#shuttingDown) {
271
272
  return { accepted: false, code: "daemon-shutting-down", httpStatus: 503 };
272
273
  }
273
- if (input.path !== PANE_STREAM_REDEEM_PATH) {
274
+ if (input.path !== new URL(this.#webSocketUrl).pathname) {
274
275
  return { accepted: false, code: "invalid-path", httpStatus: 404 };
275
276
  }
276
277
  if (input.protocols.length !== 1 || input.protocols[0] !== PANE_STREAM_WEBSOCKET_SUBPROTOCOL) {
@@ -616,6 +617,7 @@ export class PaneStreamLiveConnection {
616
617
  #semanticLayouts = new Map();
617
618
  #semanticExpectedPaneIds = null;
618
619
  #semanticRuntimeSessionId = null;
620
+ #semanticWindowLinks = null;
619
621
  #semanticTopologyEpoch = -1;
620
622
  #sendQueue = [];
621
623
  #semanticDrainWaiters = new Map();
@@ -920,7 +922,13 @@ export class PaneStreamLiveConnection {
920
922
  authority.topologyEpoch < 0
921
923
  ? null
922
924
  : this.#validateInitialLayout(authority.layouts, expectedPaneIds);
923
- if (!stagedInitialFrames) {
925
+ if (!stagedInitialFrames ||
926
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
927
+ type: "layout-snapshot",
928
+ topologyEpoch: authority?.topologyEpoch,
929
+ layouts: stagedInitialFrames,
930
+ windowLinks: authority?.windowLinks,
931
+ }).success) {
924
932
  await subscription.close().catch(() => undefined);
925
933
  this.#failTopologyChanged();
926
934
  return;
@@ -1006,7 +1014,13 @@ export class PaneStreamLiveConnection {
1006
1014
  authority.topologyEpoch < 0
1007
1015
  ? null
1008
1016
  : this.#validateInitialLayout(authority.layouts, expectedPaneIds);
1009
- if (!initialFrames) {
1017
+ if (!initialFrames ||
1018
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
1019
+ type: "layout-snapshot",
1020
+ topologyEpoch: authority?.topologyEpoch,
1021
+ layouts: initialFrames,
1022
+ windowLinks: authority?.windowLinks,
1023
+ }).success) {
1010
1024
  await Promise.all(opened.map(({ delivery }) => delivery.close().catch(() => undefined)));
1011
1025
  this.#failTopologyChanged();
1012
1026
  return;
@@ -1014,6 +1028,7 @@ export class PaneStreamLiveConnection {
1014
1028
  this.#recordDiagnosticLifecycle("pane-stream-layout-validated");
1015
1029
  this.#semanticExpectedPaneIds = expectedPaneIds;
1016
1030
  this.#semanticRuntimeSessionId = authority.runtimeSessionId;
1031
+ this.#semanticWindowLinks = authority.windowLinks;
1017
1032
  this.#semanticTopologyEpoch = authority.topologyEpoch;
1018
1033
  this.#semanticLayouts.clear();
1019
1034
  for (const event of authority.layouts)
@@ -1022,6 +1037,7 @@ export class PaneStreamLiveConnection {
1022
1037
  type: "layout-snapshot",
1023
1038
  topologyEpoch: authority.topologyEpoch,
1024
1039
  layouts: initialFrames,
1040
+ windowLinks: authority.windowLinks,
1025
1041
  });
1026
1042
  layoutActivated = true;
1027
1043
  this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
@@ -1332,10 +1348,27 @@ export class PaneStreamLiveConnection {
1332
1348
  return;
1333
1349
  }
1334
1350
  const frames = this.#validateInitialLayout(snapshot.layouts, this.#semanticExpectedPaneIds);
1335
- if (!frames) {
1351
+ if (!frames ||
1352
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
1353
+ type: "layout-snapshot",
1354
+ topologyEpoch: snapshot.topologyEpoch,
1355
+ layouts: frames,
1356
+ windowLinks: snapshot.windowLinks,
1357
+ }).success) {
1336
1358
  this.#failTopologyChanged();
1337
1359
  return;
1338
1360
  }
1361
+ const previousLinks = this.#semanticWindowLinks;
1362
+ if (!previousLinks ||
1363
+ snapshot.windowLinks.liveSessionId !== previousLinks.liveSessionId ||
1364
+ snapshot.windowLinks.linkRevision < previousLinks.linkRevision ||
1365
+ (snapshot.windowLinks.linkRevision === previousLinks.linkRevision &&
1366
+ JSON.stringify([...snapshot.windowLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId))) !==
1367
+ JSON.stringify([...previousLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId))))) {
1368
+ this.#failTopologyChanged();
1369
+ return;
1370
+ }
1371
+ this.#semanticWindowLinks = snapshot.windowLinks;
1339
1372
  this.#semanticTopologyEpoch = snapshot.topologyEpoch;
1340
1373
  this.#semanticLayouts.clear();
1341
1374
  for (const event of snapshot.layouts)
@@ -1344,6 +1377,7 @@ export class PaneStreamLiveConnection {
1344
1377
  type: "layout-snapshot",
1345
1378
  topologyEpoch: snapshot.topologyEpoch,
1346
1379
  layouts: frames,
1380
+ windowLinks: snapshot.windowLinks,
1347
1381
  });
1348
1382
  }
1349
1383
  #onLayout(event) {
@@ -0,0 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Shared opaque incarnation identity for catalog and terminal authority. */
3
+ export function liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated) {
4
+ const digest = createHash("sha256")
5
+ .update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
6
+ .digest("hex")
7
+ .slice(0, 20);
8
+ return `live-session.${digest}`;
9
+ }
@@ -1,7 +1,8 @@
1
1
  import { decodeNativeGridCapture } from "../mirror/native-grid-capture.js";
2
2
  /** One bounded read on entering scrollback. No captures are issued by wheel movement. */
3
3
  export async function readNativeBacking(options) {
4
- const url = new URL(`/api/project/${encodeURIComponent(options.workspaceName)}/terminal-native-backing/${encodeURIComponent(options.paneId)}`, options.baseUrl);
4
+ const url = new URL(options.resourcePath ??
5
+ `/api/project/${encodeURIComponent(options.workspaceName)}/terminal-native-backing/${encodeURIComponent(options.paneId)}`, options.baseUrl);
5
6
  for (const [key, value] of Object.entries(options.expected))
6
7
  url.searchParams.set(key, String(value));
7
8
  const response = await fetch(url, {
@@ -7,6 +7,12 @@ export function sessionRuntimeInteractionFacts(intent) {
7
7
  target: { kind: "pane", semanticPaneId: intent.semanticPaneId },
8
8
  summary: { operationKind: intent.verb, direction: intent.direction },
9
9
  };
10
+ case "workspace.window.link.select":
11
+ case "workspace.window.link.unlink":
12
+ return {
13
+ target: { kind: "window-link", target: intent.target },
14
+ summary: { operationKind: intent.verb },
15
+ };
10
16
  case "workspace.window.kill":
11
17
  return {
12
18
  target: { kind: "window", target: intent.target },
@@ -85,6 +91,9 @@ export function sessionRuntimeObservedProof(intent, rawResult) {
85
91
  if (result.verb !== intent.verb)
86
92
  throw new TypeError("Mutation result verb does not match intent");
87
93
  switch (result.verb) {
94
+ case "workspace.window.link.select":
95
+ case "workspace.window.link.unlink":
96
+ return { operationKind: result.verb, outcome: result.outcome, target: result.target };
88
97
  case "workspace.window.split":
89
98
  return {
90
99
  operationKind: result.verb,
@@ -258,6 +258,12 @@ export class SessionRuntimeRegistry {
258
258
  return ((this.#sessions.get(session)?.trustedInventoryQualified() ?? false) &&
259
259
  this.#mirror.hasRetainedSession(session));
260
260
  }
261
+ /** Called by the serialized semantic mutation executor, never by a transport directly. */
262
+ executeWindowLinkAction(session, request) {
263
+ if (this.#disposed)
264
+ return Promise.reject(new Error("Session runtime disposed"));
265
+ return this.#mirror.executeWindowLinkAction(session, request);
266
+ }
261
267
  /** Retire one no-longer-registered session without disturbing siblings. */
262
268
  async retireSession(session) {
263
269
  const runtime = this.#sessions.get(session);
@@ -244,7 +244,7 @@ export class SessionSemanticMutationExecutor {
244
244
  // Admission can wait behind prior work. Revalidate the opaque principal
245
245
  // at the last synchronous boundary before tmux receives any effect.
246
246
  authorizeBeforeEffect?.();
247
- result = this.#options.execute(operationId, intent, timing);
247
+ result = await this.#options.execute(operationId, intent, timing);
248
248
  }
249
249
  catch (cause) {
250
250
  if (needsTmuxObservation)
@@ -17,6 +17,7 @@ export function semanticMutationResourceChanges(raw) {
17
17
  ];
18
18
  if (result.verb === "workspace.window.split" ||
19
19
  result.verb === "workspace.window.kill" ||
20
+ result.verb === "workspace.window.link.unlink" ||
20
21
  result.verb === "workspace.pane.kill" ||
21
22
  result.verb === "workspace.session.kill") {
22
23
  changes.push({ ...base, workspaceName: null, resource: "fleet-catalog" });
@@ -1,5 +1,6 @@
1
+ import { prepareScopedOpenTuiConnection } from "./application-shell-server-connection.js";
1
2
  import { OpenTuiStartupError, startupFailureFromError } from "./startup-failure.js";
2
- import { readApplicationDaemonInfo as readCanonicalDaemonInfo, isApplicationDaemonAlive as isCanonicalDaemonAlive, } from "./runtime/application-daemon-authority.js";
3
+ import { applicationDaemonEndpoint, readApplicationDaemonInfo as readCanonicalDaemonInfo, isApplicationDaemonAlive as isCanonicalDaemonAlive, } from "./runtime/application-daemon-authority.js";
3
4
  import { APPLICATION_SHELL_RESOURCE_V2_VERSION, } from "@tmux-ide/contracts";
4
5
  import { createDirectLoopbackDaemonTransport, } from "@tmux-ide/daemon-client/direct-application-shell-transport";
5
6
  import { WebSocket } from "ws";
@@ -88,7 +89,7 @@ export async function resolveOpenTuiApplicationShellConnection(sessionName, over
88
89
  applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
89
90
  ...(diagnose ? { terminalRuntimeDiagnostic: diagnose } : {}),
90
91
  });
91
- const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName);
92
+ const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint ?? applicationDaemonEndpoint, undefined, liveSessionId ?? undefined);
92
93
  let disposed = false;
93
94
  let diagnosticClientFetchOrdinal = 0;
94
95
  let diagnosticPrewarmOrdinal = 0;
@@ -263,6 +264,10 @@ export async function resolveOpenTuiApplicationShellConnection(sessionName, over
263
264
  export async function prepareOpenTuiApplicationShellConnection(sessionName, overrides = {}) {
264
265
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
265
266
  try {
267
+ if (dependencies.server) {
268
+ const unscoped = { ...dependencies, server: undefined };
269
+ return await prepareScopedOpenTuiConnection(sessionName, dependencies, () => prepareOpenTuiApplicationShellConnection(sessionName, unscoped));
270
+ }
266
271
  const result = await dependencies.ensureSessionWorkspace(sessionName);
267
272
  if (result === false)
268
273
  return null;
@@ -0,0 +1,114 @@
1
+ import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
2
+ import { createScopedTmuxServerTransport, createScopedTmuxServerCatalogPort, createScopedTmuxServerOwnerActions, } from "@tmux-ide/daemon-client/scoped-tmux-server-transport";
3
+ import { canonicalDaemonUrl } from "../../lib/canonical-daemon.js";
4
+ import { createOpenTuiVerifiedRoutingContext } from "./open-tui-verified-routing.js";
5
+ import { OPEN_TUI_HOST_CLIENT_ID } from "./open-tui-workspace-runtime-port.js";
6
+ import { OpenTuiStartupError } from "./startup-failure.js";
7
+ /** A selected registration never falls through to a name-only/default-server route. */
8
+ export async function prepareScopedOpenTuiConnection(sessionName, dependencies, prepareDefault) {
9
+ const scope = dependencies.server;
10
+ const daemon = dependencies.readCanonicalDaemonInfo();
11
+ if (!daemon || !daemon.authToken || !(await dependencies.isCanonicalDaemonAlive(daemon)))
12
+ throw new OpenTuiStartupError({ reason: "daemon-unavailable" });
13
+ const clientOptions = {
14
+ baseUrl: canonicalDaemonUrl("http", daemon.bindHostname, daemon.port),
15
+ ownerToken: daemon.authToken,
16
+ hostClientId: OPEN_TUI_HOST_CLIENT_ID,
17
+ origin: "tmux-ide://opentui",
18
+ };
19
+ const client = createTmuxServerClient(clientOptions, scope);
20
+ try {
21
+ const catalog = await client.sessions();
22
+ const candidates = catalog.sessions.filter((session) => session.sessionName === sessionName &&
23
+ (!dependencies.expectedLiveSessionId ||
24
+ session.liveSessionId === dependencies.expectedLiveSessionId));
25
+ if (candidates.length !== 1)
26
+ throw new OpenTuiStartupError({ reason: "session-unavailable" });
27
+ const liveSessionId = candidates[0].liveSessionId;
28
+ // The borrowed default owner has the daemon's original generation. Its existing
29
+ // transport preserves the full app surface while the scoped read proves routing.
30
+ if (scope.generation === daemon.instanceId) {
31
+ const connection = await prepareDefault();
32
+ if (!connection || connection.liveSessionId !== liveSessionId) {
33
+ connection?.dispose();
34
+ throw new OpenTuiStartupError({ reason: "session-unavailable" });
35
+ }
36
+ try {
37
+ const current = await client.sessions();
38
+ if (!current.sessions.some((session) => session.liveSessionId === liveSessionId && session.sessionName === sessionName))
39
+ throw new OpenTuiStartupError({ reason: "session-unavailable" });
40
+ const options = {
41
+ clientOptions,
42
+ scope,
43
+ target: connection.target,
44
+ sessionName,
45
+ liveSessionId,
46
+ };
47
+ const routing = createOpenTuiVerifiedRoutingContext(daemon, connection.workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint, scope, liveSessionId);
48
+ connection.routing?.retire();
49
+ return {
50
+ ...connection,
51
+ server: scope,
52
+ routing,
53
+ ownerActions: createScopedTmuxServerOwnerActions(options, connection.transport),
54
+ dispose() {
55
+ routing?.retire();
56
+ connection.dispose();
57
+ },
58
+ };
59
+ }
60
+ catch (error) {
61
+ connection.dispose();
62
+ throw error;
63
+ }
64
+ }
65
+ const opened = await client.openSession(liveSessionId);
66
+ if (opened.liveSessionId !== liveSessionId)
67
+ throw new OpenTuiStartupError({ reason: "session-unavailable" });
68
+ const workspaceName = opened.workspaceName;
69
+ const target = {
70
+ daemon: {
71
+ protocolVersion: daemon.protocolVersion,
72
+ productVersion: daemon.productVersion,
73
+ instanceId: scope.generation,
74
+ startedAt: daemon.startedAt,
75
+ ...(daemon.environmentId ? { environmentId: daemon.environmentId } : {}),
76
+ },
77
+ workspaceName,
78
+ };
79
+ const options = { clientOptions, scope, target, sessionName, liveSessionId };
80
+ const transport = createScopedTmuxServerTransport(options);
81
+ const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint, scope, liveSessionId);
82
+ const controller = new AbortController();
83
+ let preparation = null;
84
+ return {
85
+ workspaceName,
86
+ liveSessionId,
87
+ target,
88
+ server: scope,
89
+ transport,
90
+ routing,
91
+ catalog: createScopedTmuxServerCatalogPort(options, transport),
92
+ ownerActions: createScopedTmuxServerOwnerActions(options, transport),
93
+ prepareTerminalRuntimeInventory() {
94
+ preparation ??= transport
95
+ .prepareTerminalRuntimeInventory(target, AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)]))
96
+ .catch(() => {
97
+ // Fallback remains inside this owner; never use the legacy default resource.
98
+ transport.selectApplicationShellFallback("preparation-rejected");
99
+ return null;
100
+ });
101
+ return preparation;
102
+ },
103
+ dispose() {
104
+ controller.abort(new DOMException("Selected server connection retired", "AbortError"));
105
+ void preparation?.then((prepared) => prepared?.dispose()).catch(() => undefined);
106
+ transport.disposeEventSupervisor();
107
+ routing?.retire();
108
+ },
109
+ };
110
+ }
111
+ finally {
112
+ client.dispose();
113
+ }
114
+ }
@@ -52,7 +52,7 @@ export const MENU_ITEMS = {
52
52
  window: [
53
53
  { id: "new", label: "New window" },
54
54
  { id: "rename", label: "Rename window", input: "rename to" },
55
- { id: "kill", label: "Kill window", danger: true },
55
+ { id: "kill", label: "Kill window and panes in all links", danger: true },
56
56
  ],
57
57
  };
58
58
  /** PURE — the pane menu's item list for a concrete pane (M22.9). App-mouse
@@ -1,8 +1,9 @@
1
- import { openPaneStreamRuntimeClient, } from "@tmux-ide/daemon-client/pane-stream-client";
1
+ import { openPaneStreamRuntimeClient, connectIssuedPaneStreamRuntimeClient, } from "@tmux-ide/daemon-client/pane-stream-client";
2
+ import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
2
3
  import { canonicalDaemonUrl } from "../../lib/canonical-daemon.js";
3
4
  import { readNativeBacking, } from "../../terminal/protocol/native-backing-client.js";
4
5
  import { applicationDaemonEndpoint, } from "./runtime/application-daemon-authority.js";
5
- export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, openClient = openPaneStreamRuntimeClient, readEndpoint = applicationDaemonEndpoint) {
6
+ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, openClient = openPaneStreamRuntimeClient, readEndpoint = applicationDaemonEndpoint, server, liveSessionId) {
6
7
  if (!daemon.authToken)
7
8
  return null;
8
9
  const ownerToken = daemon.authToken;
@@ -21,7 +22,7 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
21
22
  const localSocketOrigin = new URL(baseUrl);
22
23
  localSocketOrigin.protocol = "ws:";
23
24
  const identity = Object.freeze({
24
- daemonInstanceId: daemon.instanceId,
25
+ daemonInstanceId: server?.generation ?? daemon.instanceId,
25
26
  workspaceName,
26
27
  sessionName,
27
28
  });
@@ -47,11 +48,17 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
47
48
  };
48
49
  return Object.freeze({
49
50
  ...identity,
51
+ ...(liveSessionId ? { liveSessionId } : {}),
50
52
  assertCurrent,
51
53
  readNativeBacking: async (paneId, expected, signal) => {
52
54
  assertCurrent({ ...identity, daemonInstanceId: expected.generation });
53
55
  const result = await readNativeBacking({
54
56
  baseUrl,
57
+ ...(server
58
+ ? {
59
+ resourcePath: `/api/v1/tmux-servers/${server.serverId}/${server.generation}/native-backing/${encodeURIComponent(workspaceName)}/${encodeURIComponent(paneId)}`,
60
+ }
61
+ : {}),
55
62
  ownerToken,
56
63
  workspaceName,
57
64
  paneId,
@@ -66,7 +73,30 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
66
73
  if (options.stream.workspaceName !== identity.workspaceName) {
67
74
  throw new Error("Pane-stream request escaped its verified workspace route");
68
75
  }
69
- return await openClient({
76
+ const openScoped = async (scopedOptions) => {
77
+ const client = createTmuxServerClient({
78
+ baseUrl,
79
+ ownerToken,
80
+ hostClientId: scopedOptions.hostClientId,
81
+ origin: scopedOptions.origin,
82
+ fetch: scopedOptions.fetch,
83
+ }, server);
84
+ try {
85
+ if (scopedOptions.signal?.aborted)
86
+ throw scopedOptions.signal.reason;
87
+ const issued = await client.issuePaneStream(scopedOptions.requestId, scopedOptions.stream, liveSessionId);
88
+ assertCurrent(expected);
89
+ if (scopedOptions.signal?.aborted)
90
+ throw scopedOptions.signal.reason;
91
+ if (issued.status === "error")
92
+ throw new Error(issued.error.reason);
93
+ return await connectIssuedPaneStreamRuntimeClient(scopedOptions, issued.descriptor);
94
+ }
95
+ finally {
96
+ client.dispose();
97
+ }
98
+ };
99
+ return await (server ? openScoped : openClient)({
70
100
  ...options,
71
101
  baseUrl,
72
102
  ownerToken,