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,114 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { DAEMON_WIRE_PROTOCOL_VERSION } from "@tmux-ide/contracts";
3
+ import { createScopedTmuxServerTransport } from "./scoped-tmux-server-transport.ts";
4
+ const scope = {
5
+ serverId: `tmux-server.${"a".repeat(32)}`,
6
+ generation: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
7
+ };
8
+ const target = {
9
+ workspaceName: "main",
10
+ daemon: {
11
+ protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
12
+ productVersion: "2.9.0",
13
+ instanceId: scope.generation,
14
+ startedAt: "2026-09-23T10:00:00.000Z",
15
+ },
16
+ };
17
+ const inventory = {
18
+ workspaceName: "main",
19
+ workspaceId: "workspace.main",
20
+ sessionId: "session.abcdefghijklmnop",
21
+ semanticPaneIds: ["pane.one"],
22
+ resourceRevision: 0,
23
+ };
24
+ function fixture() {
25
+ let stream!: ReadableStreamDefaultController<Uint8Array>;
26
+ const body = new ReadableStream<Uint8Array>({
27
+ start(controller) {
28
+ stream = controller;
29
+ },
30
+ });
31
+ let inventoryReads = 0;
32
+ const pending: (() => void)[] = [];
33
+ const transport = createScopedTmuxServerTransport({
34
+ scope,
35
+ target,
36
+ sessionName: "main",
37
+ liveSessionId: `live-session.${"a".repeat(20)}`,
38
+ clientOptions: {
39
+ baseUrl: "http://localhost:4000",
40
+ ownerToken: "owner",
41
+ origin: "http://localhost",
42
+ hostClientId: "client",
43
+ timeoutMs: 50,
44
+ fetch: (async (input: RequestInfo | URL) => {
45
+ const path = new URL(String(input));
46
+ expect(path.pathname).toStartWith(
47
+ `/api/v1/tmux-servers/${scope.serverId}/${scope.generation}/`,
48
+ );
49
+ if (path.pathname.includes("session-events")) return new Response(body);
50
+ inventoryReads++;
51
+ await new Promise<void>((resolve) => pending.push(resolve));
52
+ return Response.json({ version: 1, server: scope, resource: inventory });
53
+ }) as typeof fetch,
54
+ },
55
+ });
56
+ const frame = (type: string, revision: number, server = scope) =>
57
+ stream.enqueue(
58
+ new TextEncoder().encode(
59
+ `data: ${JSON.stringify({ version: 1, server, type, revision })}\n\n`,
60
+ ),
61
+ );
62
+ return { transport, frame, pending, reads: () => inventoryReads };
63
+ }
64
+ const turn = () => new Promise((resolve) => setTimeout(resolve, 0));
65
+ describe("scoped terminal transport", () => {
66
+ it("waits for owner subscription barrier and rejects dirty prepared inventory", async () => {
67
+ const f = fixture();
68
+ try {
69
+ const preparation = f.transport.prepareTerminalRuntimeInventory(
70
+ target,
71
+ new AbortController().signal,
72
+ );
73
+ await turn();
74
+ expect(f.reads()).toBe(0);
75
+ f.frame("ready", 0);
76
+ await turn();
77
+ expect(f.reads()).toBe(1);
78
+ f.pending.shift()!();
79
+ const prepared = await preparation;
80
+ f.frame("invalidate", 1);
81
+ await turn();
82
+ expect(prepared.consume()).toBeNull();
83
+ } finally {
84
+ f.transport.disposeEventSupervisor();
85
+ }
86
+ });
87
+ it("aborts preparation while event headers/barrier are pending", async () => {
88
+ const f = fixture();
89
+ const abort = new AbortController();
90
+ const preparing = f.transport.prepareTerminalRuntimeInventory(target, abort.signal);
91
+ abort.abort(new Error("selection changed"));
92
+ await expect(preparing).rejects.toThrow("selection changed");
93
+ expect(f.reads()).toBe(0);
94
+ f.transport.disposeEventSupervisor();
95
+ });
96
+ it("rejects an event from another registered server before reading inventory", async () => {
97
+ const f = fixture();
98
+ const preparing = f.transport.prepareTerminalRuntimeInventory(
99
+ target,
100
+ new AbortController().signal,
101
+ );
102
+ f.frame("ready", 0, { ...scope, serverId: `tmux-server.${"b".repeat(32)}` });
103
+ await expect(preparing).rejects.toThrow("generation mismatch");
104
+ expect(f.reads()).toBe(0);
105
+ f.transport.disposeEventSupervisor();
106
+ });
107
+ it("bounds a missing subscription barrier", async () => {
108
+ const f = fixture();
109
+ await expect(
110
+ f.transport.prepareTerminalRuntimeInventory(target, new AbortController().signal),
111
+ ).rejects.toThrow("deadline");
112
+ f.transport.disposeEventSupervisor();
113
+ });
114
+ });
@@ -0,0 +1,385 @@
1
+ import {
2
+ ActionContractsZ,
3
+ ApplicationShellProjectionInputV2SchemaZ,
4
+ DesktopApplicationShellTargetSchemaZ,
5
+ TerminalRuntimeInventoryProjectionV1SchemaZ,
6
+ TmuxServerScopeSchemaZ,
7
+ WorkspaceCatalogResourceV2SchemaZ,
8
+ WorkspaceMultiplexerIntentSchemaZ,
9
+ WorkspacePaneCreateArgumentsSchemaZ,
10
+ type ActionResult,
11
+ type DesktopApplicationShellTarget,
12
+ type TerminalRuntimeInventoryProjectionV1,
13
+ type TmuxServerScope,
14
+ } from "@tmux-ide/contracts";
15
+ import type { TerminalFirstDaemonTransport } from "./direct-application-shell-transport.ts";
16
+ import type { ApplicationShellEventHandlers } from "./application-shell-session.ts";
17
+ import type { PreparedTerminalRuntimeInventory } from "./workspace-event-supervisor.ts";
18
+ import type {
19
+ WorkspaceClientCatalogPort,
20
+ WorkspaceClientOwnerActionPort,
21
+ } from "./workspace-client-types.ts";
22
+ import { createTmuxServerClient, type TmuxServerClientOptions } from "./tmux-server-client.ts";
23
+
24
+ export interface ScopedTmuxServerTransportOptions {
25
+ readonly clientOptions: TmuxServerClientOptions;
26
+ readonly scope: TmuxServerScope;
27
+ readonly target: DesktopApplicationShellTarget;
28
+ readonly sessionName: string;
29
+ readonly liveSessionId: string;
30
+ }
31
+
32
+ function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
33
+ signal.throwIfAborted();
34
+ return new Promise((resolve, reject) => {
35
+ const abort = () => reject(signal.reason);
36
+ signal.addEventListener("abort", abort, { once: true });
37
+ void work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
38
+ });
39
+ }
40
+
41
+ /** One selected incarnation and live session. No endpoint in this adapter can fall back to default. */
42
+ export function createScopedTmuxServerTransport(
43
+ options: ScopedTmuxServerTransportOptions,
44
+ ): TerminalFirstDaemonTransport {
45
+ const scope = TmuxServerScopeSchemaZ.parse(options.scope);
46
+ const target = DesktopApplicationShellTargetSchemaZ.parse(options.target);
47
+ if (target.daemon.instanceId !== scope.generation)
48
+ throw new Error("Scoped owner generation mismatch");
49
+ const base = `/api/v1/tmux-servers/${scope.serverId}/${scope.generation}`;
50
+ const fetcher = options.clientOptions.fetch ?? fetch;
51
+ const lifetime = new AbortController();
52
+ let retired = false;
53
+ let revision = 0;
54
+ let ready: Promise<void> | null = null;
55
+ let sink: ((resource: TerminalRuntimeInventoryProjectionV1) => void) | null = null;
56
+ const handlers = new Set<ApplicationShellEventHandlers>();
57
+ const catalogListeners = new Set<() => void>();
58
+ let refreshing = false;
59
+ let refreshAgain = false;
60
+ const validateTarget = (input: unknown): DesktopApplicationShellTarget => {
61
+ const candidate = DesktopApplicationShellTargetSchemaZ.parse(input);
62
+ if (
63
+ retired ||
64
+ candidate.workspaceName !== target.workspaceName ||
65
+ candidate.daemon.instanceId !== scope.generation ||
66
+ candidate.daemon.protocolVersion !== target.daemon.protocolVersion ||
67
+ candidate.daemon.productVersion !== target.daemon.productVersion ||
68
+ candidate.daemon.startedAt !== target.daemon.startedAt
69
+ )
70
+ throw new Error("Retired or mismatched scoped target");
71
+ return candidate;
72
+ };
73
+ const checkEnvelope = (raw: unknown): Record<string, unknown> => {
74
+ if (
75
+ !raw ||
76
+ typeof raw !== "object" ||
77
+ !("version" in raw) ||
78
+ raw.version !== 1 ||
79
+ !("server" in raw)
80
+ )
81
+ throw new Error("Invalid scoped response");
82
+ const candidate = TmuxServerScopeSchemaZ.parse(raw.server);
83
+ if (
84
+ retired ||
85
+ candidate.serverId !== scope.serverId ||
86
+ candidate.generation !== scope.generation
87
+ )
88
+ throw new Error("Scoped response generation mismatch");
89
+ return raw as Record<string, unknown>;
90
+ };
91
+ const url = (path: string) => new URL(`${base}/${path}`, options.clientOptions.baseUrl);
92
+ const headers = { Authorization: `Bearer ${options.clientOptions.ownerToken}` };
93
+ const request = async (path: string, signal: AbortSignal) => {
94
+ validateTarget(target);
95
+ const response = await fetcher(url(path), {
96
+ headers,
97
+ redirect: "error",
98
+ cache: "no-store",
99
+ signal: AbortSignal.any([
100
+ signal,
101
+ lifetime.signal,
102
+ AbortSignal.timeout(options.clientOptions.timeoutMs ?? 5000),
103
+ ]),
104
+ });
105
+ if (!response.ok) throw new Error(`Scoped resource unavailable (${response.status})`);
106
+ return checkEnvelope(await response.json());
107
+ };
108
+ const workspacePath = (kind: string) =>
109
+ `${kind}/${encodeURIComponent(target.workspaceName)}?liveSessionId=${encodeURIComponent(options.liveSessionId)}`;
110
+ const readInventory = async (signal: AbortSignal) => {
111
+ const raw = await request(workspacePath("inventory"), signal);
112
+ const resource = TerminalRuntimeInventoryProjectionV1SchemaZ.parse(raw.resource);
113
+ if (resource.workspaceName !== target.workspaceName)
114
+ throw new Error("Scoped workspace mismatch");
115
+ return resource;
116
+ };
117
+ const refresh = () => {
118
+ if (!sink || retired) return;
119
+ if (refreshing) {
120
+ refreshAgain = true;
121
+ return;
122
+ }
123
+ refreshing = true;
124
+ void (async () => {
125
+ do {
126
+ refreshAgain = false;
127
+ const observed = revision;
128
+ const resource = await readInventory(lifetime.signal);
129
+ if (retired) return;
130
+ if (observed !== revision) {
131
+ refreshAgain = true;
132
+ continue;
133
+ }
134
+ sink?.({ ...resource, resourceRevision: revision });
135
+ } while (refreshAgain && !retired);
136
+ })()
137
+ .catch((error: unknown) => {
138
+ if (!retired) for (const listener of handlers) listener.onError(String(error));
139
+ })
140
+ .finally(() => {
141
+ refreshing = false;
142
+ });
143
+ };
144
+ const ensureEvents = (): Promise<void> => {
145
+ if (ready) return ready;
146
+ let resolveReady!: () => void;
147
+ let rejectReady!: (error: unknown) => void;
148
+ ready = new Promise<void>((resolve, reject) => {
149
+ resolveReady = resolve;
150
+ rejectReady = reject;
151
+ });
152
+ void ready.catch(() => undefined);
153
+ const deadline = setTimeout(() => {
154
+ rejectReady(new Error("Scoped subscription deadline exceeded"));
155
+ lifetime.abort();
156
+ }, options.clientOptions.timeoutMs ?? 5000);
157
+ void ready.then(
158
+ () => clearTimeout(deadline),
159
+ () => clearTimeout(deadline),
160
+ );
161
+ void (async () => {
162
+ const response = await fetcher(url(workspacePath("session-events")), {
163
+ headers,
164
+ redirect: "error",
165
+ cache: "no-store",
166
+ signal: lifetime.signal,
167
+ });
168
+ if (!response.ok || !response.body)
169
+ throw new Error(`Scoped event stream unavailable (${response.status})`);
170
+ const reader = response.body.getReader();
171
+ const decoder = new TextDecoder();
172
+ let buffer = "";
173
+ let opened = false;
174
+ try {
175
+ for (;;) {
176
+ const next = await reader.read();
177
+ if (next.done) throw new Error("Scoped event stream closed");
178
+ buffer += decoder.decode(next.value, { stream: true });
179
+ if (buffer.length > 65536) throw new Error("Scoped event frame too large");
180
+ for (;;) {
181
+ const end = buffer.indexOf("\n\n");
182
+ if (end < 0) break;
183
+ const event = buffer.slice(0, end);
184
+ buffer = buffer.slice(end + 2);
185
+ const data = event
186
+ .split("\n")
187
+ .filter((line) => line.startsWith("data:"))
188
+ .map((line) => line.slice(5).trimStart())
189
+ .join("\n");
190
+ if (!data) continue;
191
+ const frame = checkEnvelope(JSON.parse(data));
192
+ if (
193
+ !Number.isSafeInteger(frame.revision) ||
194
+ Number(frame.revision) < revision ||
195
+ !["ready", "invalidate", "heartbeat"].includes(String(frame.type))
196
+ )
197
+ throw new Error("Invalid scoped event frame");
198
+ if (!opened) {
199
+ if (frame.type !== "ready") throw new Error("Missing scoped subscription barrier");
200
+ opened = true;
201
+ revision = Number(frame.revision);
202
+ resolveReady();
203
+ } else if (frame.type === "ready")
204
+ throw new Error("Repeated scoped subscription barrier");
205
+ if (frame.type === "invalidate") {
206
+ revision = Number(frame.revision);
207
+ for (const listener of handlers) listener.onInvalidate();
208
+ for (const invalidate of catalogListeners) invalidate();
209
+ refresh();
210
+ }
211
+ }
212
+ }
213
+ } finally {
214
+ await reader.cancel().catch(() => undefined);
215
+ reader.releaseLock();
216
+ }
217
+ })().catch((error: unknown) => {
218
+ rejectReady(error);
219
+ if (retired) return;
220
+ retired = true;
221
+ sink = null;
222
+ lifetime.abort();
223
+ for (const listener of handlers) listener.onPeerMismatch(String(error));
224
+ });
225
+ return ready;
226
+ };
227
+ return {
228
+ validateTarget,
229
+ async fetchApplicationShell(candidate, signal) {
230
+ validateTarget(candidate);
231
+ await abortable(ensureEvents(), signal);
232
+ const raw = await request(workspacePath("application-shell"), signal);
233
+ return ApplicationShellProjectionInputV2SchemaZ.parse(raw.resource);
234
+ },
235
+ connectEvents(candidate, listener) {
236
+ validateTarget(candidate);
237
+ handlers.add(listener);
238
+ void ensureEvents().then(
239
+ () => {
240
+ if (handlers.has(listener) && !retired) listener.onVerifiedOpen();
241
+ },
242
+ () => undefined,
243
+ );
244
+ return {
245
+ close() {
246
+ handlers.delete(listener);
247
+ },
248
+ };
249
+ },
250
+ async prepareTerminalRuntimeInventory(candidate, signal) {
251
+ validateTarget(candidate);
252
+ await abortable(ensureEvents(), signal);
253
+ for (let attempt = 0; attempt < 4; attempt++) {
254
+ signal.throwIfAborted();
255
+ const observed = revision;
256
+ const resource = await readInventory(signal);
257
+ if (observed !== revision) continue;
258
+ let consumed = false;
259
+ return {
260
+ resource,
261
+ consume() {
262
+ if (consumed || retired || signal.aborted || observed !== revision) return null;
263
+ consumed = true;
264
+ return { ...resource, resourceRevision: revision };
265
+ },
266
+ dispose() {
267
+ consumed = true;
268
+ },
269
+ };
270
+ }
271
+ throw new Error("Scoped topology changed during preparation");
272
+ },
273
+ adoptTerminalRuntimeInventory(prepared: PreparedTerminalRuntimeInventory, onResource) {
274
+ const resource = prepared.consume();
275
+ if (resource) sink = onResource;
276
+ return resource;
277
+ },
278
+ refreshTerminalRuntimeInventory: refresh,
279
+ selectApplicationShellFallback() {
280
+ sink = null;
281
+ },
282
+ disposeEventSupervisor() {
283
+ retired = true;
284
+ sink = null;
285
+ handlers.clear();
286
+ catalogListeners.clear();
287
+ lifetime.abort();
288
+ },
289
+ connectWorkspaceCatalog(candidate, invalidate) {
290
+ validateTarget(candidate);
291
+ catalogListeners.add(invalidate);
292
+ return {
293
+ ready: ensureEvents(),
294
+ close() {
295
+ catalogListeners.delete(invalidate);
296
+ },
297
+ };
298
+ },
299
+ };
300
+ }
301
+
302
+ export function createScopedTmuxServerCatalogPort(
303
+ options: ScopedTmuxServerTransportOptions,
304
+ transport: TerminalFirstDaemonTransport,
305
+ ): WorkspaceClientCatalogPort {
306
+ return {
307
+ async read(target, signal) {
308
+ transport.validateTarget(target);
309
+ const barrier = transport.connectWorkspaceCatalog(target, () => undefined);
310
+ try {
311
+ await abortable(barrier.ready, signal);
312
+ } finally {
313
+ barrier.close();
314
+ }
315
+ const { scope, clientOptions } = options;
316
+ const response = await (clientOptions.fetch ?? fetch)(
317
+ new URL(
318
+ `/api/v1/tmux-servers/${scope.serverId}/${scope.generation}/catalog`,
319
+ clientOptions.baseUrl,
320
+ ),
321
+ {
322
+ headers: { Authorization: `Bearer ${clientOptions.ownerToken}` },
323
+ redirect: "error",
324
+ cache: "no-store",
325
+ signal,
326
+ },
327
+ );
328
+ if (!response.ok) throw new Error(`Scoped catalog unavailable (${response.status})`);
329
+ const envelope = (await response.json()) as {
330
+ version: number;
331
+ server: unknown;
332
+ resource: object;
333
+ };
334
+ const actual = TmuxServerScopeSchemaZ.parse(envelope.server);
335
+ if (
336
+ envelope.version !== 1 ||
337
+ actual.serverId !== scope.serverId ||
338
+ actual.generation !== scope.generation
339
+ )
340
+ throw new Error("Scoped catalog mismatch");
341
+ transport.validateTarget(target);
342
+ return WorkspaceCatalogResourceV2SchemaZ.parse({
343
+ ...envelope.resource,
344
+ version: 2,
345
+ daemon: target.daemon,
346
+ });
347
+ },
348
+ subscribe(target, invalidate) {
349
+ return transport.connectWorkspaceCatalog(target, invalidate);
350
+ },
351
+ };
352
+ }
353
+
354
+ export function createScopedTmuxServerOwnerActions(
355
+ options: ScopedTmuxServerTransportOptions,
356
+ transport: TerminalFirstDaemonTransport,
357
+ ): WorkspaceClientOwnerActionPort {
358
+ const client = createTmuxServerClient(options.clientOptions, options.scope);
359
+ return {
360
+ async dispatch(request) {
361
+ transport.validateTarget(request.target);
362
+ if (request.name === "workspace.pane.create") {
363
+ const intent = WorkspacePaneCreateArgumentsSchemaZ.parse(request.input);
364
+ if (intent.workspaceName !== options.target.workspaceName)
365
+ throw new Error("Scoped action workspace mismatch");
366
+ const result = await client.createPane(request.operationId, intent, options.liveSessionId);
367
+ transport.validateTarget(request.target);
368
+ transport.refreshTerminalRuntimeInventory();
369
+ return result as ActionResult<typeof request.name>;
370
+ }
371
+ const intent = WorkspaceMultiplexerIntentSchemaZ.parse({
372
+ ...request.input,
373
+ verb: request.name,
374
+ });
375
+ if (intent.workspaceName !== options.target.workspaceName)
376
+ throw new Error("Scoped action workspace mismatch");
377
+ const result = await client.mutate(request.operationId, intent, options.liveSessionId);
378
+ transport.validateTarget(request.target);
379
+ transport.refreshTerminalRuntimeInventory();
380
+ return ActionContractsZ[request.name].result.parse(result) as ActionResult<
381
+ typeof request.name
382
+ >;
383
+ },
384
+ };
385
+ }