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,49 @@
1
+ import { Hono } from "hono";
2
+ import { TmuxServerScopeSchemaZ } from "@tmux-ide/contracts";
3
+ import { TmuxServerScopeError, type TmuxServerOwners } from "../lib/tmux-server-owners.ts";
4
+ import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
5
+ import { mountTerminalNativeBackingRoute } from "./resources/terminal-native-backing-route.ts";
6
+
7
+ /** Retain admission/coalescing limits for the lifetime of each independent owner. */
8
+ export function mountTmuxServerNativeBackingRoute(
9
+ app: Hono,
10
+ options: {
11
+ scopedPath: string;
12
+ ownerToken: string | null;
13
+ owners: TmuxServerOwners<NativeTmuxServerOwner>;
14
+ },
15
+ ): void {
16
+ const routers = new WeakMap<NativeTmuxServerOwner, Hono>();
17
+ app.get(`${options.scopedPath}/native-backing/:workspaceName/:pane`, async (c) => {
18
+ const scope = TmuxServerScopeSchemaZ.safeParse({
19
+ serverId: c.req.param("serverId"),
20
+ generation: c.req.param("generation"),
21
+ });
22
+ if (!scope.success) return c.json({ status: "invalid" }, 400);
23
+ try {
24
+ return await options.owners.withOwner(scope.data, async (owner) => {
25
+ let router = routers.get(owner);
26
+ if (!router) {
27
+ router = new Hono();
28
+ mountTerminalNativeBackingRoute(router, {
29
+ ownerToken: options.ownerToken,
30
+ generation: owner.generation,
31
+ resolveSession: (workspace) =>
32
+ owner.workspaceRegistry.get(workspace)?.sessionName ?? null,
33
+ capture: (session, pane, expected) =>
34
+ owner.sessionRuntimeRegistry.captureNativeBacking(session, pane, expected),
35
+ });
36
+ routers.set(owner, router);
37
+ }
38
+ const url = new URL(c.req.url);
39
+ url.pathname = `/api/project/${encodeURIComponent(c.req.param("workspaceName"))}/terminal-native-backing/${encodeURIComponent(c.req.param("pane"))}`;
40
+ return await router.request(new Request(url, c.req.raw));
41
+ });
42
+ } catch (error) {
43
+ return c.json(
44
+ { status: error instanceof TmuxServerScopeError ? "retired" : "unavailable" },
45
+ error instanceof TmuxServerScopeError ? 409 : 503,
46
+ );
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,479 @@
1
+ import { mountTmuxServerNativeBackingRoute } from "./tmux-server-native-backing.ts";
2
+ import { Hono, type Context } from "hono";
3
+ import { streamSSE } from "hono/streaming";
4
+ import { projectApplicationShellResource } from "./resources/application-shell.ts";
5
+ import { fleetSessionIdForName } from "./resources/fleet-catalog.ts";
6
+ import { z } from "zod";
7
+ import {
8
+ TMUX_SERVERS_API_PATH,
9
+ PANE_STREAM_ISSUE_PATH,
10
+ TmuxServerIdSchemaZ,
11
+ TmuxServerScopeSchemaZ,
12
+ TmuxServersResourceSchemaZ,
13
+ TmuxServerSessionsResourceSchemaZ,
14
+ WorkspaceMultiplexerMutationRequestSchemaZ,
15
+ WorkspacePaneCreateMutationRequestSchemaZ,
16
+ WorkspaceSessionCreateArgumentsSchemaZ,
17
+ } from "@tmux-ide/contracts";
18
+ import {
19
+ TmuxServerOwners,
20
+ TmuxServerRegistrationSchemaZ,
21
+ TmuxServerScopeError,
22
+ } from "../lib/tmux-server-owners.ts";
23
+ import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
24
+ import { WorkspaceMultiplexerError } from "../lib/workspace-multiplexer-verbs.ts";
25
+ import { ownerBearerMatches } from "./owner-authority.ts";
26
+ import { mountPaneStreamIssueRoute } from "./pane-stream-issue.ts";
27
+ import { projectTerminalRuntimeInventory } from "./resources/terminal-runtime-inventory.ts";
28
+
29
+ export interface TmuxServerRoutesOptions {
30
+ readonly ownerToken: string | null;
31
+ readonly owners: TmuxServerOwners<NativeTmuxServerOwner>;
32
+ }
33
+ const registrationSchema = TmuxServerRegistrationSchemaZ.omit({ serverId: true }).strict();
34
+ const workspaceSchema = z
35
+ .string()
36
+ .min(1)
37
+ .max(160)
38
+ .refine((value) => !/[\0\r\n]/u.test(value));
39
+ async function boundedJson(request: Request): Promise<unknown> {
40
+ if (!request.body) throw new TypeError("Missing body");
41
+ const reader = request.body.getReader();
42
+ const chunks: Uint8Array[] = [];
43
+ let size = 0;
44
+ try {
45
+ for (;;) {
46
+ const next = await reader.read();
47
+ if (next.done) break;
48
+ size += next.value.byteLength;
49
+ if (size > 64 * 1024) {
50
+ void reader.cancel().catch(() => undefined);
51
+ throw new TypeError("Body too large");
52
+ }
53
+ chunks.push(next.value);
54
+ }
55
+ } finally {
56
+ reader.releaseLock();
57
+ }
58
+ const bytes = new Uint8Array(size);
59
+ let offset = 0;
60
+ for (const chunk of chunks) {
61
+ bytes.set(chunk, offset);
62
+ offset += chunk.byteLength;
63
+ }
64
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
65
+ }
66
+ function failure(c: Context, error: unknown): Response {
67
+ if (error instanceof TmuxServerScopeError)
68
+ return c.json(
69
+ { error: { code: error.code } },
70
+ error.code === "not-found" ? 404 : error.code === "capacity" ? 429 : 409,
71
+ );
72
+ if (error instanceof WorkspaceMultiplexerError)
73
+ return c.json({ error: { code: error.code } }, 409);
74
+ if (error instanceof z.ZodError || error instanceof SyntaxError || error instanceof TypeError)
75
+ return c.json({ error: { code: "invalid-request" } }, 400);
76
+ return c.json({ error: { code: "server-unavailable" } }, 503);
77
+ }
78
+ /** Owner capability is mandatory even when the ordinary local HTTP surface is open. */
79
+ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOptions): void {
80
+ const base = TMUX_SERVERS_API_PATH;
81
+ const authorize = async (c: Context, next: () => Promise<void>) => {
82
+ c.header("Cache-Control", "no-store");
83
+ if (!ownerBearerMatches(c.req.header("Authorization"), options.ownerToken))
84
+ return c.json({ error: { code: "owner-required" } }, 401);
85
+ await next();
86
+ };
87
+ app.use(base, authorize);
88
+ app.use(`${base}/*`, authorize);
89
+ const route = (work: (c: Context) => Promise<Response>) => async (c: Context) => {
90
+ try {
91
+ return await work(c);
92
+ } catch (error) {
93
+ return failure(c, error);
94
+ }
95
+ };
96
+ const scope = (c: Context) =>
97
+ TmuxServerScopeSchemaZ.parse({
98
+ serverId: c.req.param("serverId"),
99
+ generation: c.req.param("generation"),
100
+ });
101
+ app.get(
102
+ base,
103
+ route(async (c) =>
104
+ c.json(
105
+ TmuxServersResourceSchemaZ.parse({ version: 1, servers: await options.owners.refresh() }),
106
+ ),
107
+ ),
108
+ );
109
+ app.post(
110
+ base,
111
+ route(async (c) =>
112
+ c.json(
113
+ await options.owners.register(registrationSchema.parse(await boundedJson(c.req.raw))),
114
+ 201,
115
+ ),
116
+ ),
117
+ );
118
+ app.delete(
119
+ `${base}/:serverId`,
120
+ route(async (c) => {
121
+ await options.owners.remove(TmuxServerIdSchemaZ.parse(c.req.param("serverId")));
122
+ return c.json({ removed: true });
123
+ }),
124
+ );
125
+ const scoped = `${base}/:serverId/:generation`;
126
+ mountTmuxServerNativeBackingRoute(app, {
127
+ scopedPath: scoped,
128
+ ownerToken: options.ownerToken,
129
+ owners: options.owners,
130
+ });
131
+ app.post(
132
+ `${scoped}/sessions/create`,
133
+ route(async (c) => {
134
+ const server = scope(c);
135
+ const request = z
136
+ .object({
137
+ operationId: z.uuid(),
138
+ expectedDaemonInstanceId: z.uuid(),
139
+ intent: WorkspaceSessionCreateArgumentsSchemaZ,
140
+ })
141
+ .strict()
142
+ .parse(await boundedJson(c.req.raw));
143
+ if (
144
+ request.expectedDaemonInstanceId !== server.generation ||
145
+ (request.intent.expectedDaemonInstanceId &&
146
+ request.intent.expectedDaemonInstanceId !== server.generation)
147
+ )
148
+ return c.json({ error: { code: "stale-generation" } }, 409);
149
+ return c.json(
150
+ await options.owners.withOwner(server, (owner) =>
151
+ owner.createSession(request.operationId, {
152
+ ...request.intent,
153
+ expectedDaemonInstanceId: server.generation,
154
+ }),
155
+ ),
156
+ );
157
+ }),
158
+ );
159
+ app.post(
160
+ `${scoped}/sessions/:liveSessionId/open`,
161
+ route(async (c) => {
162
+ const server = scope(c);
163
+ const result = await options.owners.withOwner(server, (owner) =>
164
+ owner.openSession(workspaceSchema.parse(c.req.param("liveSessionId"))),
165
+ );
166
+ return c.json({ version: 1, server, ...result });
167
+ }),
168
+ );
169
+ app.get(
170
+ `${scoped}/sessions`,
171
+ route(async (c) => {
172
+ const server = scope(c);
173
+ return c.json(
174
+ TmuxServerSessionsResourceSchemaZ.parse({
175
+ version: 1,
176
+ server,
177
+ sessions: await options.owners.withOwner(server, async (owner) => {
178
+ const sessions = await owner.catalog();
179
+ const workspaces = owner.workspaceRegistry.list();
180
+ return sessions.map((session) => {
181
+ const aliases = workspaces.filter(
182
+ (workspace) => workspace.sessionName === session.sessionName,
183
+ );
184
+ return { ...session, workspaceName: aliases.length === 1 ? aliases[0]!.name : null };
185
+ });
186
+ }),
187
+ }),
188
+ );
189
+ }),
190
+ );
191
+ app.get(
192
+ `${scoped}/inventory/:workspaceName`,
193
+ route(async (c) => {
194
+ const server = scope(c);
195
+ const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
196
+ const resource = await options.owners.withOwner(server, async (owner) => {
197
+ const sessions = await owner.catalog();
198
+ const workspace = owner.workspaceRegistry.get(workspaceName);
199
+ if (!workspace) return null;
200
+ const expected = c.req.query("liveSessionId");
201
+ if (
202
+ expected &&
203
+ !sessions.some(
204
+ (row) => row.sessionName === workspace.sessionName && row.liveSessionId === expected,
205
+ )
206
+ )
207
+ throw new TmuxServerScopeError("stale-generation");
208
+ const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
209
+ workspace.sessionName,
210
+ c.req.raw.signal,
211
+ );
212
+ return snapshot ? projectTerminalRuntimeInventory(snapshot, 0) : null;
213
+ });
214
+ return resource
215
+ ? c.json({ version: 1, server, resource })
216
+ : c.json({ error: { code: "workspace-not-found" } }, 404);
217
+ }),
218
+ );
219
+ app.get(
220
+ `${scoped}/catalog`,
221
+ route(async (c) => {
222
+ const server = scope(c);
223
+ const resource = await options.owners.withOwner(server, async (owner) => {
224
+ const sessions = await owner.catalog();
225
+ return {
226
+ intents: owner.workspaceRegistry.list().map((entry) => ({
227
+ workspaceName: entry.name,
228
+ sessionName: entry.sessionName,
229
+ source: "workspace",
230
+ availability: sessions.some((row) => row.sessionName === entry.sessionName)
231
+ ? "live"
232
+ : "stopped",
233
+ })),
234
+ liveSessions: sessions.map((row) => ({
235
+ sessionName: row.sessionName,
236
+ fleetSessionId: fleetSessionIdForName(row.sessionName),
237
+ paneCount: row.paneCount,
238
+ })),
239
+ };
240
+ });
241
+ return c.json({ version: 1, server, resource });
242
+ }),
243
+ );
244
+ app.get(
245
+ `${scoped}/application-shell/:workspaceName`,
246
+ route(async (c) => {
247
+ const server = scope(c);
248
+ const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
249
+ const resource = await options.owners.withOwner(server, async (owner) => {
250
+ const sessions = await owner.catalog();
251
+ const workspace = owner.workspaceRegistry.get(workspaceName);
252
+ if (!workspace) return null;
253
+ if (
254
+ !sessions.some(
255
+ (row) =>
256
+ row.sessionName === workspace.sessionName &&
257
+ row.liveSessionId === c.req.query("liveSessionId"),
258
+ )
259
+ )
260
+ throw new TmuxServerScopeError("stale-generation");
261
+ const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
262
+ workspace.sessionName,
263
+ c.req.raw.signal,
264
+ );
265
+ // Discovery can yield while a same-name session is replaced. Revalidate
266
+ // the captured incarnation before publishing agent metadata as current.
267
+ const currentSessions = await owner.catalog();
268
+ if (
269
+ !currentSessions.some(
270
+ (row) =>
271
+ row.sessionName === workspace.sessionName &&
272
+ row.liveSessionId === c.req.query("liveSessionId"),
273
+ )
274
+ )
275
+ throw new TmuxServerScopeError("stale-generation");
276
+ return snapshot ? projectApplicationShellResource(snapshot) : null;
277
+ });
278
+ return resource
279
+ ? c.json({ version: 1, server, resource })
280
+ : c.json({ error: { code: "workspace-not-found" } }, 404);
281
+ }),
282
+ );
283
+ // Subscribe before publishing ready: a topology change during the subsequent HTTP read
284
+ // invalidates that read. This shares the owner's retained mirror, never a new tmux client.
285
+ app.get(
286
+ `${scoped}/session-events/:workspaceName`,
287
+ route(async (c) => {
288
+ const server = scope(c);
289
+ const workspaceName = workspaceSchema.parse(c.req.param("workspaceName"));
290
+ const owner = await options.owners.withOwner(server, async (candidate) => {
291
+ const sessions = await candidate.catalog();
292
+ const workspace = candidate.workspaceRegistry.get(workspaceName);
293
+ if (
294
+ !workspace ||
295
+ !sessions.some(
296
+ (row) =>
297
+ row.sessionName === workspace.sessionName &&
298
+ row.liveSessionId === c.req.query("liveSessionId"),
299
+ )
300
+ )
301
+ throw new TmuxServerScopeError("stale-generation");
302
+ return candidate;
303
+ });
304
+ const workspace = owner.workspaceRegistry.get(workspaceName)!;
305
+ return streamSSE(c, async (stream) => {
306
+ let stopped = false;
307
+ let wake: (() => void) | null = null;
308
+ let revision = 0;
309
+ const dirty = () => {
310
+ revision++;
311
+ wake?.();
312
+ };
313
+ stream.onAbort(() => {
314
+ stopped = true;
315
+ wake?.();
316
+ });
317
+ const identity = await owner.sessionRuntimeRegistry.describeSessionAuthority(
318
+ workspace.sessionName,
319
+ );
320
+ let topology: string | null = null;
321
+ let sessionReplaced = false;
322
+ const subscription = await owner.sessionRuntimeRegistry.subscribeLayout(
323
+ workspace.sessionName,
324
+ () => undefined,
325
+ {
326
+ expectedRuntimeSessionId: identity.runtimeSessionId,
327
+ expectedSemanticPaneIds: identity.description.panes.map((pane) => pane.semanticPaneId),
328
+ onAuthority(snapshot) {
329
+ if (snapshot.windowLinks.liveSessionId !== c.req.query("liveSessionId")) {
330
+ sessionReplaced = true;
331
+ dirty();
332
+ return;
333
+ }
334
+ // Full replacement observes removed windows and retains no historical keys.
335
+ // Geometry, focus and active-link changes stay entirely on the pane stream.
336
+ const signature = JSON.stringify(
337
+ snapshot.layouts
338
+ .map((layout) => [
339
+ layout.semanticWindowId,
340
+ layout.windowName,
341
+ layout.panes.map((pane) => pane.semanticPaneId).sort(),
342
+ ])
343
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0]))),
344
+ );
345
+ if (topology === signature) return;
346
+ topology = signature;
347
+ dirty();
348
+ },
349
+ },
350
+ );
351
+ try {
352
+ options.owners.current(server);
353
+ if (sessionReplaced) throw new TmuxServerScopeError("stale-generation");
354
+ await stream.writeSSE({
355
+ data: JSON.stringify({ version: 1, server, type: "ready", revision }),
356
+ });
357
+ let sent = revision;
358
+ let nextIdentityCheck = Date.now() + 5000;
359
+ while (!stopped) {
360
+ await new Promise<void>((resolve) => {
361
+ const timer = setTimeout(resolve, 1000);
362
+ wake = () => {
363
+ clearTimeout(timer);
364
+ resolve();
365
+ };
366
+ if (stopped || revision !== sent) wake();
367
+ });
368
+ wake = null;
369
+ if (stopped) break;
370
+ if (sessionReplaced) throw new TmuxServerScopeError("stale-generation");
371
+ options.owners.current(server);
372
+ if (Date.now() >= nextIdentityCheck) {
373
+ const sessions = await owner.catalog();
374
+ options.owners.current(server);
375
+ if (
376
+ !sessions.some(
377
+ (row) =>
378
+ row.sessionName === workspace.sessionName &&
379
+ row.liveSessionId === c.req.query("liveSessionId"),
380
+ )
381
+ )
382
+ throw new TmuxServerScopeError("stale-generation");
383
+ nextIdentityCheck = Date.now() + 5000;
384
+ }
385
+ if (revision !== sent) {
386
+ sent = revision;
387
+ await stream.writeSSE({
388
+ data: JSON.stringify({ version: 1, server, type: "invalidate", revision }),
389
+ });
390
+ } else
391
+ await stream.writeSSE({
392
+ data: JSON.stringify({ version: 1, server, type: "heartbeat", revision }),
393
+ });
394
+ }
395
+ } catch {
396
+ if (!stopped)
397
+ await stream
398
+ .writeSSE({ data: JSON.stringify({ version: 1, server, type: "retired", revision }) })
399
+ .catch(() => undefined);
400
+ } finally {
401
+ await subscription.close();
402
+ }
403
+ });
404
+ }),
405
+ );
406
+ app.post(
407
+ `${scoped}/panes/create`,
408
+ route(async (c) => {
409
+ const server = scope(c);
410
+ const liveSessionId = workspaceSchema.parse(c.req.query("liveSessionId"));
411
+ const request = WorkspacePaneCreateMutationRequestSchemaZ.parse(await boundedJson(c.req.raw));
412
+ if (request.expectedDaemonInstanceId !== server.generation)
413
+ return c.json({ error: { code: "stale-generation" } }, 409);
414
+ return c.json(
415
+ await options.owners.withOwner(server, (owner) =>
416
+ owner.createSessionPane(liveSessionId, request),
417
+ ),
418
+ );
419
+ }),
420
+ );
421
+ app.post(
422
+ `${scoped}/mutations`,
423
+ route(async (c) => {
424
+ const server = scope(c);
425
+ const request = WorkspaceMultiplexerMutationRequestSchemaZ.parse(
426
+ await boundedJson(c.req.raw),
427
+ );
428
+ if (request.expectedDaemonInstanceId !== server.generation)
429
+ return c.json({ error: { code: "stale-generation" } }, 409);
430
+ const liveSessionId = c.req.query("liveSessionId");
431
+ const result = await options.owners.withOwner(server, (owner) =>
432
+ liveSessionId
433
+ ? owner.mutateSession(liveSessionId, request, c.req.header("X-Tmux-Ide-Host-Client-Id"))
434
+ : owner.multiplexerBackend.mutate(
435
+ request,
436
+ c.req.header("X-Tmux-Ide-Host-Client-Id"),
437
+ undefined,
438
+ true,
439
+ ),
440
+ );
441
+ return c.json(result);
442
+ }),
443
+ );
444
+ app.post(
445
+ `${scoped}/pane-streams/issue`,
446
+ route(async (c) => {
447
+ const server = scope(c);
448
+ return options.owners.withOwner(server, async (owner) => {
449
+ const sessions = await owner.catalog();
450
+ const liveSessionId = c.req.query("liveSessionId");
451
+ if (liveSessionId) {
452
+ const body = (await boundedJson(c.req.raw.clone())) as {
453
+ stream?: { workspaceName?: string };
454
+ };
455
+ const workspace = owner.workspaceRegistry.get(body.stream?.workspaceName ?? "");
456
+ if (
457
+ !workspace ||
458
+ !sessions.some(
459
+ (row) =>
460
+ row.sessionName === workspace.sessionName && row.liveSessionId === liveSessionId,
461
+ )
462
+ )
463
+ throw new TmuxServerScopeError("stale-generation");
464
+ }
465
+ const issueApp = new Hono();
466
+ mountPaneStreamIssueRoute(issueApp, {
467
+ daemonInstanceId: server.generation,
468
+ ownerToken: options.ownerToken,
469
+ workspaceRegistry: owner.workspaceRegistry,
470
+ backend: owner.paneStreamRuntime.coordinator,
471
+ });
472
+ const url = new URL(c.req.url);
473
+ url.pathname = PANE_STREAM_ISSUE_PATH;
474
+ url.search = "";
475
+ return await issueApp.request(new Request(url, c.req.raw));
476
+ });
477
+ }),
478
+ );
479
+ }