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,101 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "@tmux-ide/contracts";
3
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
4
+ import { fleetSessionIdForName } from "../command-center/resources/fleet-catalog.js";
5
+ import { isVisibleFleetSession } from "../command-center/discovery.js";
6
+ import { WorkspacePromotionAuthority } from "./workspace-promotion.js";
7
+ import { shellEscape } from "./shell.js";
8
+ /** Owner-local bounded admission; all native IO must already fence the server generation. */
9
+ export function createNativeTmuxSessionOpener(options) {
10
+ let disposed = false;
11
+ let tail = Promise.resolve();
12
+ const pending = new Map();
13
+ const assertOpen = () => {
14
+ options.assertOpen();
15
+ if (disposed)
16
+ throw new Error("Session admission is retired");
17
+ };
18
+ const open = async (liveSessionId) => {
19
+ assertOpen();
20
+ const records = await options.run([
21
+ "list-sessions",
22
+ "-F",
23
+ "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}",
24
+ ]);
25
+ const matches = records
26
+ .trim()
27
+ .split("\n")
28
+ .map((line) => line.split("\t"))
29
+ .filter(([pid, id, created, name]) => pid &&
30
+ /^\d+$/.test(pid) &&
31
+ id &&
32
+ /^\$\d+$/.test(id) &&
33
+ created &&
34
+ /^\d+$/.test(created) &&
35
+ name &&
36
+ isVisibleFleetSession(name) &&
37
+ liveSessionIdForNativeIdentity(pid, id, created) === liveSessionId);
38
+ if (matches.length !== 1)
39
+ throw new Error("Selected live tmux session is no longer available");
40
+ const [, nativeSessionId, created, name] = matches[0];
41
+ // Each command checks the exact native session on the server accepting it.
42
+ // A name-only validation followed by stamping would cross a session replacement.
43
+ const run = async (args) => {
44
+ assertOpen();
45
+ const sentinel = `tmux-session-stale.${randomUUID()}`;
46
+ const condition = `#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_created},${created}}}`;
47
+ const command = args.map((arg) => (arg === ";" ? ";" : shellEscape(arg))).join(" ");
48
+ const output = await options.run([
49
+ "if-shell",
50
+ "-t",
51
+ nativeSessionId,
52
+ "-F",
53
+ condition,
54
+ command,
55
+ `display-message -p '${sentinel}'`,
56
+ ]);
57
+ assertOpen();
58
+ if (output.trim() === sentinel)
59
+ throw new Error("Selected live tmux session changed during admission");
60
+ return output;
61
+ };
62
+ const promotion = new WorkspacePromotionAuthority({
63
+ daemonInstanceId: options.generation,
64
+ registry: options.registry,
65
+ io: { runTmux: run },
66
+ });
67
+ try {
68
+ const result = await promotion.promote({
69
+ operationId: randomUUID(),
70
+ expectedDaemonInstanceId: options.generation,
71
+ intent: { sessionId: fleetSessionIdForName(name) },
72
+ });
73
+ await run(["display-message", "-p", "-t", nativeSessionId, "#{session_id}"]);
74
+ return { workspaceName: result.resource.workspaceName, liveSessionId };
75
+ }
76
+ finally {
77
+ await promotion.dispose();
78
+ }
79
+ };
80
+ return {
81
+ openSession(liveSessionId) {
82
+ WorkspaceCatalogLiveSessionIdSchemaZ.parse(liveSessionId);
83
+ assertOpen();
84
+ const previous = pending.get(liveSessionId);
85
+ if (previous)
86
+ return previous;
87
+ if (pending.size >= 16)
88
+ throw new Error("Session admission capacity reached");
89
+ const operation = tail
90
+ .then(() => open(liveSessionId))
91
+ .finally(() => pending.delete(liveSessionId));
92
+ pending.set(liveSessionId, operation);
93
+ tail = operation.catch(() => { });
94
+ return operation;
95
+ },
96
+ async dispose() {
97
+ disposed = true;
98
+ await tail;
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,134 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { SavedMachineSchema, TmuxServerIdSchemaZ, TmuxServerRegistrationRequestSchemaZ, WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
3
+ import { createTmuxServerClient, listTmuxServers, registerTmuxServer, removeTmuxServer, TmuxServerClientError, } from "@tmux-ide/daemon-client/tmux-server-client";
4
+ import { canonicalDaemonUrl } from "./canonical-daemon.js";
5
+ import { ensureCanonicalDaemon } from "./canonical-daemon-bootstrap.js";
6
+ import { openSshDaemonTransport } from "./ssh-daemon-transport.js";
7
+ import { loadSavedMachines } from "./saved-machines.js";
8
+ /** Parse before acquiring a transport; invalid input cannot start a daemon. */
9
+ export function parseTmuxServersOperation(options) {
10
+ const command = options.command ?? "list";
11
+ if (options.ssh !== undefined)
12
+ SavedMachineSchema.shape.sshTarget.parse(options.ssh);
13
+ if (command === "create") {
14
+ if (options.socketName !== undefined ||
15
+ options.socketPath !== undefined ||
16
+ options.label !== undefined)
17
+ throw new Error("servers create requires a registered server ID and --session-name NAME");
18
+ return {
19
+ command,
20
+ serverId: TmuxServerIdSchemaZ.parse(options.serverId),
21
+ input: WorkspaceSessionCreateArgumentsSchemaZ.parse({
22
+ displayName: options.sessionName,
23
+ ...(options.cwd ? { cwd: options.cwd } : {}),
24
+ }),
25
+ };
26
+ }
27
+ if (options.sessionName !== undefined || options.cwd !== undefined)
28
+ throw new Error("--session-name and --dir are only supported by servers create");
29
+ if (command === "add") {
30
+ if (options.serverId ||
31
+ (options.socketName === undefined) === (options.socketPath === undefined))
32
+ throw new Error("servers add requires exactly one of --socket-name NAME or --socket-path /PATH");
33
+ return {
34
+ command,
35
+ registration: TmuxServerRegistrationRequestSchemaZ.parse({
36
+ label: options.label ?? options.socketName ?? "Custom server",
37
+ selector: options.socketName !== undefined
38
+ ? { kind: "name", name: options.socketName }
39
+ : { kind: "path", path: options.socketPath },
40
+ }),
41
+ };
42
+ }
43
+ if (options.socketName !== undefined ||
44
+ options.socketPath !== undefined ||
45
+ options.label !== undefined)
46
+ throw new Error("Socket selectors and --name are only supported by servers add");
47
+ if (command === "list" && options.serverId === undefined)
48
+ return { command };
49
+ if (command === "remove" || command === "sessions")
50
+ return { command, serverId: TmuxServerIdSchemaZ.parse(options.serverId) };
51
+ throw new Error("Usage: tmux-ide servers list|add|remove <server-id>|sessions <server-id>|create <server-id> --session-name NAME [--ssh HOST] [--json]");
52
+ }
53
+ export async function connectTmuxServersCli(options) {
54
+ const hostClientId = randomUUID();
55
+ if (options.ssh !== undefined) {
56
+ const transport = await openSshDaemonTransport({ alias: options.ssh });
57
+ try {
58
+ const expected = loadSavedMachines().machines.find((machine) => machine.sshTarget === options.ssh)?.expectedEnvironmentId;
59
+ if (expected && transport.daemon.environmentId !== expected)
60
+ throw new Error("SSH route reached a different environment than the saved profile");
61
+ return {
62
+ client: {
63
+ baseUrl: transport.baseUrl,
64
+ ownerToken: transport.daemon.authToken,
65
+ hostClientId,
66
+ origin: transport.baseUrl,
67
+ },
68
+ dispose: () => transport.dispose(),
69
+ };
70
+ }
71
+ catch (error) {
72
+ transport.dispose();
73
+ throw error;
74
+ }
75
+ }
76
+ // Server registration addresses the daemon, not the invoking shell's tmux server.
77
+ const { candidate } = await ensureCanonicalDaemon({
78
+ entryPath: options.entryPath,
79
+ expectedProductVersion: options.expectedProductVersion,
80
+ tmuxServerIntent: null,
81
+ });
82
+ if (!candidate.authToken)
83
+ throw new Error("Daemon owner authority is unavailable; update the local daemon");
84
+ const baseUrl = canonicalDaemonUrl("http", candidate.bindHostname, candidate.port);
85
+ return {
86
+ client: { baseUrl, ownerToken: candidate.authToken, hostClientId, origin: baseUrl },
87
+ dispose() { },
88
+ };
89
+ }
90
+ export async function runTmuxServersCli(options, connect) {
91
+ const operation = parseTmuxServersOperation(options);
92
+ const connection = await connect();
93
+ try {
94
+ // Probe the versioned resource before writes. An older daemon is unsupported,
95
+ // never a reason to execute a raw command against a default server.
96
+ let catalog;
97
+ try {
98
+ catalog = await listTmuxServers(connection.client);
99
+ }
100
+ catch (error) {
101
+ if (error instanceof TmuxServerClientError && [404, 405, 426].includes(error.status ?? 0))
102
+ throw new Error("This daemon does not support tmux server selection; update the selected machine's daemon", { cause: error });
103
+ throw error;
104
+ }
105
+ if (operation.command === "list")
106
+ return catalog;
107
+ if (operation.command === "add")
108
+ return await registerTmuxServer(connection.client, operation.registration);
109
+ const server = catalog.servers.find((candidate) => candidate.serverId === operation.serverId);
110
+ if (!server)
111
+ throw new Error("Selected tmux server is not registered; run tmux-ide servers list");
112
+ if (operation.command === "remove") {
113
+ await removeTmuxServer(connection.client, server.serverId);
114
+ return { removed: true, serverId: server.serverId };
115
+ }
116
+ if (server.state !== "online")
117
+ throw new Error("Selected tmux server is offline; no session operation was attempted");
118
+ const client = createTmuxServerClient(connection.client, {
119
+ serverId: server.serverId,
120
+ generation: server.generation,
121
+ });
122
+ try {
123
+ return operation.command === "create"
124
+ ? await client.createSession(randomUUID(), operation.input)
125
+ : await client.sessions();
126
+ }
127
+ finally {
128
+ client.dispose();
129
+ }
130
+ }
131
+ finally {
132
+ connection.dispose();
133
+ }
134
+ }
@@ -0,0 +1,55 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "@tmux-ide/contracts";
4
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
5
+ import { shellEscape } from "./shell.js";
6
+ /** Session identity travels with the mutation lane, never in global mutable selection. */
7
+ export function createTmuxSessionMutationFence() {
8
+ const context = new AsyncLocalStorage();
9
+ return {
10
+ wrap(run) {
11
+ return (args) => {
12
+ const selected = context.getStore();
13
+ if (!selected)
14
+ return run(args);
15
+ const sentinel = `tmux-session-stale.${randomUUID()}`;
16
+ const output = run([
17
+ "if-shell",
18
+ "-t",
19
+ `=${selected.name}`,
20
+ "-F",
21
+ `#{&&:#{==:#{session_id},${selected.id}},#{==:#{session_created},${selected.created}}}`,
22
+ args.map((arg) => (arg === ";" ? ";" : shellEscape(arg))).join(" "),
23
+ `display-message -p '${sentinel}'`,
24
+ ]);
25
+ if (output.trim() === sentinel)
26
+ throw new Error("Selected live tmux session changed before mutation");
27
+ return output;
28
+ };
29
+ },
30
+ async execute(options) {
31
+ WorkspaceCatalogLiveSessionIdSchemaZ.parse(options.liveSessionId);
32
+ const raw = await options.run([
33
+ "list-sessions",
34
+ "-F",
35
+ "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}",
36
+ ]);
37
+ const rows = raw
38
+ .trim()
39
+ .split("\n")
40
+ .map((line) => line.split("\t"))
41
+ .filter(([pid, id, created, name]) => pid &&
42
+ /^\d+$/.test(pid) &&
43
+ id &&
44
+ /^\$\d+$/.test(id) &&
45
+ created &&
46
+ /^\d+$/.test(created) &&
47
+ name === options.sessionName &&
48
+ liveSessionIdForNativeIdentity(pid, id, created) === options.liveSessionId);
49
+ if (rows.length !== 1)
50
+ throw new Error("Selected live tmux session is no longer available");
51
+ const [, id, created] = rows[0];
52
+ return context.run({ id: id, created: created, name: options.sessionName }, options.mutate);
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,88 @@
1
+ export const WINDOW_LINK_GUARD_APPLIED = "link-guard.ok";
2
+ export const WINDOW_LINK_GUARD_STALE = "link-guard.stale";
3
+ /** if-shell -F and its first branch command do not yield in tmux's command queue.
4
+ * Keep the mutation first and synchronous. Do not insert run-shell/wait-for/UI.
5
+ * This checks a current tuple, not an immutable historical winlink (tmux has none).
6
+ * Use buildNativeWindowLinkPaneSelectGuard for guarded compound pane selection.
7
+ */
8
+ export function buildNativeWindowLinkGuard(address, action) {
9
+ const { sessionId, windowIndex, expectedWindowId } = address;
10
+ if (!/^\$(?:0|[1-9]\d{0,15})$/u.test(sessionId) ||
11
+ !/^@(?:0|[1-9]\d{0,15})$/u.test(expectedWindowId) ||
12
+ !Number.isSafeInteger(Number(sessionId.slice(1))) ||
13
+ !Number.isSafeInteger(Number(expectedWindowId.slice(1))) ||
14
+ !Number.isSafeInteger(windowIndex) ||
15
+ windowIndex < 0 ||
16
+ (address.expectedServerPid !== undefined &&
17
+ !/^[1-9][0-9]{0,15}$/u.test(address.expectedServerPid)) ||
18
+ (address.expectedSessionCreated !== undefined &&
19
+ !/^(?:0|[1-9][0-9]{0,15})$/u.test(address.expectedSessionCreated)) ||
20
+ (action !== "select" && action !== "unlink")) {
21
+ throw new Error("Invalid native window link guard address or action");
22
+ }
23
+ const target = `${sessionId}:${windowIndex}`;
24
+ // CANFAIL target lookup makes all three comparisons necessary for missing targets.
25
+ let condition = `#{&&:#{==:#{session_id},${sessionId}},#{&&:#{==:#{window_index},${windowIndex}},#{==:#{window_id},${expectedWindowId}}}}`;
26
+ if (address.expectedServerPid !== undefined)
27
+ condition = `#{&&:#{==:#{pid},${address.expectedServerPid}},${condition}}`;
28
+ if (address.expectedSessionCreated !== undefined)
29
+ condition = `#{&&:#{==:#{session_created},${address.expectedSessionCreated}},${condition}}`;
30
+ const verb = action === "select" ? "select-window" : "unlink-window";
31
+ // Validated native IDs/index contain no quote, separator, or format syntax.
32
+ return [
33
+ "if-shell",
34
+ "-F",
35
+ "-t",
36
+ target,
37
+ condition,
38
+ `${verb} -t '${target}' ; display-message -p '${WINDOW_LINK_GUARD_APPLIED}'`,
39
+ `display-message -p '${WINDOW_LINK_GUARD_STALE}'`,
40
+ ];
41
+ }
42
+ /** Select an exact link and pane while preserving native user hooks. The window
43
+ * selection hook may yield or move topology, so revalidate after it. A failed
44
+ * second check is indeterminate: the window selection may already have happened.
45
+ */
46
+ export function buildNativeWindowLinkPaneSelectGuard(address, paneId) {
47
+ const windowGuard = buildNativeWindowLinkGuard(address, "select");
48
+ if (!/^%(?:0|[1-9]\d{0,15})$/u.test(paneId) || !Number.isSafeInteger(Number(paneId.slice(1)))) {
49
+ throw new Error("Invalid native pane guard address");
50
+ }
51
+ const target = `${address.sessionId}:${address.windowIndex}`;
52
+ const paneCondition = `#{&&:#{==:#{pane_id},${paneId}},#{==:#{window_id},${address.expectedWindowId}}}`;
53
+ const stale = `display-message -p '${WINDOW_LINK_GUARD_STALE}'`;
54
+ const interrupted = "display-message -p 'link-guard.interrupted'";
55
+ // All interpolated IDs are canonical numeric native identities. Braces are
56
+ // tmux command groups, not a host shell or a caller-provided command language.
57
+ const linkCheck = (yes, no) => `if-shell -F -t '${target}' '${windowGuard[4]}' { ${yes} } { ${no} }`;
58
+ const paneCheck = (yes, no) => `if-shell -F -t '${paneId}' '${paneCondition}' { ${yes} } { ${no} }`;
59
+ const selectPane = `select-pane -t '${paneId}' ; display-message -p '${WINDOW_LINK_GUARD_APPLIED}'`;
60
+ const afterHook = paneCheck(linkCheck(selectPane, interrupted), interrupted);
61
+ return [
62
+ "if-shell",
63
+ "-F",
64
+ "-t",
65
+ paneId,
66
+ paneCondition,
67
+ linkCheck(`select-window -t '${target}' ; ${afterHook}`, stale),
68
+ stale,
69
+ ];
70
+ }
71
+ /** Consume only output associated with this request's own native command boundary.
72
+ * Nonzero completion reports native failure, including last-link unlink refusal.
73
+ * For compound selection this can follow partial mutation; it is not rollback proof.
74
+ * Missing/extra output or interrupted completion is indeterminate, never success;
75
+ * it must not trigger an automatic retry of a potentially completed mutation.
76
+ */
77
+ export function classifyNativeWindowLinkGuardResult(status, stdout) {
78
+ if (status === null)
79
+ return "indeterminate";
80
+ if (status !== 0)
81
+ return "native-refused";
82
+ const output = stdout.trim();
83
+ if (output === WINDOW_LINK_GUARD_APPLIED)
84
+ return "applied";
85
+ if (output === WINDOW_LINK_GUARD_STALE)
86
+ return "stale";
87
+ return "indeterminate";
88
+ }
@@ -1,3 +1,6 @@
1
+ // Consumers outside the daemon tsconfig need the bundled declaration as well.
2
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
3
+ /// <reference path="../js-yaml.d.ts" />
1
4
  /**
2
5
  * Side-effect-free loader for repository-scoped WorkspaceConfigV1 files.
3
6
  *
@@ -26,6 +26,7 @@ import { getDefaultWorkspaceRegistry } from "./workspace-registry.js";
26
26
  import { internalInteractionOperationMarker } from "./tmux-external-interaction-observer.js";
27
27
  import { INTERNAL_READ_OPERATION_OPTION, INTERNAL_SEND_OPERATION_OPTION, } from "./tmux-interaction-options.js";
28
28
  import { memorablePaneName } from "../terminal/protocol/pane-display-name.js";
29
+ import { WindowLinkResolutionError } from "../terminal/mirror/window-link-authority.js";
29
30
  const CREATION_OPTION = "@tmux_ide_creation_id";
30
31
  const SEMANTIC_PANE_OPTION = "@tmux_ide_pane_id";
31
32
  const SEMANTIC_WINDOW_OPTION = "@tmux_ide_window_id";
@@ -47,6 +48,10 @@ const ERROR_MESSAGES = {
47
48
  workspace_unavailable: "The requested workspace is not available for multiplexer verbs.",
48
49
  pane_not_found: "No pane in this workspace carries the requested semantic identity.",
49
50
  window_not_found: "No window in this workspace carries the requested identity.",
51
+ window_link_stale: "The window link observation is no longer current.",
52
+ window_link_ambiguous: "Select an explicit window link for this pane.",
53
+ window_link_session_mismatch: "The window link belongs to another session generation.",
54
+ window_link_backing_mismatch: "The window link no longer names the expected window.",
50
55
  ambiguous_target: "The requested identity names more than one live tmux object.",
51
56
  last_window_refused: "This is the session's last window. Close the session instead if that is what you mean.",
52
57
  last_pane_refused: "This is the session's last pane. Close the session instead if that is what you mean.",
@@ -121,7 +126,19 @@ export function parseMultiplexerPaneRows(output) {
121
126
  creationId: creationId === "" ? null : creationId,
122
127
  });
123
128
  }
124
- return rows;
129
+ // list-panes -s repeats each backing for its session links. Identical native
130
+ // rows are one pane; conflicting observations fail closed.
131
+ const unique = new Map();
132
+ for (const row of rows) {
133
+ const previous = unique.get(row.paneId);
134
+ if (previous && JSON.stringify(previous) !== JSON.stringify(row)) {
135
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
136
+ reason: "conflicting_linked_pane_observation",
137
+ });
138
+ }
139
+ unique.set(row.paneId, row);
140
+ }
141
+ return [...unique.values()];
125
142
  }
126
143
  /** Distinct windows in a pane listing, in first-seen order. */
127
144
  export function windowIdsOf(rows) {
@@ -205,6 +222,61 @@ export class WorkspaceMultiplexerAuthority {
205
222
  mutate(raw, timing) {
206
223
  return this.#mutate(raw, timing);
207
224
  }
225
+ /** Runs only inside the existing semantic mutation lane; never rediscovers a stale link. */
226
+ async mutateWindowLink(raw, execute) {
227
+ if (this.#disposed)
228
+ throw new WorkspaceMultiplexerError("workspace_unavailable");
229
+ const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(raw);
230
+ if (request.expectedDaemonInstanceId !== this.#daemonInstanceId)
231
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch");
232
+ const intent = request.intent;
233
+ if (intent.verb !== "workspace.window.link.select" &&
234
+ intent.verb !== "workspace.window.link.unlink" &&
235
+ intent.verb !== "workspace.pane.select")
236
+ throw new TypeError("Expected a window-link selection or unlink intent");
237
+ const workspace = this.#registry.get(intent.workspaceName);
238
+ if (!workspace)
239
+ throw new WorkspaceMultiplexerError("workspace_not_found");
240
+ const envelope = {
241
+ operationId: request.operationId,
242
+ daemonInstanceId: this.#daemonInstanceId,
243
+ workspaceName: intent.workspaceName,
244
+ };
245
+ try {
246
+ const result = await execute(workspace.sessionName, intent.verb === "workspace.pane.select"
247
+ ? {
248
+ action: "select",
249
+ ...(intent.windowLink ? { target: intent.windowLink } : {}),
250
+ paneId: intent.semanticPaneId,
251
+ }
252
+ : {
253
+ action: intent.verb === "workspace.window.link.unlink" ? "unlink" : "select",
254
+ target: intent.target,
255
+ });
256
+ if (result.outcome !== "applied") {
257
+ throw new WorkspaceMultiplexerError(result.outcome === "stale"
258
+ ? "window_link_stale"
259
+ : result.outcome === "native-refused"
260
+ ? "mutation_failed"
261
+ : "mutation_unverified", { reason: result.outcome });
262
+ }
263
+ return WorkspaceMultiplexerMutationResultSchemaZ.parse(intent.verb === "workspace.pane.select"
264
+ ? {
265
+ ...envelope,
266
+ verb: intent.verb,
267
+ outcome: "applied",
268
+ semanticPaneId: intent.semanticPaneId,
269
+ }
270
+ : { ...envelope, verb: intent.verb, outcome: "applied", target: intent.target });
271
+ }
272
+ catch (error) {
273
+ if (error instanceof WorkspaceMultiplexerError)
274
+ throw error;
275
+ if (error instanceof WindowLinkResolutionError)
276
+ throw new WorkspaceMultiplexerError(error.reason, { operationId: request.operationId }, error);
277
+ throw new WorkspaceMultiplexerError("mutation_unverified", { operationId: request.operationId }, error);
278
+ }
279
+ }
208
280
  /**
209
281
  * Capture a semantically addressed pane for an authored read. The tmux
210
282
  * after-capture hook is the completion authority; this method only performs
@@ -436,6 +508,11 @@ export class WorkspaceMultiplexerAuthority {
436
508
  workspaceName: intent.workspaceName,
437
509
  };
438
510
  switch (intent.verb) {
511
+ case "workspace.window.link.select":
512
+ case "workspace.window.link.unlink":
513
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
514
+ reason: "window_link_authority_required",
515
+ });
439
516
  case "workspace.window.split":
440
517
  return this.#split(request, workspace, envelope);
441
518
  case "workspace.window.kill":
@@ -826,6 +903,10 @@ export class WorkspaceMultiplexerAuthority {
826
903
  };
827
904
  }
828
905
  #select(intent, sessionName, envelope, timing) {
906
+ if (intent.windowLink)
907
+ throw new WorkspaceMultiplexerError("workspace_unavailable", {
908
+ reason: "window_link_authority_required",
909
+ });
829
910
  const timed = (operation, run) => {
830
911
  if (!timing)
831
912
  return run();
@@ -1,10 +1,10 @@
1
1
  import { boundedTmuxRead } from "./bounded-tmux-read.js";
2
2
  import { accessSync, constants, realpathSync, statSync } from "node:fs";
3
- import { delimiter, isAbsolute, join, relative, sep } from "node:path";
3
+ import { isAbsolute, relative, sep } from "node:path";
4
4
  import { WorkspacePaneCreateMutationRequestSchemaZ, WorkspacePaneCreateMutationResultSchemaZ, } from "@tmux-ide/contracts";
5
5
  import { runTmuxBinary, TmuxError } from "@tmux-ide/tmux-bridge";
6
6
  import { createNamedSocketFence } from "./tmux-named-socket-fence.js";
7
- import { resolveBundledTmux } from "./bundled-tmux.js";
7
+ import { resolveTmuxExecutable as resolvePinnedTmuxExecutable, tmuxClientEnvironment, } from "./tmux-client-execution.js";
8
8
  import { probeProjectReadiness } from "./project-readiness-probe.js";
9
9
  import { agentHintForCommand } from "./agent-kind.js";
10
10
  import { loadWorkspaceConfig, WorkspaceConfigLoadError, } from "./workspace-config-loader.js";
@@ -12,7 +12,7 @@ import { getDefaultWorkspaceRegistry } from "./workspace-registry.js";
12
12
  import { memorablePaneName } from "../terminal/protocol/pane-display-name.js";
13
13
  import { shellEscape } from "./shell.js";
14
14
  import { MissionRepository } from "./mission-repository.js";
15
- import { resolveRuntimeNamespace } from "./runtime-namespace.js";
15
+ import { resolveRuntimeNamespace, resolveTmuxServerIntent } from "./runtime-namespace.js";
16
16
  import { prepareTmuxTruecolorEnvironment } from "./tmux-terminal-color.js";
17
17
  import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "./unix-socket-authority.js";
18
18
  const MAX_LIVE_OR_UNSAFE_OPERATIONS = 128;
@@ -102,61 +102,17 @@ function assertEffectiveConfigProvenance(workspace, canonicalRoot, source) {
102
102
  }
103
103
  }
104
104
  function resolveTmuxExecutable() {
105
+ // Preserve development artifact qualification errors instead of masking them.
105
106
  if (resolveRuntimeNamespace().development)
106
- return resolveBundledTmux();
107
- const configured = process.env.TMUX_IDE_TMUX_BIN;
108
- if (!configured) {
109
- const bundled = resolveBundledTmux();
110
- if (bundled)
111
- return bundled;
112
- }
113
- const candidates = configured
114
- ? [configured]
115
- : (process.env.PATH ?? "")
116
- .split(delimiter)
117
- // Empty and relative PATH entries mean the daemon/project cwd. They
118
- // are never executable authority for a privileged tmux mutation.
119
- .filter((entry) => entry.length > 0 && isAbsolute(entry))
120
- .map((entry) => join(entry, "tmux"));
121
- for (const candidate of candidates) {
122
- try {
123
- if (!isAbsolute(candidate))
124
- continue;
125
- accessSync(candidate, constants.X_OK);
126
- const canonical = realpathSync(candidate);
127
- if (statSync(canonical).isFile())
128
- return canonical;
129
- }
130
- catch {
131
- // Continue to the next daemon-start candidate.
132
- }
107
+ return resolvePinnedTmuxExecutable();
108
+ try {
109
+ return resolvePinnedTmuxExecutable();
133
110
  }
134
- throw new WorkspacePaneCreationError("workspace_unavailable", {
135
- reason: "tmux_executable_unavailable",
136
- });
137
- }
138
- const SAFE_TERMINAL_VALUE = /^(?:xterm|screen|tmux|rxvt|vt100|ansi)[A-Za-z0-9+._-]{0,58}$/u;
139
- const SAFE_LOCALE_VALUE = /^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}$/u;
140
- /**
141
- * The pinned tmux client receives only presentation metadata. Everything else
142
- * is denied by omission: PATH/HOME/SHELL, TMUX/TMUX_PANE/TMUX_TMPDIR, shell
143
- * startup hooks, dynamic-loader variables, Node options, and project secrets
144
- * cannot redirect authority or inject code into this daemon-owned execution.
145
- */
146
- function tmuxClientEnvironment(source) {
147
- const environment = {
148
- TERM: SAFE_TERMINAL_VALUE.test(source.TERM ?? "") ? source.TERM : "xterm-256color",
149
- // A pinned runner may be the first tmux client and therefore create the
150
- // server. Never let a headless parent (`TERM=dumb`, `NO_COLOR=1`) become
151
- // the global environment inherited by every later interactive child.
152
- COLORTERM: "truecolor",
153
- };
154
- for (const name of ["LANG", "LC_ALL", "LC_CTYPE"]) {
155
- const value = source[name];
156
- if (value && SAFE_LOCALE_VALUE.test(value))
157
- environment[name] = value;
111
+ catch {
112
+ throw new WorkspacePaneCreationError("workspace_unavailable", {
113
+ reason: "tmux_executable_unavailable",
114
+ });
158
115
  }
159
- return environment;
160
116
  }
161
117
  function tmuxSocketFromEnvironment() {
162
118
  const match = /^(.*),[0-9]+,[0-9]+$/u.exec(process.env.TMUX ?? "");
@@ -165,7 +121,7 @@ function tmuxSocketFromEnvironment() {
165
121
  export function resolveWorkspacePaneTmuxAuthority() {
166
122
  const namespace = resolveRuntimeNamespace();
167
123
  const executablePath = resolveTmuxExecutable();
168
- if (namespace.development)
124
+ if (resolveTmuxServerIntent()?.source === "explicit")
169
125
  return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
170
126
  const environmentSocket = tmuxSocketFromEnvironment();
171
127
  if (environmentSocket) {
@@ -186,7 +142,7 @@ export function resolveWorkspacePaneTmuxAuthority() {
186
142
  // A sessionless daemon may start before the default tmux server exists. Pin
187
143
  // the daemon's default socket *name* and captured environment rather than
188
144
  // consulting a later request's TMUX/PATH.
189
- return Object.freeze({ executablePath, socketSelector: resolveRuntimeNamespace().tmuxSocket });
145
+ return Object.freeze({ executablePath, socketSelector: namespace.tmuxSocket });
190
146
  }
191
147
  /** Execute mutations only through the daemon-generation-pinned tmux authority. */
192
148
  export function createPinnedWorkspaceTmuxRunner(authority, options = {}) {