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
@@ -1,3 +1,4 @@
1
+ import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
1
2
  import { applicationDaemonEndpoint, readApplicationDaemonInfo as readCanonicalDaemonInfo, } from "./application-daemon-authority.js";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { dispatchOwnerAction } from "@tmux-ide/daemon-client/owner-action-client";
@@ -43,13 +44,31 @@ async function dispatchOnHandle(handle, name, input) {
43
44
  ? result
44
45
  : null;
45
46
  }
46
- export function createFleetSession(input, name) {
47
+ export function createFleetSession(input, name, server, options) {
47
48
  if ("read" in input) {
48
49
  const daemon = input.read();
49
50
  if (!daemon || !name)
50
51
  return Promise.resolve(null);
52
+ if (server) {
53
+ if (!daemon.authToken || input.endpoint().state !== "ready")
54
+ return Promise.resolve(null);
55
+ const epoch = input.endpoint().epoch;
56
+ const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
57
+ const client = createTmuxServerClient({ baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl }, server);
58
+ return client
59
+ .createSession(randomUUID(), {
60
+ displayName: name,
61
+ ...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
62
+ expectedDaemonInstanceId: server.generation,
63
+ })
64
+ .then((result) => input.endpoint().epoch === epoch && input.read()?.instanceId === daemon.instanceId
65
+ ? result
66
+ : null)
67
+ .finally(() => client.dispose());
68
+ }
51
69
  return dispatchOnHandle(input, "workspace.session.create", {
52
70
  displayName: name,
71
+ ...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
53
72
  expectedDaemonInstanceId: daemon.instanceId,
54
73
  });
55
74
  }
@@ -59,6 +78,30 @@ export function createFleetSession(input, name) {
59
78
  export function closeFleetSession(handle, target) {
60
79
  if (handle.read()?.instanceId !== target.daemonInstanceId)
61
80
  return Promise.resolve(null);
81
+ if (target.server) {
82
+ const daemon = handle.read();
83
+ if (!daemon?.authToken || handle.endpoint().state !== "ready")
84
+ return Promise.resolve(null);
85
+ const epoch = handle.endpoint().epoch;
86
+ const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
87
+ const client = createTmuxServerClient({ baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl }, target.server);
88
+ return client
89
+ .mutate(randomUUID(), {
90
+ verb: "workspace.session.kill",
91
+ workspaceName: target.workspaceName ?? target.sessionName,
92
+ fleetTarget: {
93
+ daemonInstanceId: target.server.generation,
94
+ liveSessionId: target.liveSessionId,
95
+ sessionName: target.sessionName,
96
+ },
97
+ })
98
+ .then((result) => handle.endpoint().epoch === epoch &&
99
+ handle.read()?.instanceId === target.daemonInstanceId &&
100
+ result.verb === "workspace.session.kill"
101
+ ? result
102
+ : null)
103
+ .finally(() => client.dispose());
104
+ }
62
105
  return dispatchOnHandle(handle, "workspace.session.kill", {
63
106
  workspaceName: target.workspaceName ?? target.sessionName,
64
107
  fleetTarget: {
@@ -0,0 +1,54 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { Show } from "solid-js";
3
+ import { TuiButton } from "../ui/button.jsx";
4
+ import { clipTerminal } from "../terminal-text.js";
5
+ import { GUIDED_TOUR_STEPS, guidedTourCopy, } from "./guided-tour.js";
6
+ /** Non-modal coach: terminal interaction stays available throughout the tour. */
7
+ export function GuidedTourCoach(props) {
8
+ const copy = () => guidedTourCopy(props.state.step, props.shortcuts);
9
+ const width = () => Math.max(1, props.width);
10
+ const primary = () => {
11
+ switch (props.state.step) {
12
+ case "welcome":
13
+ return { label: "Let's try it", run: props.onWelcomeRead };
14
+ case "practice":
15
+ return { label: "Create practice session", run: props.onCreatePractice };
16
+ case "split":
17
+ case "commands":
18
+ return { label: "Open commands", run: props.onOpenCommands };
19
+ case "theme":
20
+ return { label: "Choose theme", run: props.onOpenAppearance };
21
+ case "leave":
22
+ return { label: "Open Home", run: props.onOpenHome };
23
+ case "reopen":
24
+ return { label: "Reopen practice", run: props.onOpenPractice };
25
+ default:
26
+ return null;
27
+ }
28
+ };
29
+ return (<box id="guided-tour-coach" width={width()} height={props.height ?? 10} overflow="hidden" flexShrink={0} flexDirection="column" paddingX={1} backgroundColor={props.theme.roles.surfaces.panel}>
30
+ <text height={1} fg={props.theme.roles.text.primary}>
31
+ <strong>
32
+ {clipTerminal(`Learn tmux-ide · ${GUIDED_TOUR_STEPS.indexOf(props.state.step) + 1}/${GUIDED_TOUR_STEPS.length} · ${copy().title}`, Math.max(0, width() - 2))}
33
+ </strong>
34
+ </text>
35
+ <scrollbox flexGrow={1} width="100%">
36
+ <text fg={props.theme.roles.text.secondary}>{copy().body}</text>
37
+ </scrollbox>
38
+ <Show when={props.error}>
39
+ <text fg={props.theme.roles.text.primary}>{props.error}</text>
40
+ </Show>
41
+ <box flexDirection="row" flexWrap="wrap" flexShrink={0} gap={1}>
42
+ <Show when={props.state.practice && ["split", "focus", "resize"].includes(props.state.step)}>
43
+ <TuiButton theme={props.theme} label="Open practice" onPress={props.onOpenPractice} width={Math.min(Math.max(1, width() - 2), 17)}/>
44
+ </Show>
45
+ <Show when={props.onReplay && props.error}>
46
+ <TuiButton theme={props.theme} label="Restart tour" onPress={props.onReplay} width={Math.min(Math.max(1, width() - 2), 16)}/>
47
+ </Show>
48
+ <Show when={primary()}>
49
+ {(action) => (<TuiButton theme={props.theme} label={action().label} width={Math.min(Math.max(1, width() - 2), action().label.length + 4)} loading={props.busy} onPress={action().run}/>)}
50
+ </Show>
51
+ <TuiButton theme={props.theme} label={props.state.step === "complete" ? "Done" : "Pause tour"} width={Math.min(Math.max(1, width() - 2), 14)} variant="ghost" onPress={props.onPause}/>
52
+ </box>
53
+ </box>);
54
+ }
@@ -0,0 +1,67 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { runtimeOwnedPath } from "../../../lib/runtime-namespace.js";
5
+ import { stateHome } from "../../../lib/state-home.js";
6
+ import { GUIDED_TOUR_STEPS, initialGuidedTourState } from "./guided-tour.js";
7
+ export function guidedTourStatePath() {
8
+ return runtimeOwnedPath(join(stateHome(), "guided-tour.json"));
9
+ }
10
+ export function parseGuidedTourState(value) {
11
+ if (!value || typeof value !== "object")
12
+ return initialGuidedTourState();
13
+ const raw = value;
14
+ if (raw.version !== 1 ||
15
+ !GUIDED_TOUR_STEPS.includes(raw.step) ||
16
+ typeof raw.active !== "boolean")
17
+ return initialGuidedTourState();
18
+ let practice = null;
19
+ if (raw.practice && typeof raw.practice === "object") {
20
+ const p = raw.practice;
21
+ if ([p.machineId, p.serverId, p.generation, p.sessionId, p.sessionName].every((v) => typeof v === "string" && v.length > 0)) {
22
+ practice = {
23
+ machineId: p.machineId,
24
+ serverId: p.serverId,
25
+ generation: p.generation,
26
+ sessionId: p.sessionId,
27
+ sessionName: p.sessionName,
28
+ };
29
+ }
30
+ }
31
+ const step = raw.step;
32
+ return {
33
+ version: 1,
34
+ step: !practice && !["welcome", "practice", "complete"].includes(step) ? "practice" : step,
35
+ active: raw.active,
36
+ practice,
37
+ };
38
+ }
39
+ export function readGuidedTourState(path = guidedTourStatePath()) {
40
+ try {
41
+ return parseGuidedTourState(JSON.parse(readFileSync(path, "utf8")));
42
+ }
43
+ catch {
44
+ return initialGuidedTourState();
45
+ }
46
+ }
47
+ /** Atomic, best-effort persistence; failure must not prevent using terminals. */
48
+ export function writeGuidedTourState(state, path = guidedTourStatePath()) {
49
+ const temporary = `${path}.${randomUUID()}.tmp`;
50
+ try {
51
+ mkdirSync(dirname(path), { recursive: true });
52
+ writeFileSync(temporary, JSON.stringify(state), { mode: 0o600 });
53
+ renameSync(temporary, path);
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ finally {
60
+ try {
61
+ rmSync(temporary, { force: true });
62
+ }
63
+ catch {
64
+ /* Best effort. */
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,113 @@
1
+ /** A small, action-driven tour. No timers and no mutations of terminal sessions. */
2
+ export const GUIDED_TOUR_STEPS = [
3
+ "welcome",
4
+ "practice",
5
+ "split",
6
+ "focus",
7
+ "resize",
8
+ "commands",
9
+ "theme",
10
+ "leave",
11
+ "reopen",
12
+ "complete",
13
+ ];
14
+ export function initialGuidedTourState() {
15
+ return { version: 1, step: "welcome", active: false, practice: null };
16
+ }
17
+ export function sameGuidedTourPractice(a, b) {
18
+ return (a !== null &&
19
+ a.machineId === b.machineId &&
20
+ a.serverId === b.serverId &&
21
+ a.generation === b.generation &&
22
+ a.sessionId === b.sessionId);
23
+ }
24
+ /** Call confirmation events only after an observed change, never on request dispatch. */
25
+ export function reduceGuidedTour(state, event) {
26
+ if (event.type === "replay")
27
+ return { ...initialGuidedTourState(), active: true };
28
+ if (event.type === "pause")
29
+ return state.active ? { ...state, active: false } : state;
30
+ if (event.type === "start" || event.type === "resume") {
31
+ return state.step === "complete" ? state : { ...state, active: true };
32
+ }
33
+ if (event.type === "practice-created" && state.step === "practice") {
34
+ return { ...state, step: "split", practice: event.practice };
35
+ }
36
+ if (!state.active)
37
+ return state;
38
+ if (event.type === "practice-missing" && state.practice && state.step !== "complete") {
39
+ return { ...state, step: "practice", practice: null };
40
+ }
41
+ if (event.type === "welcome-read" && state.step === "welcome")
42
+ return { ...state, step: "practice" };
43
+ if ("practice" in event && !sameGuidedTourPractice(state.practice, event.practice))
44
+ return state;
45
+ const expected = {
46
+ split: "split-confirmed",
47
+ focus: "focus-confirmed",
48
+ resize: "resize-confirmed",
49
+ commands: "commands-opened",
50
+ theme: "theme-changed",
51
+ leave: "home-opened",
52
+ reopen: "reopened",
53
+ };
54
+ if (expected[state.step] !== event.type)
55
+ return state;
56
+ const step = GUIDED_TOUR_STEPS[GUIDED_TOUR_STEPS.indexOf(state.step) + 1];
57
+ return { ...state, step };
58
+ }
59
+ export function guidedTourCopy(step, keys = {}) {
60
+ const shortcut = (key) => (key ? ` (${key})` : "");
61
+ switch (step) {
62
+ case "welcome":
63
+ return {
64
+ title: "Meet tmux-ide",
65
+ body: "A machine can run several tmux servers. Each server has sessions; sessions contain windows, and windows contain terminal panes. Home brings agents across connected machines together.",
66
+ };
67
+ case "practice":
68
+ return {
69
+ title: "Your practice space",
70
+ body: "Create a separate local practice session. The tour leaves your existing sessions alone. You can pause now and resume from Home.",
71
+ };
72
+ case "split":
73
+ return {
74
+ title: "Make room",
75
+ body: `Split the practice pane${shortcut(keys.split)}. You can also find Split right in Commands${shortcut(keys.commands)}. The next step unlocks when the new pane appears.`,
76
+ };
77
+ case "focus":
78
+ return {
79
+ title: "Move between panes",
80
+ body: `Select the other practice pane by clicking it or using your pane navigation shortcut${shortcut(keys.focus)}. Each pane is an independent terminal.`,
81
+ };
82
+ case "resize":
83
+ return {
84
+ title: "Find a comfortable layout",
85
+ body: "Drag the divider between the practice panes. Continue working while tmux updates the layout; the tour advances after the changed dimensions arrive.",
86
+ };
87
+ case "commands":
88
+ return {
89
+ title: "Find an action",
90
+ body: `Open Commands${shortcut(keys.commands)} to discover actions and their current shortcuts. Close the palette when you are ready to continue.`,
91
+ };
92
+ case "theme":
93
+ return {
94
+ title: "Make it yours",
95
+ body: "Choose a different theme from Home. Automatic contrast correction improves hard-to-read terminal colors; you can turn it off in settings.",
96
+ };
97
+ case "leave":
98
+ return {
99
+ title: "Leave without losing work",
100
+ body: `Open Home${shortcut(keys.home)}. Your practice terminals keep running in tmux while you look at agents or other sessions.`,
101
+ };
102
+ case "reopen":
103
+ return {
104
+ title: "Pick up where you left off",
105
+ body: "Reopen your practice session. tmux keeps your terminals alive independently of which page or session you are viewing.",
106
+ };
107
+ case "complete":
108
+ return {
109
+ title: "Ready to explore",
110
+ body: "You have split, focused and resized panes, found commands, changed theme and returned to a running session. Your practice session remains available; close it when you are finished. Replay this tour from Home.",
111
+ };
112
+ }
113
+ }
@@ -162,7 +162,7 @@ function buildProductionBundle(connection, callbacks, readDaemon = readCanonical
162
162
  requestTerminalRuntimeInventoryRefresh: () => {
163
163
  connection.transport.refreshTerminalRuntimeInventory();
164
164
  },
165
- actions: productionOwnerActions(readDaemon),
165
+ actions: connection.ownerActions ?? productionOwnerActions(readDaemon),
166
166
  },
167
167
  });
168
168
  fastLane = createOpenTuiWorkspaceTerminalFastLane(client, OPEN_TUI_HOST_CLIENT_ID, causalCellLedger);
@@ -27,6 +27,10 @@ export async function resizeTerminalPane(expected, current, input, onFailure) {
27
27
  });
28
28
  };
29
29
  try {
30
+ // Geometry acquisition is itself a mutation of the ownership lease. A stale
31
+ // gesture must not take it from another client even when resize is refused.
32
+ if (!isCurrent())
33
+ return fail({ stage: "pre-dispatch", reason: "generation-replaced" });
30
34
  if (expected.client.ownsRuntimeAuthority?.("geometry") !== true) {
31
35
  let lease;
32
36
  try {
@@ -1,5 +1,5 @@
1
1
  /** Acquire controller authority before issuing the canonical pane-select intent. */
2
- export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure) {
2
+ export async function selectTerminalPane(expected, current, semanticPaneId, operationId, onFailure, windowLink) {
3
3
  const fail = (failure) => {
4
4
  try {
5
5
  onFailure?.(failure);
@@ -57,6 +57,7 @@ export async function selectTerminalPane(expected, current, semanticPaneId, oper
57
57
  verb: "workspace.pane.select",
58
58
  workspaceName: expected.workspaceName,
59
59
  semanticPaneId,
60
+ ...(windowLink ? { windowLink } : {}),
60
61
  },
61
62
  });
62
63
  }
@@ -8,7 +8,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
8
8
  let applied = null;
9
9
  let pending = null;
10
10
  let desired = null;
11
- let scopedFlight = false;
11
+ let scopedFlight = null;
12
12
  let scopedEpoch = 0;
13
13
  let authorityClient = null;
14
14
  let stopAuthority = null;
@@ -21,7 +21,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
21
21
  const drain = async () => {
22
22
  if (scopedFlight || disposed || authoritySuspended)
23
23
  return;
24
- scopedFlight = true;
24
+ const flight = {};
25
+ scopedFlight = flight;
25
26
  try {
26
27
  while (desired && !disposed && !authoritySuspended) {
27
28
  const owner = desired;
@@ -41,7 +42,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
41
42
  if (disposed || !desired)
42
43
  break;
43
44
  if (epoch !== scopedEpoch)
44
- continue;
45
+ break;
45
46
  const stillDesired = desired.targets.some((entry) => targetKey(entry) === targetKey(target) && targetSize(entry) === targetSize(target));
46
47
  if (outcome.status !== "applied") {
47
48
  if (!stillDesired)
@@ -55,7 +56,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
55
56
  }
56
57
  }
57
58
  finally {
58
- scopedFlight = false;
59
+ if (scopedFlight === flight)
60
+ scopedFlight = null;
59
61
  }
60
62
  };
61
63
  const sameAuthority = (left, right) => left.lane === right.lane &&
@@ -83,6 +85,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
83
85
  pending = null;
84
86
  desired = null;
85
87
  scopedEpoch += 1;
88
+ scopedFlight = null;
86
89
  scopedApplied.clear();
87
90
  return;
88
91
  }
@@ -110,6 +113,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
110
113
  pending = null;
111
114
  scopedApplied.clear();
112
115
  scopedEpoch += 1;
116
+ scopedFlight = null;
113
117
  if (!authoritySuspended)
114
118
  retry?.();
115
119
  }) ?? null;
@@ -130,6 +134,8 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
130
134
  pending = null;
131
135
  if (!desired || !sameAuthority(desired.identity, target)) {
132
136
  scopedEpoch += 1;
137
+ // A retired lane must not hold a replacement lane behind its receipt.
138
+ scopedFlight = null;
133
139
  scopedApplied.clear();
134
140
  }
135
141
  const retainedWindows = new Set(["", ...windows.map((window) => window.semanticWindowId)]);
@@ -156,6 +162,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
156
162
  if (desired) {
157
163
  desired = null;
158
164
  scopedEpoch += 1;
165
+ scopedFlight = null;
159
166
  scopedApplied.clear();
160
167
  applied = null;
161
168
  pending = null;
@@ -186,6 +193,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
186
193
  stopAuthority = null;
187
194
  authorityClient = null;
188
195
  desired = null;
196
+ scopedFlight = null;
189
197
  scopedApplied.clear();
190
198
  applied = null;
191
199
  pending = null;
@@ -30,15 +30,18 @@ export class TerminalPaneInputRouter {
30
30
  this.#pending = null;
31
31
  }
32
32
  selectPane(paneId, options = {}) {
33
- if (this.#pending?.paneId === paneId)
33
+ if (!options.selection && this.#pending?.paneId === paneId)
34
34
  return;
35
- if (this.#pending === null && this.#canonicalPane === paneId && this.#focusedPane === paneId)
35
+ if (!options.selection &&
36
+ this.#pending === null &&
37
+ this.#canonicalPane === paneId &&
38
+ this.#focusedPane === paneId)
36
39
  return;
37
40
  const token = ++this.#selectionToken;
38
41
  const presentOptimistically = options.presentOptimistically !== false;
39
42
  if (presentOptimistically)
40
43
  this.#setFocusedPane(paneId);
41
- const settled = this.#options.select(paneId);
44
+ const settled = options.selection ? options.selection() : this.#options.select(paneId);
42
45
  this.#pending = { token, paneId, presentOptimistically, settled, selected: null };
43
46
  void settled.then((selected) => {
44
47
  if (this.#pending?.token !== token)
@@ -5,8 +5,14 @@ import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
5
5
  export function applicationCommandDescription(command) {
6
6
  if (typeof command === "object" && command.kind === "open-machine")
7
7
  return {
8
- id: JSON.stringify([command.kind, command.fleet.machineId]),
9
- label: `Machine · ${command.label}`,
8
+ id: JSON.stringify([
9
+ command.kind,
10
+ command.fleet.machineId,
11
+ ...(command.fleet.server
12
+ ? [command.fleet.server.serverId, command.fleet.server.generation]
13
+ : []),
14
+ ]),
15
+ label: `${command.fleet.server ? "Server" : "Machine"} · ${command.label}`,
10
16
  detail: command.fleet.disabled ? "Unavailable" : "Browse sessions or create on this host",
11
17
  };
12
18
  if (typeof command === "object") {
@@ -16,6 +22,9 @@ export function applicationCommandDescription(command) {
16
22
  ? [
17
23
  command.kind,
18
24
  command.fleet.machineId,
25
+ ...(command.fleet.server
26
+ ? [command.fleet.server.serverId, command.fleet.server.generation]
27
+ : []),
19
28
  command.fleet.liveSessionId,
20
29
  session ? null : command.paneId,
21
30
  ]
@@ -214,9 +214,10 @@ function agentStatus(status) {
214
214
  */
215
215
  export function TerminalWindowStrip(props) {
216
216
  const tabs = () => (typeof props.tabs === "function" ? props.tabs() : props.tabs);
217
- const tabId = (tab) => tab.semanticWindowId ?? `window:${tab.index}`;
217
+ const tabId = (tab) => tab.linkId ?? tab.semanticWindowId ?? `window:${tab.index}`;
218
218
  const items = createMemo(() => tabs().map((tab) => ({
219
219
  id: tabId(tab),
220
+ disabled: tab.disabled,
220
221
  windowIndex: tab.index,
221
222
  title: tab.name,
222
223
  agentStatus: agentStatus(tab.status),
@@ -48,6 +48,12 @@ const ACTION_COMMAND_METADATA: Record<ActionName, ActionCommandMetadata> = {
48
48
  "workspace.open.cancel": { label: "Cancel workspace handoff", category: "workspace" },
49
49
  "workspace.promote": { label: "Promote live session to workspace", category: "workspace" },
50
50
  "workspace.app-window.mutate": { label: "Mutate application window", category: "workspace" },
51
+ "workspace.window.link.select": { label: "Focus window link", category: "workspace" },
52
+ "workspace.window.link.unlink": {
53
+ label: "Unlink window",
54
+ category: "workspace",
55
+ dangerous: true,
56
+ },
51
57
  "workspace.window.split": { label: "Split pane", category: "workspace" },
52
58
  "workspace.window.kill": { label: "Close window", category: "workspace", dangerous: true },
53
59
  "workspace.pane.kill": { label: "Close pane", category: "workspace", dangerous: true },
@@ -72,6 +72,10 @@ export type ActionErrorCode =
72
72
  | "pane_not_found"
73
73
  | "window_not_found"
74
74
  | "ambiguous_target"
75
+ | "window_link_stale"
76
+ | "window_link_ambiguous"
77
+ | "window_link_session_mismatch"
78
+ | "window_link_backing_mismatch"
75
79
  | "last_window_refused"
76
80
  | "last_pane_refused"
77
81
  | "mutation_failed"
@@ -84,6 +84,8 @@ function verbHandler<V extends WorkspaceMultiplexerVerb>(verb: V) {
84
84
  ): Promise<ActionResult<V>> => (await runVerb(verb, input, context, deps)) as ActionResult<V>;
85
85
  }
86
86
 
87
+ export const workspaceWindowLinkSelectHandler = verbHandler("workspace.window.link.select");
88
+ export const workspaceWindowLinkUnlinkHandler = verbHandler("workspace.window.link.unlink");
87
89
  export const workspaceWindowSplitHandler = verbHandler("workspace.window.split");
88
90
  export const workspaceWindowKillHandler = verbHandler("workspace.window.kill");
89
91
  export const workspacePaneKillHandler = verbHandler("workspace.pane.kill");
@@ -70,6 +70,8 @@ import {
70
70
  workspaceRenameHandler,
71
71
  workspaceSessionKillHandler,
72
72
  workspaceWindowKillHandler,
73
+ workspaceWindowLinkSelectHandler,
74
+ workspaceWindowLinkUnlinkHandler,
73
75
  workspaceWindowSplitHandler,
74
76
  } from "./handlers/workspace-multiplexer.ts";
75
77
 
@@ -301,6 +303,18 @@ export const actionRegistry: RegistryShape = {
301
303
  handler: (input) => workspacePaneZoomToggleHandler(input),
302
304
  handlerWithContext: workspacePaneZoomToggleHandler,
303
305
  },
306
+ "workspace.window.link.select": {
307
+ inputSchema: ActionContractsZ["workspace.window.link.select"].input,
308
+ resultSchema: ActionContractsZ["workspace.window.link.select"].result,
309
+ handler: (input) => workspaceWindowLinkSelectHandler(input),
310
+ handlerWithContext: workspaceWindowLinkSelectHandler,
311
+ },
312
+ "workspace.window.link.unlink": {
313
+ inputSchema: ActionContractsZ["workspace.window.link.unlink"].input,
314
+ resultSchema: ActionContractsZ["workspace.window.link.unlink"].result,
315
+ handler: (input) => workspaceWindowLinkUnlinkHandler(input),
316
+ handlerWithContext: workspaceWindowLinkUnlinkHandler,
317
+ },
304
318
  "workspace.pane.select": {
305
319
  inputSchema: ActionContractsZ["workspace.pane.select"].input,
306
320
  resultSchema: ActionContractsZ["workspace.pane.select"].result,
@@ -1,6 +1,6 @@
1
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.ts";
1
2
  import { runtimeTmuxArgs } from "../lib/runtime-namespace.ts";
2
3
  import { execFileSync } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
4
  import { listSessionPanes } from "../widgets/lib/pane-comms.ts";
5
5
  import type { PaneInfo } from "@tmux-ide/contracts";
6
6
  import { getDefaultWorkspaceRegistry } from "../lib/workspace-registry.ts";
@@ -80,14 +80,6 @@ export interface LiveSessionSummary {
80
80
  readonly paneCount: number;
81
81
  }
82
82
 
83
- function liveSessionId(serverPid: string, sessionId: string, sessionCreated: string) {
84
- const digest = createHash("sha256")
85
- .update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
86
- .digest("hex")
87
- .slice(0, 20);
88
- return `live-session.${digest}` as const;
89
- }
90
-
91
83
  /**
92
84
  * Enumerate observed tmux truth without consulting the workspace registry.
93
85
  *
@@ -120,7 +112,7 @@ export function discoverLiveSessionSummaries(
120
112
  if (!/^\d+$/u.test(serverPid) || !/^\$\d+$/u.test(sessionId) || !/^\d+$/u.test(sessionCreated))
121
113
  continue;
122
114
  if (!sessionName || !isVisibleFleetSession(sessionName)) continue;
123
- const identity = liveSessionId(serverPid, sessionId, sessionCreated);
115
+ const identity = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
124
116
  const previous = sessions.get(identity);
125
117
  sessions.set(identity, {
126
118
  liveSessionId: identity,
@@ -1,3 +1,4 @@
1
+ import { mountTmuxServerRoutes } from "./tmux-servers.ts";
1
2
  import { streamBoundedLogs } from "./log-stream.ts";
2
3
  import { mountWorkspaceAdmissionRoute } from "./resources/workspace-admission-route.ts";
3
4
  import { mountFleetPreviewRoute } from "./resources/fleet-preview-route.ts";
@@ -162,6 +163,11 @@ import {
162
163
  } from "./resources/startup-readiness-route.ts";
163
164
  import { readWidgetAsset } from "../lib/widget-asset-store.ts";
164
165
  export interface CreateAppOptions {
166
+ tmuxServerOwners?: import("../lib/tmux-server-owners.ts").TmuxServerOwners<
167
+ import("../lib/tmux-server-owner.ts").NativeTmuxServerOwner
168
+ >;
169
+ /** Fresh, credential-free proof of this owner's pinned tmux server. */
170
+ tmuxServerProof?: () => Promise<import("../lib/tmux-server-proof.ts").TmuxServerProof | null>;
165
171
  authService?: AuthService;
166
172
  authConfig?: AuthConfig;
167
173
  remoteAccess?: {
@@ -533,6 +539,12 @@ export function createApp(options: CreateAppOptions = {}): Hono {
533
539
  // Allow cross-origin (Next.js dashboard, Tailscale, etc.)
534
540
  app.use("/*", cors());
535
541
 
542
+ if (options.tmuxServerOwners)
543
+ mountTmuxServerRoutes(app, {
544
+ ownerToken: options.remoteAccess?.ownerToken ?? null,
545
+ owners: options.tmuxServerOwners,
546
+ });
547
+
536
548
  // Owner-only reads use the same early routing boundary as issuance below:
537
549
  // remote/project credentials neither authorize nor block the owner bearer.
538
550
  mountDiagnosticsRoute(app, {
@@ -743,10 +755,26 @@ export function createApp(options: CreateAppOptions = {}): Hono {
743
755
  // Credential-free endpoint binding. A desktop host reads the nonce from the
744
756
  // owner-only canonical record, probes this endpoint, and compares before it
745
757
  // sends any remote-access or local-bypass credential.
746
- app.get("/identity", (c) => {
758
+ // Coalesce concurrent probes, but never reuse a completed server proof.
759
+ let pendingTmuxProof: Promise<
760
+ import("../lib/tmux-server-proof.ts").TmuxServerProof | null
761
+ > | null = null;
762
+ app.get("/identity", async (c) => {
763
+ const wantsTmuxProof = c.req.query("tmuxServerProof") === "1";
764
+ let tmuxServerProof = null;
765
+ if (wantsTmuxProof && options.tmuxServerProof) {
766
+ pendingTmuxProof ??= Promise.resolve()
767
+ .then(options.tmuxServerProof)
768
+ .catch(() => null)
769
+ .finally(() => {
770
+ pendingTmuxProof = null;
771
+ });
772
+ tmuxServerProof = await pendingTmuxProof;
773
+ }
747
774
  return c.json({
748
775
  ok: true,
749
776
  pid: process.pid,
777
+ ...(wantsTmuxProof && options.tmuxServerProof ? { tmuxServerProof } : {}),
750
778
  protocolVersion: DAEMON_WIRE_PROTOCOL_VERSION,
751
779
  productVersion: daemonIdentity.productVersion,
752
780
  instanceId: daemonIdentity.instanceId,