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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/bin/cli.js +52253 -49070
  2. package/package.json +1 -1
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
  4. package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
  5. package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
  6. package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
  7. package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
  8. package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
  9. package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
  10. package/packages/contracts/src/actions-contract.ts +12 -0
  11. package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
  12. package/packages/contracts/src/daemon-wire.ts +10 -5
  13. package/packages/contracts/src/fleet-client-state.ts +3 -0
  14. package/packages/contracts/src/index.ts +2 -0
  15. package/packages/contracts/src/interaction-receipts.ts +39 -0
  16. package/packages/contracts/src/issue-error.ts +1 -1
  17. package/packages/contracts/src/multiplexer-verbs.ts +25 -1
  18. package/packages/contracts/src/pane-stream.ts +51 -8
  19. package/packages/contracts/src/tmux-server-scope.ts +172 -0
  20. package/packages/contracts/src/window-links.ts +76 -0
  21. package/packages/contracts/src/workspace-multiplexer.ts +54 -0
  22. package/packages/core/src/interaction-receipts.ts +4 -0
  23. package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
  24. package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
  25. package/packages/daemon/dist/command-center/actions/registry.js +13 -1
  26. package/packages/daemon/dist/command-center/discovery.js +2 -9
  27. package/packages/daemon/dist/command-center/server.js +21 -1
  28. package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
  29. package/packages/daemon/dist/command-center/tmux-servers.js +345 -0
  30. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
  31. package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
  32. package/packages/daemon/dist/lib/daemon-embed.js +198 -30
  33. package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
  34. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +16 -10
  35. package/packages/daemon/dist/lib/headless-daemon.js +10 -1
  36. package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
  37. package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
  38. package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
  39. package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
  40. package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
  41. package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
  42. package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
  43. package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
  44. package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
  45. package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
  46. package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
  47. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
  48. package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
  49. package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
  50. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
  51. package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
  52. package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
  53. package/packages/daemon/dist/lib/workspace-registry.js +4 -0
  54. package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
  55. package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
  56. package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
  57. package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
  58. package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
  59. package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
  60. package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
  61. package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
  62. package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
  63. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
  64. package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
  65. package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
  66. package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
  67. package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
  68. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
  69. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
  70. package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
  71. package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
  72. package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
  73. package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
  74. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
  75. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
  76. package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
  77. package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
  78. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
  79. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
  80. package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
  81. package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
  82. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
  83. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
  84. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
  85. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +42 -1
  86. package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
  87. package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
  88. package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
  89. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
  90. package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
  91. package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
  92. package/packages/daemon/src/command-center/actions/errors.ts +4 -0
  93. package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
  94. package/packages/daemon/src/command-center/actions/registry.ts +14 -0
  95. package/packages/daemon/src/command-center/discovery.ts +2 -10
  96. package/packages/daemon/src/command-center/server.ts +29 -1
  97. package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
  98. package/packages/daemon/src/command-center/tmux-servers.ts +468 -0
  99. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
  100. package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
  101. package/packages/daemon/src/lib/daemon-embed.ts +241 -28
  102. package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
  103. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +16 -8
  104. package/packages/daemon/src/lib/headless-daemon.ts +18 -1
  105. package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
  106. package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
  107. package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
  108. package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
  109. package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
  110. package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
  111. package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
  112. package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
  113. package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
  114. package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
  115. package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
  116. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
  117. package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
  118. package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
  119. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
  120. package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
  121. package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
  122. package/packages/daemon/src/lib/workspace-registry.ts +5 -0
  123. package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
  124. package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
  125. package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
  126. package/packages/daemon/src/terminal/mirror/events.ts +2 -0
  127. package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
  128. package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
  129. package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
  130. package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
  131. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
  132. package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
  133. package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
  134. package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
  135. package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
  136. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
  137. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
  138. package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
  139. package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
  140. package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
  141. package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
  142. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
  143. package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
  144. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
  145. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
  146. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
  147. package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
  148. package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
  149. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -36
  150. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
  151. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
  152. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
  153. package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
  154. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
  155. package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
  156. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -0
  157. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
  158. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
  159. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
  160. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +52 -0
  161. package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
  162. package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
  163. package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
  164. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
  165. package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
  166. package/packages/daemon-client/package.json +3 -1
  167. package/packages/daemon-client/src/index.ts +2 -0
  168. package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
  169. package/packages/daemon-client/src/pane-stream-client.ts +26 -0
  170. package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
  171. package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
  172. package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
  173. package/packages/daemon-client/src/tmux-server-client.ts +287 -0
@@ -1,3 +1,4 @@
1
+ import { WindowLinkTargetSchemaZ } from "./window-links.ts";
1
2
  import { z } from "zod";
2
3
 
3
4
  import { DesktopWorkspaceNameSchemaZ } from "./desktop-workspace-name.ts";
@@ -14,6 +15,8 @@ export type AuthoredInteractionOrigin = z.infer<typeof AuthoredInteractionOrigin
14
15
 
15
16
  /** Every semantic session-runtime verb; raw tmux addresses never enter this vocabulary. */
16
17
  export const InteractionOperationKindSchemaZ = z.enum([
18
+ "workspace.window.link.select",
19
+ "workspace.window.link.unlink",
17
20
  "workspace.window.split",
18
21
  "workspace.window.kill",
19
22
  "workspace.pane.kill",
@@ -46,6 +49,7 @@ const InteractionWindowReferenceSchemaZ = z.discriminatedUnion("by", [
46
49
 
47
50
  /** Stable semantic target retained even when the operation removes or renames it. */
48
51
  export const InteractionTargetSchemaZ = z.discriminatedUnion("kind", [
52
+ z.object({ kind: z.literal("window-link"), target: WindowLinkTargetSchemaZ }).strict(),
49
53
  z.object({ kind: z.literal("session") }).strict(),
50
54
  z.object({ kind: z.literal("window"), target: InteractionWindowReferenceSchemaZ }).strict(),
51
55
  z
@@ -63,6 +67,8 @@ const MutationOutcomeSchemaZ = z.enum(["applied", "unchanged", "replayed"]);
63
67
  * different verb.
64
68
  */
65
69
  export const InteractionSafeSummarySchemaZ = z.union([
70
+ z.object({ operationKind: z.literal("workspace.window.link.unlink") }).strict(),
71
+ z.object({ operationKind: z.literal("workspace.window.link.select") }).strict(),
66
72
  z
67
73
  .object({
68
74
  operationKind: z.literal("workspace.window.split"),
@@ -129,6 +135,20 @@ export type PaneReadSafeSummary = Extract<
129
135
  * terminal bytes never enter the replay journal.
130
136
  */
131
137
  export const InteractionProofSchemaZ = z.discriminatedUnion("operationKind", [
138
+ z
139
+ .object({
140
+ operationKind: z.literal("workspace.window.link.unlink"),
141
+ outcome: MutationOutcomeSchemaZ,
142
+ target: WindowLinkTargetSchemaZ,
143
+ })
144
+ .strict(),
145
+ z
146
+ .object({
147
+ operationKind: z.literal("workspace.window.link.select"),
148
+ outcome: MutationOutcomeSchemaZ,
149
+ target: WindowLinkTargetSchemaZ,
150
+ })
151
+ .strict(),
132
152
  z
133
153
  .object({
134
154
  operationKind: z.literal("workspace.window.split"),
@@ -280,6 +300,25 @@ export const InteractionReceiptSchemaZ = z
280
300
  message: "external observations require a presence-only summary",
281
301
  });
282
302
  }
303
+ if (receipt.operationKind.startsWith("workspace.window.link.")) {
304
+ if (receipt.target.kind !== "window-link") {
305
+ context.addIssue({
306
+ code: "custom",
307
+ path: ["target"],
308
+ message: "Window link verbs require a link target",
309
+ });
310
+ } else if (
311
+ receipt.proof &&
312
+ "target" in receipt.proof &&
313
+ JSON.stringify(receipt.proof.target) !== JSON.stringify(receipt.target.target)
314
+ ) {
315
+ context.addIssue({
316
+ code: "custom",
317
+ path: ["proof"],
318
+ message: "Window link proof must match its target",
319
+ });
320
+ }
321
+ }
283
322
  const paneVerb = receipt.operationKind.startsWith("workspace.pane.");
284
323
  if (paneVerb && receipt.target.kind !== "pane") {
285
324
  context.addIssue({
@@ -84,7 +84,7 @@ export const RendererSafeIssueReasonSchemaZ = z
84
84
  .max(240)
85
85
  .refine(
86
86
  (reason) =>
87
- !/(?:authorization|bearer\s+|owner.?token|redemptionticket|ps1_|ta1_)/iu.test(reason),
87
+ !/(?:authorization|bearer\s+|owner.?token|redemptionticket|ps[12]_|ta1_)/iu.test(reason),
88
88
  "issue error reason must be credential-redacted",
89
89
  );
90
90
 
@@ -118,6 +118,29 @@ function deepFreeze<Value>(value: Value): Value {
118
118
  }
119
119
 
120
120
  const ENTRIES: readonly MultiplexerVerbEntry[] = [
121
+ {
122
+ version: MULTIPLEXER_VERB_TABLE_VERSION,
123
+ id: "window.link.select",
124
+ label: "Select window link",
125
+ description: "Select this link without changing the shared window's active pane.",
126
+ scope: "window",
127
+ execution: { kind: "daemon-action", action: "workspace.window.link.select" },
128
+ availabilityInputs: ["workspaceConnected"],
129
+ destructive: false,
130
+ tmuxKeyHint: null,
131
+ },
132
+ {
133
+ version: MULTIPLEXER_VERB_TABLE_VERSION,
134
+ id: "window.link.unlink",
135
+ label: "Unlink window",
136
+ description:
137
+ "Remove this session link; preserve other links and their panes. Native last-link restrictions apply.",
138
+ scope: "window",
139
+ execution: { kind: "daemon-action", action: "workspace.window.link.unlink" },
140
+ availabilityInputs: ["workspaceConnected"],
141
+ destructive: true,
142
+ tmuxKeyHint: null,
143
+ },
121
144
  {
122
145
  version: MULTIPLEXER_VERB_TABLE_VERSION,
123
146
  id: "session.new",
@@ -179,7 +202,8 @@ const ENTRIES: readonly MultiplexerVerbEntry[] = [
179
202
  version: MULTIPLEXER_VERB_TABLE_VERSION,
180
203
  id: "window.kill",
181
204
  label: "Close window",
182
- description: "Kill this tmux window and every pane in it. Refused for a session's last window.",
205
+ description:
206
+ "Kill this shared tmux window and its panes in every linked session. Refused for a session's last window.",
183
207
  scope: "window",
184
208
  execution: { kind: "daemon-action", action: "workspace.window.kill" },
185
209
  availabilityInputs: ["workspaceConnected", "sessionWindowCount"],
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { isTmuxServerPaneStreamPath } from "./tmux-server-scope.ts";
2
3
  import {
3
4
  CausalCellCapabilitySchemaZ,
4
5
  CausalCellFailureV1SchemaZ,
@@ -41,6 +42,7 @@ import {
41
42
  SessionRuntimeTerminalTextInputSchemaZ,
42
43
  } from "./session-runtime.ts";
43
44
  import { WorkspaceMultiplexerMutationResultSchemaZ } from "./workspace-multiplexer.ts";
45
+ import { WindowLinkTopologySchemaZ } from "./window-links.ts";
44
46
 
45
47
  export const PANE_STREAM_CLOCK_BOUNDS_CAPABILITY_V1 = "clock-bounds-v1" as const;
46
48
  export const PaneStreamDiagnosticCapabilitySchemaZ = z.union([
@@ -55,7 +57,7 @@ export type PaneStreamDiagnosticCapability = z.infer<typeof PaneStreamDiagnostic
55
57
  *
56
58
  * The discipline is the terminal-attachment one, applied to mirror streams:
57
59
  * semantic identity only (runtime `%N`/`@N`/`$N` addresses never cross this
58
- * boundary), a one-time `ps1_` redemption ticket bound to one daemon
60
+ * boundary), a one-time `ps2_` redemption ticket bound to one daemon
59
61
  * generation, and bounded shapes everywhere. Two deliberate differences,
60
62
  * both recorded product decisions:
61
63
  *
@@ -65,11 +67,11 @@ export type PaneStreamDiagnosticCapability = z.infer<typeof PaneStreamDiagnostic
65
67
  * (the control client casts no size vote), so a renderer has no geometry
66
68
  * to declare.
67
69
  */
68
- export const PANE_STREAM_PROTOCOL_VERSION = 1 as const;
70
+ export const PANE_STREAM_PROTOCOL_VERSION = 2 as const;
69
71
 
70
- export const PANE_STREAM_ISSUE_PATH = "/api/v1/terminal/pane-streams/issue" as const;
71
- export const PANE_STREAM_REDEEM_PATH = "/v1/terminal/pane-streams/redeem" as const;
72
- export const PANE_STREAM_WEBSOCKET_SUBPROTOCOL = "tmux-ide-pane-stream.v1" as const;
72
+ export const PANE_STREAM_ISSUE_PATH = "/api/v2/terminal/pane-streams/issue" as const;
73
+ export const PANE_STREAM_REDEEM_PATH = "/v2/terminal/pane-streams/redeem" as const;
74
+ export const PANE_STREAM_WEBSOCKET_SUBPROTOCOL = "tmux-ide-pane-stream.v2" as const;
73
75
 
74
76
  /** Perf-plan ceiling (~16-24 live nodes) with headroom; enforced at issue. */
75
77
  export const PANE_STREAM_MAX_PANES = 24;
@@ -115,7 +117,7 @@ export const PaneStreamLeaseRequestSchemaZ = z
115
117
  .strict();
116
118
  export type PaneStreamLeaseRequest = z.infer<typeof PaneStreamLeaseRequestSchemaZ>;
117
119
 
118
- export const PaneStreamRedemptionTicketSchemaZ = z.string().regex(/^ps1_[A-Za-z0-9_-]{43}$/u);
120
+ export const PaneStreamRedemptionTicketSchemaZ = z.string().regex(/^ps2_[A-Za-z0-9_-]{43}$/u);
119
121
 
120
122
  /** Same canonical loopback discipline as the terminal-attachment endpoint. */
121
123
  export const PaneStreamLoopbackWebSocketUrlSchemaZ = z
@@ -129,7 +131,7 @@ export const PaneStreamLoopbackWebSocketUrlSchemaZ = z
129
131
  url.port.length > 0 &&
130
132
  url.username.length === 0 &&
131
133
  url.password.length === 0 &&
132
- url.pathname === PANE_STREAM_REDEEM_PATH &&
134
+ (url.pathname === PANE_STREAM_REDEEM_PATH || isTmuxServerPaneStreamPath(url.pathname)) &&
133
135
  url.search.length === 0 &&
134
136
  url.hash.length === 0 &&
135
137
  url.toString() === value
@@ -471,7 +473,7 @@ export const PaneStreamLayoutFrameSchemaZ = z
471
473
  })
472
474
  .strict();
473
475
 
474
- export const PaneStreamLayoutSnapshotFrameSchemaZ = z
476
+ export const PaneStreamLayoutSnapshotV1FrameSchemaZ = z
475
477
  .object({
476
478
  type: z.literal("layout-snapshot"),
477
479
  topologyEpoch: z.number().int().nonnegative(),
@@ -526,6 +528,47 @@ export const PaneStreamLayoutSnapshotFrameSchemaZ = z
526
528
  }
527
529
  });
528
530
 
531
+ /**
532
+ * Atomic v2 grammar: backing layouts and session links share one snapshot.
533
+ */
534
+ export const PaneStreamLayoutSnapshotV2FrameSchemaZ =
535
+ PaneStreamLayoutSnapshotV1FrameSchemaZ.safeExtend({
536
+ windowLinks: WindowLinkTopologySchemaZ,
537
+ }).superRefine((snapshot, context) => {
538
+ const backings = new Map(snapshot.layouts.map((layout) => [layout.semanticWindowId, layout]));
539
+ const linkedBackings = new Set<string>();
540
+ for (const [index, link] of snapshot.windowLinks.links.entries()) {
541
+ linkedBackings.add(link.semanticWindowId);
542
+ const backing = backings.get(link.semanticWindowId);
543
+ if (!backing) {
544
+ context.addIssue({
545
+ code: "custom",
546
+ message: "Window links require a known backing layout",
547
+ path: ["windowLinks", "links", index, "semanticWindowId"],
548
+ });
549
+ } else if (link.linkId === snapshot.windowLinks.activeLinkId && !backing.currentWindow) {
550
+ context.addIssue({
551
+ code: "custom",
552
+ message: "The current backing must own the active window link",
553
+ path: ["windowLinks", "activeLinkId"],
554
+ });
555
+ }
556
+ }
557
+ for (const [index, layout] of snapshot.layouts.entries()) {
558
+ if (layout.semanticWindowId !== null && !linkedBackings.has(layout.semanticWindowId)) {
559
+ context.addIssue({
560
+ code: "custom",
561
+ message: "Every backing layout requires a window link",
562
+ path: ["layouts", index, "semanticWindowId"],
563
+ });
564
+ }
565
+ }
566
+ });
567
+ export const PaneStreamLayoutSnapshotFrameSchemaZ = PaneStreamLayoutSnapshotV2FrameSchemaZ;
568
+ export type PaneStreamLayoutSnapshotV2Frame = z.infer<
569
+ typeof PaneStreamLayoutSnapshotV2FrameSchemaZ
570
+ >;
571
+
529
572
  export const PaneStreamFlowFrameSchemaZ = z
530
573
  .object({
531
574
  type: z.literal("flow"),
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ import {
3
+ TerminalAttachmentSemanticPaneIdSchemaZ,
4
+ TerminalAttachmentSemanticWindowIdSchemaZ,
5
+ } from "./semantic-identity.ts";
6
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "./workspace-catalog-resource.ts";
7
+
8
+ /** Stable registration identity. Never derive it from a socket path or native ID. */
9
+ export const TmuxServerIdSchemaZ = z.string().regex(/^tmux-server\.[a-f0-9]{32}$/u);
10
+ /** Fresh live authority incarnation, independently retired for each server owner. */
11
+ export const TmuxServerGenerationSchemaZ = z.uuid();
12
+ export const TmuxServerScopeSchemaZ = z
13
+ .object({ serverId: TmuxServerIdSchemaZ, generation: TmuxServerGenerationSchemaZ })
14
+ .strict();
15
+
16
+ const descriptorShape = {
17
+ serverId: TmuxServerIdSchemaZ,
18
+ label: z
19
+ .string()
20
+ .trim()
21
+ .min(1)
22
+ .max(160)
23
+ .refine(
24
+ (value) =>
25
+ [...value].every(
26
+ (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127,
27
+ ),
28
+ "server label contains control characters",
29
+ ),
30
+ };
31
+ export const TmuxServerDescriptorSchemaZ = z.discriminatedUnion("state", [
32
+ z
33
+ .object({
34
+ ...descriptorShape,
35
+ state: z.literal("online"),
36
+ generation: TmuxServerGenerationSchemaZ,
37
+ })
38
+ .strict(),
39
+ z.object({ ...descriptorShape, state: z.literal("offline"), generation: z.null() }).strict(),
40
+ ]);
41
+
42
+ /** Explicitly versioned endpoint; this is not an additive field on legacy wire resources. */
43
+ export const TMUX_SERVERS_API_PATH = "/api/v1/tmux-servers" as const;
44
+ export const TmuxServersResourceSchemaZ = z
45
+ .object({
46
+ version: z.literal(1),
47
+ servers: z.array(TmuxServerDescriptorSchemaZ),
48
+ })
49
+ .strict()
50
+ .superRefine((resource, context) => {
51
+ const seen = new Set<string>();
52
+ for (const [index, server] of resource.servers.entries()) {
53
+ if (seen.has(server.serverId))
54
+ context.addIssue({
55
+ code: "custom",
56
+ path: ["servers", index, "serverId"],
57
+ message: "Server registrations must be unique",
58
+ });
59
+ seen.add(server.serverId);
60
+ }
61
+ });
62
+ export const TmuxServerSessionTargetSchemaZ = z
63
+ .object({
64
+ server: TmuxServerScopeSchemaZ,
65
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
66
+ })
67
+ .strict();
68
+ export const TmuxServerWindowTargetSchemaZ = TmuxServerSessionTargetSchemaZ.extend({
69
+ semanticWindowId: TerminalAttachmentSemanticWindowIdSchemaZ,
70
+ });
71
+ export const TmuxServerPaneTargetSchemaZ = TmuxServerSessionTargetSchemaZ.extend({
72
+ semanticPaneId: TerminalAttachmentSemanticPaneIdSchemaZ,
73
+ });
74
+
75
+ export type TmuxServerId = z.infer<typeof TmuxServerIdSchemaZ>;
76
+ export type TmuxServerGeneration = z.infer<typeof TmuxServerGenerationSchemaZ>;
77
+ export type TmuxServerScope = z.infer<typeof TmuxServerScopeSchemaZ>;
78
+ export type TmuxServerDescriptor = z.infer<typeof TmuxServerDescriptorSchemaZ>;
79
+ export type TmuxServersResource = z.infer<typeof TmuxServersResourceSchemaZ>;
80
+ export type TmuxServerSessionTarget = z.infer<typeof TmuxServerSessionTargetSchemaZ>;
81
+ export type TmuxServerWindowTarget = z.infer<typeof TmuxServerWindowTargetSchemaZ>;
82
+ export type TmuxServerPaneTarget = z.infer<typeof TmuxServerPaneTargetSchemaZ>;
83
+
84
+ export type TmuxServerScopeResolution =
85
+ | { status: "matched"; scope: TmuxServerScope }
86
+ | { status: "not-found" | "offline" | "stale-generation" | "ambiguous" };
87
+
88
+ /**
89
+ * Legacy adaptation considers every registration, including offline ones. Losing
90
+ * one connection must not redirect an old unscoped request to a different server.
91
+ * Explicit callers never fall back when their registration or incarnation is gone.
92
+ */
93
+ export function resolveTmuxServerScope(
94
+ registrations: readonly TmuxServerDescriptor[],
95
+ requested?: TmuxServerScope,
96
+ ): TmuxServerScopeResolution {
97
+ const candidates = requested
98
+ ? registrations.filter((entry) => entry.serverId === requested.serverId)
99
+ : registrations;
100
+ if (candidates.length === 0) return { status: "not-found" };
101
+ if (candidates.length !== 1) return { status: "ambiguous" };
102
+ const candidate = candidates[0]!;
103
+ if (candidate.state === "offline") return { status: "offline" };
104
+ if (requested && requested.generation !== candidate.generation) {
105
+ return { status: "stale-generation" };
106
+ }
107
+ return {
108
+ status: "matched",
109
+ scope: { serverId: candidate.serverId, generation: candidate.generation },
110
+ };
111
+ }
112
+
113
+ /** Namespaces copied stamps/native IDs and retires keys on authority replacement. */
114
+ export function tmuxServerScopedResourceKey(
115
+ scope: TmuxServerScope,
116
+ kind: string,
117
+ resourceId: string,
118
+ ): string {
119
+ return JSON.stringify([scope.serverId, scope.generation, kind, resourceId]);
120
+ }
121
+
122
+ /** Server and generation are part of the upgrade address, never query authority. */
123
+ export function tmuxServerPaneStreamPath(scope: TmuxServerScope): string {
124
+ const parsed = TmuxServerScopeSchemaZ.parse(scope);
125
+ return `/v2/tmux-servers/${parsed.serverId}/${parsed.generation}/pane-streams/redeem`;
126
+ }
127
+
128
+ export function isTmuxServerPaneStreamPath(path: string): boolean {
129
+ const match = /^\/v2\/tmux-servers\/([^/]+)\/([^/]+)\/pane-streams\/redeem$/u.exec(path);
130
+ if (!match) return false;
131
+ return TmuxServerScopeSchemaZ.safeParse({ serverId: match[1], generation: match[2] }).success;
132
+ }
133
+
134
+ export const TmuxServerSessionsResourceSchemaZ = z
135
+ .object({
136
+ version: z.literal(1),
137
+ server: TmuxServerScopeSchemaZ,
138
+ sessions: z.array(
139
+ z
140
+ .object({
141
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
142
+ sessionName: z.string().min(1).max(160),
143
+ workspaceName: z.string().min(1).max(160).nullable(),
144
+ paneCount: z.number().int().nonnegative(),
145
+ })
146
+ .strict(),
147
+ ),
148
+ })
149
+ .strict();
150
+
151
+ /** Selector intent is interpreted only by the daemon on its own host. */
152
+ export const TmuxServerRegistrationRequestSchemaZ = z
153
+ .object({
154
+ label: descriptorShape.label.max(128),
155
+ selector: z.discriminatedUnion("kind", [
156
+ z
157
+ .object({
158
+ kind: z.literal("path"),
159
+ path: z
160
+ .string()
161
+ .min(1)
162
+ .max(4096)
163
+ .refine((path) => path.startsWith("/") && !/[\0\r\n]/u.test(path)),
164
+ })
165
+ .strict(),
166
+ z
167
+ .object({ kind: z.literal("name"), name: z.string().regex(/^[A-Za-z0-9_.-]{1,128}$/u) })
168
+ .strict(),
169
+ ]),
170
+ })
171
+ .strict();
172
+ export type TmuxServerRegistrationRequest = z.infer<typeof TmuxServerRegistrationRequestSchemaZ>;
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ import { TerminalAttachmentSemanticWindowIdSchemaZ } from "./semantic-identity.ts";
3
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "./workspace-catalog-resource.ts";
4
+
5
+ /** Schema bound, not a measured runtime resource budget. */
6
+ export const WINDOW_LINK_MAX_LINKS = 256;
7
+
8
+ /** Random daemon/session-generation-scoped handle; never a native index or ID. */
9
+ export const WindowLinkIdSchemaZ = z.string().regex(/^window-link\.[a-f0-9]{32}$/u);
10
+ export const WindowLinkRevisionSchemaZ = z
11
+ .number()
12
+ .int()
13
+ .nonnegative()
14
+ .max(Number.MAX_SAFE_INTEGER);
15
+
16
+ /** Shape validation only: the authority must validate session, revision and backing. */
17
+ export const WindowLinkTargetSchemaZ = z
18
+ .object({
19
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
20
+ linkId: WindowLinkIdSchemaZ,
21
+ expectedSemanticWindowId: TerminalAttachmentSemanticWindowIdSchemaZ,
22
+ linkRevision: WindowLinkRevisionSchemaZ,
23
+ })
24
+ .strict();
25
+
26
+ export const WindowLinkObservationSchemaZ = z
27
+ .object({
28
+ linkId: WindowLinkIdSchemaZ,
29
+ semanticWindowId: TerminalAttachmentSemanticWindowIdSchemaZ,
30
+ /** Display only. Mutations resolve the opaque handle under live authority. */
31
+ displayIndex: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
32
+ })
33
+ .strict();
34
+
35
+ export const WindowLinkTopologySchemaZ = z
36
+ .object({
37
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
38
+ linkRevision: WindowLinkRevisionSchemaZ,
39
+ activeLinkId: WindowLinkIdSchemaZ,
40
+ links: z.array(WindowLinkObservationSchemaZ).min(1).max(WINDOW_LINK_MAX_LINKS),
41
+ })
42
+ .strict()
43
+ .superRefine((topology, context) => {
44
+ const ids = new Set<string>();
45
+ const indexes = new Set<number>();
46
+ for (const [index, link] of topology.links.entries()) {
47
+ if (ids.has(link.linkId)) {
48
+ context.addIssue({
49
+ code: "custom",
50
+ message: "Window link handles must be unique",
51
+ path: ["links", index, "linkId"],
52
+ });
53
+ }
54
+ if (indexes.has(link.displayIndex)) {
55
+ context.addIssue({
56
+ code: "custom",
57
+ message: "Window link display indexes must be unique",
58
+ path: ["links", index, "displayIndex"],
59
+ });
60
+ }
61
+ ids.add(link.linkId);
62
+ indexes.add(link.displayIndex);
63
+ }
64
+ if (!ids.has(topology.activeLinkId)) {
65
+ context.addIssue({
66
+ code: "custom",
67
+ message: "The active window link must be present",
68
+ path: ["activeLinkId"],
69
+ });
70
+ }
71
+ });
72
+
73
+ export type WindowLinkId = z.infer<typeof WindowLinkIdSchemaZ>;
74
+ export type WindowLinkTarget = z.infer<typeof WindowLinkTargetSchemaZ>;
75
+ export type WindowLinkObservation = z.infer<typeof WindowLinkObservationSchemaZ>;
76
+ export type WindowLinkTopology = z.infer<typeof WindowLinkTopologySchemaZ>;
@@ -27,6 +27,7 @@ import {
27
27
  WorkspacePaneDisplayTitleSchemaZ,
28
28
  } from "./workspace-pane-creation.ts";
29
29
  import { AuthoredInteractionOriginSchemaZ } from "./interaction-receipts.ts";
30
+ import { WindowLinkTargetSchemaZ } from "./window-links.ts";
30
31
 
31
32
  /**
32
33
  * A tmux window named either by its own durable stamp or by a pane inside it.
@@ -130,10 +131,40 @@ export type WorkspacePaneZoomToggleArguments = z.infer<
130
131
  >;
131
132
 
132
133
  export const WorkspacePaneSelectArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
134
+ windowLink: WindowLinkTargetSchemaZ.optional(),
133
135
  semanticPaneId: TerminalAttachmentSemanticPaneIdSchemaZ,
134
136
  }).strict();
135
137
  export type WorkspacePaneSelectArguments = z.infer<typeof WorkspacePaneSelectArgumentsSchemaZ>;
136
138
 
139
+ /**
140
+ * Link-aware arguments use opaque observed authority.
141
+ * Execution requires daemon/session generation fencing and
142
+ * execution-time native link guards. These shapes carry no executable target.
143
+ */
144
+ export const WorkspaceWindowLinkSelectArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
145
+ target: WindowLinkTargetSchemaZ,
146
+ }).strict();
147
+ export type WorkspaceWindowLinkSelectArguments = z.infer<
148
+ typeof WorkspaceWindowLinkSelectArgumentsSchemaZ
149
+ >;
150
+
151
+ /** Ordinary unlink semantics; no forced unlink or implicit backing destruction. */
152
+ export const WorkspaceWindowLinkUnlinkArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
153
+ target: WindowLinkTargetSchemaZ,
154
+ }).strict();
155
+ export type WorkspaceWindowLinkUnlinkArguments = z.infer<
156
+ typeof WorkspaceWindowLinkUnlinkArgumentsSchemaZ
157
+ >;
158
+
159
+ /** Future explicit-link pane selection; runtime must verify pane/backing membership. */
160
+ export const WorkspacePaneSelectWithWindowLinkArgumentsSchemaZ =
161
+ WorkspacePaneSelectArgumentsSchemaZ.extend({
162
+ windowLink: WindowLinkTargetSchemaZ,
163
+ }).strict();
164
+ export type WorkspacePaneSelectWithWindowLinkArguments = z.infer<
165
+ typeof WorkspacePaneSelectWithWindowLinkArgumentsSchemaZ
166
+ >;
167
+
137
168
  /**
138
169
  * Deliver literal terminal input through daemon authority. The text exists only
139
170
  * on the mutation request; neither the result nor the interaction receipt can
@@ -197,6 +228,12 @@ export type WorkspacePaneResizeArguments = z.infer<typeof WorkspacePaneResizeArg
197
228
 
198
229
  /** Every multiplexer intent, discriminated by the route that carries it. */
199
230
  export const WorkspaceMultiplexerIntentSchemaZ = z.discriminatedUnion("verb", [
231
+ WorkspaceWindowLinkSelectArgumentsSchemaZ.extend({
232
+ verb: z.literal("workspace.window.link.select"),
233
+ }).strict(),
234
+ WorkspaceWindowLinkUnlinkArgumentsSchemaZ.extend({
235
+ verb: z.literal("workspace.window.link.unlink"),
236
+ }).strict(),
200
237
  WorkspaceWindowSplitArgumentsSchemaZ.extend({
201
238
  verb: z.literal("workspace.window.split"),
202
239
  }).strict(),
@@ -273,6 +310,21 @@ const MutationEnvelopeSchemaZ = z.object({
273
310
  workspaceName: WorkspacePaneCreationWorkspaceNameSchemaZ,
274
311
  });
275
312
 
313
+ export const WorkspaceWindowLinkSelectResultSchemaZ = MutationEnvelopeSchemaZ.extend({
314
+ verb: z.literal("workspace.window.link.select"),
315
+ target: WindowLinkTargetSchemaZ,
316
+ }).strict();
317
+ export type WorkspaceWindowLinkSelectResult = z.infer<
318
+ typeof WorkspaceWindowLinkSelectResultSchemaZ
319
+ >;
320
+ export const WorkspaceWindowLinkUnlinkResultSchemaZ = MutationEnvelopeSchemaZ.extend({
321
+ verb: z.literal("workspace.window.link.unlink"),
322
+ target: WindowLinkTargetSchemaZ,
323
+ }).strict();
324
+ export type WorkspaceWindowLinkUnlinkResult = z.infer<
325
+ typeof WorkspaceWindowLinkUnlinkResultSchemaZ
326
+ >;
327
+
276
328
  export const WorkspaceWindowSplitResultSchemaZ = MutationEnvelopeSchemaZ.extend({
277
329
  verb: z.literal("workspace.window.split"),
278
330
  direction: WorkspaceSplitDirectionSchemaZ,
@@ -355,6 +407,8 @@ export const WorkspacePaneResizeResultSchemaZ = MutationEnvelopeSchemaZ.extend({
355
407
  export type WorkspacePaneResizeResult = z.infer<typeof WorkspacePaneResizeResultSchemaZ>;
356
408
 
357
409
  export const WorkspaceMultiplexerMutationResultSchemaZ = z.discriminatedUnion("verb", [
410
+ WorkspaceWindowLinkSelectResultSchemaZ,
411
+ WorkspaceWindowLinkUnlinkResultSchemaZ,
358
412
  WorkspaceWindowSplitResultSchemaZ,
359
413
  WorkspaceWindowKillResultSchemaZ,
360
414
  WorkspacePaneKillResultSchemaZ,
@@ -121,6 +121,10 @@ export function interactionSummaryLabel(
121
121
  ): string {
122
122
  const observed = phase === "observed";
123
123
  switch (operationKind) {
124
+ case "workspace.window.link.select":
125
+ return observed ? "window link selected" : "select window link";
126
+ case "workspace.window.link.unlink":
127
+ return observed ? "window unlinked" : "unlink window";
124
128
  case "workspace.window.split":
125
129
  return `split ${summary.operationKind === operationKind ? summary.direction : "window"}`;
126
130
  case "workspace.window.kill":
@@ -34,6 +34,12 @@ const ACTION_COMMAND_METADATA = {
34
34
  "workspace.open.cancel": { label: "Cancel workspace handoff", category: "workspace" },
35
35
  "workspace.promote": { label: "Promote live session to workspace", category: "workspace" },
36
36
  "workspace.app-window.mutate": { label: "Mutate application window", category: "workspace" },
37
+ "workspace.window.link.select": { label: "Focus window link", category: "workspace" },
38
+ "workspace.window.link.unlink": {
39
+ label: "Unlink window",
40
+ category: "workspace",
41
+ dangerous: true,
42
+ },
37
43
  "workspace.window.split": { label: "Split pane", category: "workspace" },
38
44
  "workspace.window.kill": { label: "Close window", category: "workspace", dangerous: true },
39
45
  "workspace.pane.kill": { label: "Close pane", category: "workspace", dangerous: true },
@@ -43,6 +43,8 @@ async function runVerb(verb, input, context, deps) {
43
43
  function verbHandler(verb) {
44
44
  return async (input, context = {}, deps = {}) => (await runVerb(verb, input, context, deps));
45
45
  }
46
+ export const workspaceWindowLinkSelectHandler = verbHandler("workspace.window.link.select");
47
+ export const workspaceWindowLinkUnlinkHandler = verbHandler("workspace.window.link.unlink");
46
48
  export const workspaceWindowSplitHandler = verbHandler("workspace.window.split");
47
49
  export const workspaceWindowKillHandler = verbHandler("workspace.window.kill");
48
50
  export const workspacePaneKillHandler = verbHandler("workspace.pane.kill");
@@ -25,7 +25,7 @@ import { workspacePaneCreateHandler } from "./handlers/workspace-pane-create.js"
25
25
  import { workspaceOpenCancelHandler, workspaceOpenCommitHandler, workspaceOpenHandler, workspaceOpenPrepareHandler, } from "./handlers/workspace-open.js";
26
26
  import { workspacePromoteHandler } from "./handlers/workspace-promote.js";
27
27
  import { fleetAgentMutateHandler, fleetAgentProvisionHandler, workspaceSessionCreateHandler, } from "./handlers/fleet-lifecycle.js";
28
- import { workspacePaneKillHandler, workspacePaneResizeHandler, workspacePaneSelectHandler, workspacePaneSendHandler, workspacePaneSwapHandler, workspacePaneZoomToggleHandler, workspaceRenameHandler, workspaceSessionKillHandler, workspaceWindowKillHandler, workspaceWindowSplitHandler, } from "./handlers/workspace-multiplexer.js";
28
+ import { workspacePaneKillHandler, workspacePaneResizeHandler, workspacePaneSelectHandler, workspacePaneSendHandler, workspacePaneSwapHandler, workspacePaneZoomToggleHandler, workspaceRenameHandler, workspaceSessionKillHandler, workspaceWindowKillHandler, workspaceWindowLinkSelectHandler, workspaceWindowLinkUnlinkHandler, workspaceWindowSplitHandler, } from "./handlers/workspace-multiplexer.js";
29
29
  export const actionRegistry = {
30
30
  "project.openTerminal": {
31
31
  inputSchema: ActionContractsZ["project.openTerminal"].input,
@@ -203,6 +203,18 @@ export const actionRegistry = {
203
203
  handler: (input) => workspacePaneZoomToggleHandler(input),
204
204
  handlerWithContext: workspacePaneZoomToggleHandler,
205
205
  },
206
+ "workspace.window.link.select": {
207
+ inputSchema: ActionContractsZ["workspace.window.link.select"].input,
208
+ resultSchema: ActionContractsZ["workspace.window.link.select"].result,
209
+ handler: (input) => workspaceWindowLinkSelectHandler(input),
210
+ handlerWithContext: workspaceWindowLinkSelectHandler,
211
+ },
212
+ "workspace.window.link.unlink": {
213
+ inputSchema: ActionContractsZ["workspace.window.link.unlink"].input,
214
+ resultSchema: ActionContractsZ["workspace.window.link.unlink"].result,
215
+ handler: (input) => workspaceWindowLinkUnlinkHandler(input),
216
+ handlerWithContext: workspaceWindowLinkUnlinkHandler,
217
+ },
206
218
  "workspace.pane.select": {
207
219
  inputSchema: ActionContractsZ["workspace.pane.select"].input,
208
220
  resultSchema: ActionContractsZ["workspace.pane.select"].result,
@@ -1,6 +1,6 @@
1
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
1
2
  import { runtimeTmuxArgs } from "../lib/runtime-namespace.js";
2
3
  import { execFileSync } from "node:child_process";
3
- import { createHash } from "node:crypto";
4
4
  import { listSessionPanes } from "../widgets/lib/pane-comms.js";
5
5
  import { getDefaultWorkspaceRegistry } from "../lib/workspace-registry.js";
6
6
  import { ADOPTED_OPTION } from "../lib/chrome-front-door.js";
@@ -47,13 +47,6 @@ export function listTmuxSessions() {
47
47
  return [];
48
48
  return raw.split("\n").filter(Boolean);
49
49
  }
50
- function liveSessionId(serverPid, sessionId, sessionCreated) {
51
- const digest = createHash("sha256")
52
- .update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
53
- .digest("hex")
54
- .slice(0, 20);
55
- return `live-session.${digest}`;
56
- }
57
50
  /**
58
51
  * Enumerate observed tmux truth without consulting the workspace registry.
59
52
  *
@@ -86,7 +79,7 @@ export function discoverLiveSessionSummaries(runTmux = _tmuxRunner) {
86
79
  continue;
87
80
  if (!sessionName || !isVisibleFleetSession(sessionName))
88
81
  continue;
89
- const identity = liveSessionId(serverPid, sessionId, sessionCreated);
82
+ const identity = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
90
83
  const previous = sessions.get(identity);
91
84
  sessions.set(identity, {
92
85
  liveSessionId: identity,