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

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 (173) hide show
  1. package/bin/cli.js +52253 -49070
  2. package/package.json +1 -1
  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/index.ts +2 -0
  15. package/packages/contracts/src/interaction-receipts.ts +39 -0
  16. package/packages/contracts/src/issue-error.ts +1 -1
  17. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  18. package/packages/contracts/src/pane-stream.ts +51 -8
  19. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  20. package/packages/contracts/src/window-links.ts +76 -0
  21. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  22. package/packages/core/src/interaction-receipts.ts +4 -0
  23. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  24. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  25. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  26. package/packages/daemon/dist/command-center/discovery.js +2 -9
  27. package/packages/daemon/dist/command-center/server.js +21 -1
  28. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  29. package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
  30. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  31. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  32. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  33. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  34. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
  35. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  36. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  37. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  38. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  39. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  40. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  41. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  42. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  43. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  44. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  46. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  47. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  48. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  49. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  50. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  51. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  52. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  53. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  54. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  55. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  56. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  57. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  58. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  59. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  60. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  61. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  62. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  63. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  64. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  65. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  66. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  67. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  68. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  70. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  71. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  72. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  73. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  74. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  75. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  76. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  77. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  78. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
  79. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  80. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  81. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  82. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  83. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  84. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  85. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
  86. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  87. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  88. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  89. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  90. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  91. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  92. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  93. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  94. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  95. package/packages/daemon/src/command-center/discovery.ts +2 -10
  96. package/packages/daemon/src/command-center/server.ts +29 -1
  97. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  98. package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
  99. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  100. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  101. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  102. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  103. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
  104. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  105. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  106. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  107. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  108. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  109. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  110. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  111. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  112. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  113. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  114. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  115. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  116. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  117. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  118. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  119. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  120. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  121. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  122. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  123. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  124. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  125. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  126. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  127. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  128. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  129. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  130. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  131. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  132. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  133. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  134. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  135. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  136. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  137. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  138. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  139. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  140. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  141. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  142. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  143. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  144. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  145. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  146. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  147. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  148. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  149. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
  150. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  151. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  152. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  153. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  154. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
  155. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  156. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
  157. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  158. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  159. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  160. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
  161. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  162. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  163. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  164. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  165. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  166. package/packages/daemon-client/package.json +3 -1
  167. package/packages/daemon-client/src/index.ts +2 -0
  168. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  169. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  170. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  171. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  172. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  173. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -0,0 +1,203 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ WINDOW_LINK_MAX_LINKS,
4
+ WindowLinkTargetSchemaZ,
5
+ WindowLinkTopologySchemaZ,
6
+ WorkspaceCatalogLiveSessionIdSchemaZ,
7
+ TerminalAttachmentSemanticWindowIdSchemaZ,
8
+ type WindowLinkTarget,
9
+ type WindowLinkTopology,
10
+ } from "@tmux-ide/contracts";
11
+
12
+ /** Trusted native observation. Native addresses never become public link handles. */
13
+ export interface NativeWindowLinkObservation {
14
+ readonly index: number;
15
+ readonly runtimeWindowId: string;
16
+ readonly semanticWindowId: string;
17
+ readonly active: boolean;
18
+ }
19
+
20
+ export interface ResolvedWindowLink {
21
+ readonly runtimeSessionId: string;
22
+ readonly runtimeWindowId: string;
23
+ readonly index: number;
24
+ }
25
+
26
+ export type WindowLinkRefusal =
27
+ | "window_link_stale"
28
+ | "window_link_session_mismatch"
29
+ | "window_link_backing_mismatch"
30
+ | "window_link_ambiguous";
31
+
32
+ export class WindowLinkResolutionError extends Error {
33
+ constructor(readonly reason: WindowLinkRefusal) {
34
+ super(reason);
35
+ this.name = "WindowLinkResolutionError";
36
+ }
37
+ }
38
+
39
+ interface LinkRecord extends NativeWindowLinkObservation {
40
+ readonly linkId: string;
41
+ }
42
+
43
+ /**
44
+ * One authority per daemon/server/session incarnation. This is an observation
45
+ * model, not an immutable native winlink identity: unobserved unlink/relink ABA
46
+ * is unknowable. Resolved targets still require an execution-time native guard.
47
+ * Not installed into the v1 stream; C2 activates it with the complete link-aware
48
+ * producer/consumer boundary.
49
+ */
50
+ export class WindowLinkAuthority {
51
+ private records = new Map<number, LinkRecord>();
52
+ private revision = 0;
53
+ private topology: WindowLinkTopology | null = null;
54
+ private disposed = false;
55
+
56
+ constructor(
57
+ readonly liveSessionId: string,
58
+ private readonly runtimeSessionId: string,
59
+ ) {
60
+ WorkspaceCatalogLiveSessionIdSchemaZ.parse(liveSessionId);
61
+ if (
62
+ !/^\$(?:0|[1-9]\d{0,15})$/u.test(runtimeSessionId) ||
63
+ !Number.isSafeInteger(Number(runtimeSessionId.slice(1)))
64
+ )
65
+ throw new TypeError("Invalid native session id");
66
+ }
67
+
68
+ /** A malformed fresh observation revokes old actions; no partial topology is published. */
69
+ reconcile(observations: readonly NativeWindowLinkObservation[]): WindowLinkTopology {
70
+ try {
71
+ return this.stageAndPublish(observations);
72
+ } catch (error) {
73
+ this.invalidate();
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ private stageAndPublish(
79
+ observations: readonly NativeWindowLinkObservation[],
80
+ ): WindowLinkTopology {
81
+ if (this.disposed) throw new WindowLinkResolutionError("window_link_stale");
82
+ if (observations.length === 0 || observations.length > WINDOW_LINK_MAX_LINKS)
83
+ throw new TypeError("Invalid window link count");
84
+ const indexes = new Set<number>();
85
+ const semanticByRuntime = new Map<string, string>();
86
+ const runtimeBySemantic = new Map<string, string>();
87
+ let activeCount = 0;
88
+ for (const row of observations) {
89
+ if (
90
+ !Number.isSafeInteger(row.index) ||
91
+ row.index < 0 ||
92
+ indexes.has(row.index) ||
93
+ !/^@(?:0|[1-9]\d{0,15})$/u.test(row.runtimeWindowId) ||
94
+ !Number.isSafeInteger(Number(row.runtimeWindowId.slice(1))) ||
95
+ typeof row.active !== "boolean"
96
+ )
97
+ throw new TypeError("Malformed native window link observation");
98
+ TerminalAttachmentSemanticWindowIdSchemaZ.parse(row.semanticWindowId);
99
+ indexes.add(row.index);
100
+ if (row.active) activeCount++;
101
+ const semantic = semanticByRuntime.get(row.runtimeWindowId);
102
+ const runtime = runtimeBySemantic.get(row.semanticWindowId);
103
+ if (
104
+ (semantic !== undefined && semantic !== row.semanticWindowId) ||
105
+ (runtime !== undefined && runtime !== row.runtimeWindowId)
106
+ )
107
+ throw new TypeError("Window backing identity is inconsistent");
108
+ semanticByRuntime.set(row.runtimeWindowId, row.semanticWindowId);
109
+ runtimeBySemantic.set(row.semanticWindowId, row.runtimeWindowId);
110
+ }
111
+ if (activeCount !== 1) throw new TypeError("Expected one active window link");
112
+ const rows = [...observations].sort((left, right) => left.index - right.index);
113
+ const changed =
114
+ rows.length !== this.records.size ||
115
+ rows.some((row) => {
116
+ const prior = this.records.get(row.index);
117
+ return (
118
+ prior?.runtimeWindowId !== row.runtimeWindowId ||
119
+ prior?.semanticWindowId !== row.semanticWindowId
120
+ );
121
+ });
122
+ const revision = this.revision + (changed ? 1 : 0);
123
+ if (!Number.isSafeInteger(revision)) throw new RangeError("Window link revision exhausted");
124
+ const next = new Map<number, LinkRecord>();
125
+ for (const row of rows) {
126
+ const prior = this.records.get(row.index);
127
+ const retained =
128
+ prior?.runtimeWindowId === row.runtimeWindowId &&
129
+ prior.semanticWindowId === row.semanticWindowId;
130
+ next.set(row.index, {
131
+ ...row,
132
+ linkId: retained ? prior.linkId : `window-link.${randomUUID().replaceAll("-", "")}`,
133
+ });
134
+ }
135
+ const records = [...next.values()];
136
+ const topology = WindowLinkTopologySchemaZ.parse({
137
+ liveSessionId: this.liveSessionId,
138
+ linkRevision: revision,
139
+ activeLinkId: records.find((row) => row.active)!.linkId,
140
+ links: records.map((row) => ({
141
+ linkId: row.linkId,
142
+ semanticWindowId: row.semanticWindowId,
143
+ displayIndex: row.index,
144
+ })),
145
+ });
146
+ this.records = next;
147
+ this.revision = revision;
148
+ this.topology = topology;
149
+ return this.snapshot()!;
150
+ }
151
+
152
+ snapshot(): WindowLinkTopology | null {
153
+ return this.topology
154
+ ? { ...this.topology, links: this.topology.links.map((link) => ({ ...link })) }
155
+ : null;
156
+ }
157
+
158
+ /** Reconnect/gap/discontinuous observation must retire all previously issued handles. */
159
+ invalidate(): void {
160
+ this.records.clear();
161
+ this.topology = null;
162
+ this.revision++;
163
+ if (!Number.isSafeInteger(this.revision)) this.disposed = true;
164
+ }
165
+
166
+ dispose(): void {
167
+ this.invalidate();
168
+ this.disposed = true;
169
+ }
170
+
171
+ resolve(target: WindowLinkTarget): ResolvedWindowLink {
172
+ WindowLinkTargetSchemaZ.parse(target);
173
+ if (target.liveSessionId !== this.liveSessionId)
174
+ throw new WindowLinkResolutionError("window_link_session_mismatch");
175
+ if (!this.topology || this.disposed || target.linkRevision !== this.revision)
176
+ throw new WindowLinkResolutionError("window_link_stale");
177
+ const record = [...this.records.values()].find((row) => row.linkId === target.linkId);
178
+ if (!record) throw new WindowLinkResolutionError("window_link_stale");
179
+ if (record.semanticWindowId !== target.expectedSemanticWindowId)
180
+ throw new WindowLinkResolutionError("window_link_backing_mismatch");
181
+ return {
182
+ runtimeSessionId: this.runtimeSessionId,
183
+ runtimeWindowId: record.runtimeWindowId,
184
+ index: record.index,
185
+ };
186
+ }
187
+
188
+ /** Legacy backing-only selection is safe only when exactly one link applies. */
189
+ uniqueTargetForBacking(semanticWindowId: string): WindowLinkTarget {
190
+ if (!this.topology || this.disposed) throw new WindowLinkResolutionError("window_link_stale");
191
+ const links = [...this.records.values()].filter(
192
+ (row) => row.semanticWindowId === semanticWindowId,
193
+ );
194
+ if (links.length === 0) throw new WindowLinkResolutionError("window_link_stale");
195
+ if (links.length !== 1) throw new WindowLinkResolutionError("window_link_ambiguous");
196
+ return {
197
+ liveSessionId: this.liveSessionId,
198
+ linkId: links[0]!.linkId,
199
+ expectedSemanticWindowId: semanticWindowId,
200
+ linkRevision: this.revision,
201
+ };
202
+ }
203
+ }
@@ -11,7 +11,7 @@ import {
11
11
  * PaneStreamLeaseManager — in-memory authority for pane-stream leases (m43
12
12
  * card 2), following the terminal-attachment lease discipline:
13
13
  *
14
- * - a one-time `ps1_` bearer ticket whose SHA-256 digest is all the manager
14
+ * - a one-time `ps2_` bearer ticket whose SHA-256 digest is all the manager
15
15
  * retains, burned before activation so no concurrent caller can replay it;
16
16
  * - the ticket TTL bounds credential DELIVERY (the authenticated redemption
17
17
  * frame must arrive in time); execution after delivery gets its own bounded
@@ -36,7 +36,7 @@ const SessionNameSchemaZ = z
36
36
  .min(1)
37
37
  .max(256)
38
38
  .refine((value) => !/[\0\r\n]/u.test(value));
39
- const TicketPattern = /^ps1_[A-Za-z0-9_-]{43}$/u;
39
+ const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
40
40
 
41
41
  export interface PaneStreamLeaseBinding {
42
42
  readonly daemonInstanceId: string;
@@ -197,7 +197,7 @@ export class PaneStreamLeaseManager {
197
197
  "The secure random source returned an invalid ticket.",
198
198
  );
199
199
  }
200
- const redemptionTicket = `ps1_${Buffer.from(ticketBytes).toString("base64url")}`;
200
+ const redemptionTicket = `ps2_${Buffer.from(ticketBytes).toString("base64url")}`;
201
201
  const state: LeaseState = {
202
202
  leaseId,
203
203
  requestId,
@@ -1,3 +1,4 @@
1
+ import { PaneStreamLayoutSnapshotFrameSchemaZ, type WindowLinkTopology } from "@tmux-ide/contracts";
1
2
  import { z } from "zod";
2
3
  import {
3
4
  PANE_STREAM_MAX_HELD_DELTAS,
@@ -72,7 +73,7 @@ import {
72
73
  } from "./wire-ledger.ts";
73
74
 
74
75
  /**
75
- * The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `ps1_`
76
+ * The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `ps2_`
76
77
  * ticket and bridges MirrorService subscriptions to wire frames. Admission is
77
78
  * the direct-websocket discipline verbatim — Origin-gated upgrade reservation,
78
79
  * digest-only pending tickets, one text redemption frame, delivery-bound TTL —
@@ -126,7 +127,7 @@ function hasCanonicalInputFramePrefix(raw: Buffer): boolean {
126
127
  startsWithBuffer(raw, TYPE_FIRST_INPUT_FRAME_PREFIX)
127
128
  );
128
129
  }
129
- const TicketPattern = /^ps1_[A-Za-z0-9_-]{43}$/u;
130
+ const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
130
131
  const BindingIdSchemaZ = z
131
132
  .string()
132
133
  .min(1)
@@ -593,7 +594,7 @@ export class PaneStreamAdmissionCoordinator {
593
594
  if (this.#shuttingDown) {
594
595
  return { accepted: false, code: "daemon-shutting-down", httpStatus: 503 };
595
596
  }
596
- if (input.path !== PANE_STREAM_REDEEM_PATH) {
597
+ if (input.path !== new URL(this.#webSocketUrl).pathname) {
597
598
  return { accepted: false, code: "invalid-path", httpStatus: 404 };
598
599
  }
599
600
  if (input.protocols.length !== 1 || input.protocols[0] !== PANE_STREAM_WEBSOCKET_SUBPROTOCOL) {
@@ -1069,6 +1070,7 @@ export class PaneStreamLiveConnection {
1069
1070
  readonly #semanticLayouts = new Map<string, MirrorLayoutEvent>();
1070
1071
  #semanticExpectedPaneIds: readonly string[] | null = null;
1071
1072
  #semanticRuntimeSessionId: string | null = null;
1073
+ #semanticWindowLinks: WindowLinkTopology | null = null;
1072
1074
  #semanticTopologyEpoch = -1;
1073
1075
  readonly #sendQueue: QueuedSend[] = [];
1074
1076
  readonly #semanticDrainWaiters = new Map<string, Array<() => void>>();
@@ -1411,7 +1413,15 @@ export class PaneStreamLiveConnection {
1411
1413
  authority.topologyEpoch < 0
1412
1414
  ? null
1413
1415
  : this.#validateInitialLayout(authority.layouts, expectedPaneIds);
1414
- if (!stagedInitialFrames) {
1416
+ if (
1417
+ !stagedInitialFrames ||
1418
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
1419
+ type: "layout-snapshot",
1420
+ topologyEpoch: authority?.topologyEpoch,
1421
+ layouts: stagedInitialFrames,
1422
+ windowLinks: authority?.windowLinks,
1423
+ }).success
1424
+ ) {
1415
1425
  await subscription.close().catch(() => undefined);
1416
1426
  this.#failTopologyChanged();
1417
1427
  return;
@@ -1513,7 +1523,15 @@ export class PaneStreamLiveConnection {
1513
1523
  authority.topologyEpoch < 0
1514
1524
  ? null
1515
1525
  : this.#validateInitialLayout(authority.layouts, expectedPaneIds);
1516
- if (!initialFrames) {
1526
+ if (
1527
+ !initialFrames ||
1528
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
1529
+ type: "layout-snapshot",
1530
+ topologyEpoch: authority?.topologyEpoch,
1531
+ layouts: initialFrames,
1532
+ windowLinks: authority?.windowLinks,
1533
+ }).success
1534
+ ) {
1517
1535
  await Promise.all(opened.map(({ delivery }) => delivery.close().catch(() => undefined)));
1518
1536
  this.#failTopologyChanged();
1519
1537
  return;
@@ -1521,6 +1539,7 @@ export class PaneStreamLiveConnection {
1521
1539
  this.#recordDiagnosticLifecycle("pane-stream-layout-validated");
1522
1540
  this.#semanticExpectedPaneIds = expectedPaneIds;
1523
1541
  this.#semanticRuntimeSessionId = authority!.runtimeSessionId;
1542
+ this.#semanticWindowLinks = authority!.windowLinks;
1524
1543
  this.#semanticTopologyEpoch = authority!.topologyEpoch;
1525
1544
  this.#semanticLayouts.clear();
1526
1545
  for (const event of authority!.layouts)
@@ -1529,6 +1548,7 @@ export class PaneStreamLiveConnection {
1529
1548
  type: "layout-snapshot",
1530
1549
  topologyEpoch: authority!.topologyEpoch,
1531
1550
  layouts: initialFrames,
1551
+ windowLinks: authority!.windowLinks,
1532
1552
  });
1533
1553
  layoutActivated = true;
1534
1554
  this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
@@ -1854,10 +1874,33 @@ export class PaneStreamLiveConnection {
1854
1874
  return;
1855
1875
  }
1856
1876
  const frames = this.#validateInitialLayout(snapshot.layouts, this.#semanticExpectedPaneIds);
1857
- if (!frames) {
1877
+ if (
1878
+ !frames ||
1879
+ !PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
1880
+ type: "layout-snapshot",
1881
+ topologyEpoch: snapshot.topologyEpoch,
1882
+ layouts: frames,
1883
+ windowLinks: snapshot.windowLinks,
1884
+ }).success
1885
+ ) {
1886
+ this.#failTopologyChanged();
1887
+ return;
1888
+ }
1889
+ const previousLinks = this.#semanticWindowLinks;
1890
+ if (
1891
+ !previousLinks ||
1892
+ snapshot.windowLinks.liveSessionId !== previousLinks.liveSessionId ||
1893
+ snapshot.windowLinks.linkRevision < previousLinks.linkRevision ||
1894
+ (snapshot.windowLinks.linkRevision === previousLinks.linkRevision &&
1895
+ JSON.stringify(
1896
+ [...snapshot.windowLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId)),
1897
+ ) !==
1898
+ JSON.stringify([...previousLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId))))
1899
+ ) {
1858
1900
  this.#failTopologyChanged();
1859
1901
  return;
1860
1902
  }
1903
+ this.#semanticWindowLinks = snapshot.windowLinks;
1861
1904
  this.#semanticTopologyEpoch = snapshot.topologyEpoch;
1862
1905
  this.#semanticLayouts.clear();
1863
1906
  for (const event of snapshot.layouts)
@@ -1866,6 +1909,7 @@ export class PaneStreamLiveConnection {
1866
1909
  type: "layout-snapshot",
1867
1910
  topologyEpoch: snapshot.topologyEpoch,
1868
1911
  layouts: frames,
1912
+ windowLinks: snapshot.windowLinks,
1869
1913
  });
1870
1914
  }
1871
1915
 
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /** Shared opaque incarnation identity for catalog and terminal authority. */
4
+ export function liveSessionIdForNativeIdentity(
5
+ serverPid: string,
6
+ sessionId: string,
7
+ sessionCreated: string,
8
+ ) {
9
+ const digest = createHash("sha256")
10
+ .update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
11
+ .digest("hex")
12
+ .slice(0, 20);
13
+ return `live-session.${digest}` as const;
14
+ }
@@ -15,6 +15,8 @@ export type ReadNativeBacking = (
15
15
  /** One bounded read on entering scrollback. No captures are issued by wheel movement. */
16
16
  export async function readNativeBacking(options: {
17
17
  baseUrl: string;
18
+ /** Trusted host-selected owner route; credentials and proof remain unchanged. */
19
+ resourcePath?: string;
18
20
  ownerToken: string;
19
21
  workspaceName: string;
20
22
  paneId: string;
@@ -22,7 +24,8 @@ export async function readNativeBacking(options: {
22
24
  signal: AbortSignal;
23
25
  }): Promise<NativeGridCapture | null> {
24
26
  const url = new URL(
25
- `/api/project/${encodeURIComponent(options.workspaceName)}/terminal-native-backing/${encodeURIComponent(options.paneId)}`,
27
+ options.resourcePath ??
28
+ `/api/project/${encodeURIComponent(options.workspaceName)}/terminal-native-backing/${encodeURIComponent(options.paneId)}`,
26
29
  options.baseUrl,
27
30
  );
28
31
  for (const [key, value] of Object.entries(options.expected))
@@ -22,6 +22,12 @@ export function sessionRuntimeInteractionFacts(
22
22
  target: { kind: "pane", semanticPaneId: intent.semanticPaneId },
23
23
  summary: { operationKind: intent.verb, direction: intent.direction },
24
24
  };
25
+ case "workspace.window.link.select":
26
+ case "workspace.window.link.unlink":
27
+ return {
28
+ target: { kind: "window-link", target: intent.target },
29
+ summary: { operationKind: intent.verb },
30
+ };
25
31
  case "workspace.window.kill":
26
32
  return {
27
33
  target: { kind: "window", target: intent.target },
@@ -103,6 +109,9 @@ export function sessionRuntimeObservedProof(
103
109
  if (result.verb !== intent.verb)
104
110
  throw new TypeError("Mutation result verb does not match intent");
105
111
  switch (result.verb) {
112
+ case "workspace.window.link.select":
113
+ case "workspace.window.link.unlink":
114
+ return { operationKind: result.verb, outcome: result.outcome, target: result.target };
106
115
  case "workspace.window.split":
107
116
  return {
108
117
  operationKind: result.verb,
@@ -550,6 +550,15 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
550
550
  );
551
551
  }
552
552
 
553
+ /** Called by the serialized semantic mutation executor, never by a transport directly. */
554
+ executeWindowLinkAction(
555
+ session: string,
556
+ request: Parameters<MirrorService["executeWindowLinkAction"]>[1],
557
+ ): ReturnType<MirrorService["executeWindowLinkAction"]> {
558
+ if (this.#disposed) return Promise.reject(new Error("Session runtime disposed"));
559
+ return this.#mirror.executeWindowLinkAction(session, request);
560
+ }
561
+
553
562
  /** Retire one no-longer-registered session without disturbing siblings. */
554
563
  async retireSession(session: string): Promise<void> {
555
564
  const runtime = this.#sessions.get(session);
@@ -56,7 +56,7 @@ export interface SessionSemanticMutationExecutorOptions {
56
56
  nowMicros(): number;
57
57
  record(operation: string, startedAtMicros: number, endedAtMicros: number): void;
58
58
  }>,
59
- ) => SessionRuntimeIntentResult;
59
+ ) => SessionRuntimeIntentResult | Promise<SessionRuntimeIntentResult>;
60
60
  /** Publishes through the daemon's existing replayable interaction journal. */
61
61
  readonly publishReceipt: (receipt: SessionRuntimeReceiptInput) => InteractionReceipt;
62
62
  /** Publishes the same replayable invalidation contract as HTTP semantic actions. */
@@ -416,7 +416,7 @@ export class SessionSemanticMutationExecutor {
416
416
  // Admission can wait behind prior work. Revalidate the opaque principal
417
417
  // at the last synchronous boundary before tmux receives any effect.
418
418
  authorizeBeforeEffect?.();
419
- result = this.#options.execute(operationId, intent, timing);
419
+ result = await this.#options.execute(operationId, intent, timing);
420
420
  } catch (cause) {
421
421
  if (needsTmuxObservation) this.#deletePending(session, operationId);
422
422
  const error = new SessionRuntimeIntentError(
@@ -32,6 +32,7 @@ export function semanticMutationResourceChanges(
32
32
  if (
33
33
  result.verb === "workspace.window.split" ||
34
34
  result.verb === "workspace.window.kill" ||
35
+ result.verb === "workspace.window.link.unlink" ||
35
36
  result.verb === "workspace.pane.kill" ||
36
37
  result.verb === "workspace.session.kill"
37
38
  ) {
@@ -1,12 +1,16 @@
1
+ import { prepareScopedOpenTuiConnection } from "./application-shell-server-connection.ts";
1
2
  import { OpenTuiStartupError, startupFailureFromError } from "./startup-failure.ts";
2
3
  import type { OpenTuiSessionWorkspaceEnsureResult } from "./configless-session-bootstrap.ts";
3
4
  import {
5
+ applicationDaemonEndpoint,
6
+ type ApplicationDaemonEndpoint,
4
7
  readApplicationDaemonInfo as readCanonicalDaemonInfo,
5
8
  isApplicationDaemonAlive as isCanonicalDaemonAlive,
6
9
  } from "./runtime/application-daemon-authority.ts";
7
10
  import {
8
11
  APPLICATION_SHELL_RESOURCE_V2_VERSION,
9
12
  type CanonicalDaemonInfo,
13
+ type TmuxServerScope,
10
14
  type DesktopApplicationShellTarget,
11
15
  type DesktopDaemonHostDescriptor,
12
16
  type WorkspaceCatalogResourceV2,
@@ -18,7 +22,10 @@ import {
18
22
  type TerminalFirstDaemonTransport,
19
23
  } from "@tmux-ide/daemon-client/direct-application-shell-transport";
20
24
  import type { PreparedTerminalRuntimeInventory } from "@tmux-ide/daemon-client/workspace-event-supervisor";
21
- import type { WorkspaceClientCatalogPort } from "@tmux-ide/daemon-client/workspace-client-types";
25
+ import type {
26
+ WorkspaceClientCatalogPort,
27
+ WorkspaceClientOwnerActionPort,
28
+ } from "@tmux-ide/daemon-client/workspace-client-types";
22
29
  import { WebSocket } from "ws";
23
30
 
24
31
  import { canonicalDaemonUrl } from "../../lib/canonical-daemon.ts";
@@ -41,6 +48,8 @@ import {
41
48
  */
42
49
  export interface OpenTuiApplicationShellConnection {
43
50
  readonly workspaceName: string;
51
+ readonly server?: TmuxServerScope;
52
+ readonly ownerActions?: WorkspaceClientOwnerActionPort;
44
53
  /** Incarnation captured by the connection's routing read, never copied from a later catalog. */
45
54
  readonly liveSessionId?: string | null;
46
55
  readonly target: DesktopApplicationShellTarget;
@@ -53,6 +62,9 @@ export interface OpenTuiApplicationShellConnection {
53
62
  }
54
63
 
55
64
  export interface OpenTuiApplicationShellConnectionDependencies {
65
+ readonly server?: TmuxServerScope;
66
+ readonly readDaemonEndpoint?: () => ApplicationDaemonEndpoint;
67
+ readonly expectedLiveSessionId?: string;
56
68
  readonly readCanonicalDaemonInfo: () => CanonicalDaemonInfo | null;
57
69
  readonly isCanonicalDaemonAlive: (daemon: CanonicalDaemonInfo) => Promise<boolean>;
58
70
  readonly fetchCanonicalWorkspaceRouting: (
@@ -171,7 +183,15 @@ export async function resolveOpenTuiApplicationShellConnection(
171
183
  applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
172
184
  ...(diagnose ? { terminalRuntimeDiagnostic: diagnose } : {}),
173
185
  });
174
- const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName);
186
+ const routing = createOpenTuiVerifiedRoutingContext(
187
+ daemon,
188
+ workspaceName,
189
+ sessionName,
190
+ undefined,
191
+ dependencies.readDaemonEndpoint ?? applicationDaemonEndpoint,
192
+ undefined,
193
+ liveSessionId ?? undefined,
194
+ );
175
195
  let disposed = false;
176
196
  let diagnosticClientFetchOrdinal = 0;
177
197
  let diagnosticPrewarmOrdinal = 0;
@@ -368,6 +388,12 @@ export async function prepareOpenTuiApplicationShellConnection(
368
388
  ): Promise<OpenTuiApplicationShellConnection | null> {
369
389
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
370
390
  try {
391
+ if (dependencies.server) {
392
+ const unscoped = { ...dependencies, server: undefined };
393
+ return await prepareScopedOpenTuiConnection(sessionName, dependencies, () =>
394
+ prepareOpenTuiApplicationShellConnection(sessionName, unscoped),
395
+ );
396
+ }
371
397
  const result = await dependencies.ensureSessionWorkspace(sessionName);
372
398
  if (result === false) return null;
373
399
  if (typeof result !== "boolean" && result.status === "unavailable") {