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,6 @@
1
+ import { Menu } from "../ui/index.ts";
2
+ import type { WindowLinkTarget } from "@tmux-ide/contracts";
3
+ import { windowLinkTarget } from "./application-terminal-workspace-policy.ts";
1
4
  import type { PaneInteractionProjection } from "@tmux-ide/core";
2
5
  /* @jsxImportSource @opentui/solid */
3
6
  import {
@@ -61,7 +64,6 @@ import {
61
64
  type TerminalSelectionRange,
62
65
  } from "./terminal-selection.ts";
63
66
  import {
64
- retainedTerminalWindowKey,
65
67
  terminalAgentStatusLabel,
66
68
  terminalPaneDisplayTitle,
67
69
  terminalPaneResizePreview,
@@ -168,6 +170,8 @@ export interface ApplicationTerminalWorkspaceProps {
168
170
  /** Daemon-authored semantic agent state, keyed by durable pane identity. */
169
171
  readonly agentIndicators?: Accessor<ReadonlyMap<string, ApplicationTerminalAgentIndicator>>;
170
172
  readonly onSelectPane: (paneId: string) => void;
173
+ readonly onSelectWindowLink?: (target: WindowLinkTarget) => void;
174
+ readonly onUnlinkWindowLink?: (target: WindowLinkTarget) => void;
171
175
  readonly onCreateWindow?: () => void;
172
176
  readonly onPaneContextAction?: (
173
177
  paneId: string,
@@ -332,37 +336,56 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
332
336
  ),
333
337
  );
334
338
  const visibleFrames = createMemo(() => projectedFrames().filter((frame) => frame.visible));
335
- const retainedWindowIds = createMemo(
336
- () =>
337
- Object.freeze(
338
- layout()
339
- .windows.map(retainedTerminalWindowKey)
340
- .filter((id): id is string => id !== null),
341
- ),
342
- undefined,
343
- {
344
- equals: (previous, next) =>
345
- previous.length === next.length && previous.every((id, index) => id === next[index]),
346
- },
347
- );
348
- const terminalWindowTabs = createMemo<readonly TerminalWindowTab[]>(() =>
349
- retainedWindowIds().map((windowId, index) => {
350
- const window = layout().windows.find(
351
- (candidate) => retainedTerminalWindowKey(candidate) === windowId,
352
- )!;
353
- const indicator = terminalWindowAgentIndicator(window, agentIndicators());
354
- return {
339
+ const [windowLinkMenu, setWindowLinkMenu] = createSignal<WindowLinkTarget | null>(null);
340
+ const unlinkWindowTab = () => {
341
+ const target = windowLinkMenu();
342
+ setWindowLinkMenu(null);
343
+ if (target) props.onUnlinkWindowLink?.(target);
344
+ };
345
+ createEffect(() => {
346
+ const target = windowLinkMenu();
347
+ const observed = target && windowLinkTarget(layout(), target.linkId);
348
+ if (
349
+ target &&
350
+ (!observed ||
351
+ observed.liveSessionId !== target.liveSessionId ||
352
+ observed.linkRevision !== target.linkRevision)
353
+ )
354
+ setWindowLinkMenu(null);
355
+ });
356
+ const terminalWindowTabs = createMemo<readonly TerminalWindowTab[]>(() => {
357
+ const snapshot = layout();
358
+ if (!snapshot.windowLinks)
359
+ return snapshot.windows.map((window, index) => ({
355
360
  index,
356
361
  name: terminalWindowTitle(window),
357
362
  active: window.currentWindow,
358
363
  sync: false,
359
364
  semanticWindowId: window.semanticWindowId,
360
365
  activePaneId: terminalWindowPane(window),
361
- status: indicator ? terminalAgentStatusLabel(indicator.activity) : undefined,
362
- attention: indicator?.attention,
363
- };
364
- }),
365
- );
366
+ disabled: true,
367
+ }));
368
+ return snapshot.windowLinks.links.flatMap((link) => {
369
+ const window = snapshot.windows.find(
370
+ (candidate) => candidate.semanticWindowId === link.semanticWindowId,
371
+ );
372
+ if (!window) return [];
373
+ const indicator = terminalWindowAgentIndicator(window, agentIndicators());
374
+ return [
375
+ {
376
+ linkId: link.linkId,
377
+ index: link.displayIndex,
378
+ name: terminalWindowTitle(window),
379
+ active: snapshot.windowLinks?.activeLinkId === link.linkId,
380
+ sync: false,
381
+ semanticWindowId: window.semanticWindowId,
382
+ activePaneId: terminalWindowPane(window),
383
+ status: indicator ? terminalAgentStatusLabel(indicator.activity) : undefined,
384
+ attention: indicator?.attention,
385
+ },
386
+ ];
387
+ });
388
+ });
366
389
  if (props.onWindowPresented)
367
390
  createRenderEffect(() => {
368
391
  const current = layout().current;
@@ -911,6 +934,12 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
911
934
  props.onSelectionCopyOwner?.(copySelection);
912
935
  const handlePaneMenuKey: PaneMenuKeyHandler = (name, event) => {
913
936
  if (props.interactive === false) return false;
937
+ if (windowLinkMenu()) {
938
+ if (event?.eventType === "release") return true;
939
+ if (name === "escape") setWindowLinkMenu(null);
940
+ else if (name === "return" || name === "enter" || name === "u") unlinkWindowTab();
941
+ return true;
942
+ }
914
943
  if (paneMenu.handleKey(name, event)) return true;
915
944
  const focused = props.focusedPane;
916
945
  const keyboard = keyboardCopy();
@@ -1097,7 +1126,10 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1097
1126
  },
1098
1127
  () =>
1099
1128
  props.interactive !== false &&
1100
- (drag !== null || paneMenu.ownsInput() || keyboardCopy() !== null),
1129
+ (drag !== null ||
1130
+ windowLinkMenu() !== null ||
1131
+ paneMenu.ownsInput() ||
1132
+ keyboardCopy() !== null),
1101
1133
  () => {
1102
1134
  // Called only after global shortcuts, copy, and local navigation decline
1103
1135
  // the event, immediately before terminal key or paste delivery.
@@ -1116,7 +1148,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1116
1148
  // listener and the typed listener on a target, even after stopPropagation.
1117
1149
  const routePointer = (event: WorkspaceMouseEvent): void => {
1118
1150
  if (event.type === "down") wheelGesture.reset();
1119
- if (paneMenu.ownsInput()) {
1151
+ if (windowLinkMenu() || paneMenu.ownsInput()) {
1120
1152
  event.stopPropagation?.();
1121
1153
  return;
1122
1154
  }
@@ -1649,9 +1681,18 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1649
1681
  tabs={terminalWindowTabs}
1650
1682
  hoveredIndex={null}
1651
1683
  onActivate={(index) => {
1652
- const pane = terminalWindowTabs()[index]?.activePaneId;
1653
- if (pane) props.onSelectPane(pane);
1684
+ const tab = terminalWindowTabs().find((candidate) => candidate.index === index);
1685
+ const target = tab?.linkId ? windowLinkTarget(layout(), tab.linkId) : null;
1686
+ if (target) props.onSelectWindowLink?.(target);
1654
1687
  }}
1688
+ onWindowActions={
1689
+ props.onUnlinkWindowLink
1690
+ ? (index) => {
1691
+ const tab = terminalWindowTabs().find((candidate) => candidate.index === index);
1692
+ if (tab?.linkId) setWindowLinkMenu(windowLinkTarget(layout(), tab.linkId));
1693
+ }
1694
+ : undefined
1695
+ }
1655
1696
  onNewWindow={() => props.onCreateWindow?.()}
1656
1697
  />
1657
1698
  </Show>
@@ -1805,6 +1846,22 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1805
1846
  </box>
1806
1847
  )}
1807
1848
  </For>
1849
+ <Show when={windowLinkMenu()}>
1850
+ <Menu
1851
+ theme={props.theme}
1852
+ title="Window link"
1853
+ left={0}
1854
+ top={1}
1855
+ width={Math.min(36, props.width)}
1856
+ viewportWidth={props.width}
1857
+ viewportHeight={props.height}
1858
+ selectedId="unlink"
1859
+ items={[{ id: "unlink", label: "Unlink this tab", shortcut: "U" }]}
1860
+ footer="Other links keep their panes"
1861
+ onDismiss={() => setWindowLinkMenu(null)}
1862
+ onSelect={unlinkWindowTab}
1863
+ />
1864
+ </Show>
1808
1865
  <Show when={paneContextMenu()}>
1809
1866
  {(menu) => (
1810
1867
  <PaneActionMenu
@@ -1,3 +1,5 @@
1
+ import { createEffect } from "solid-js";
2
+ import type { ApplicationAppearanceOwner } from "./application-appearance-owner.ts";
1
3
  import type { createCliRenderer } from "@opentui/core";
2
4
  import type { SemanticThemeSnapshot } from "../theme.ts";
3
5
 
@@ -43,3 +45,20 @@ export function applyApplicationAppearanceToRenderer(
43
45
  requestApplicationThemeRepaint(renderer);
44
46
  return generation;
45
47
  }
48
+
49
+ /** Bind the complete appearance publication to one renderer transaction. */
50
+ export function createApplicationAppearanceRendererBinding(
51
+ renderer: Renderer,
52
+ appearance: Pick<ApplicationAppearanceOwner, "appearance">,
53
+ ): void {
54
+ let paintedGeneration: number | null = null;
55
+ createEffect(() => {
56
+ const next = appearance.appearance();
57
+ paintedGeneration = applyApplicationAppearanceToRenderer(
58
+ renderer,
59
+ next.theme,
60
+ next.generation,
61
+ paintedGeneration,
62
+ );
63
+ });
64
+ }
@@ -1,3 +1,5 @@
1
+ import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
2
+ import type { TmuxServerScope } from "@tmux-ide/contracts";
1
3
  import type { ApplicationMachineAuthorityHandle } from "./application-machine-authority.ts";
2
4
  import type { ActionResult, WorkspaceSessionKillArguments } from "@tmux-ide/contracts";
3
5
  import {
@@ -52,6 +54,7 @@ async function dispatch<
52
54
 
53
55
  type FleetHandle = Pick<ApplicationMachineAuthorityHandle, "read" | "endpoint">;
54
56
  export interface FleetSessionCloseTarget {
57
+ readonly server?: TmuxServerScope;
55
58
  readonly daemonInstanceId: string;
56
59
  readonly liveSessionId: string;
57
60
  readonly sessionName: string;
@@ -91,12 +94,36 @@ async function dispatchOnHandle<Name extends "workspace.session.create" | "works
91
94
  export function createFleetSession(
92
95
  input: WorkspaceSessionCreateArguments | FleetHandle,
93
96
  name?: string,
97
+ server?: TmuxServerScope,
98
+ options?: Pick<WorkspaceSessionCreateArguments, "includeLiveSessionId">,
94
99
  ): Promise<WorkspaceSessionCreateResult | null> {
95
100
  if ("read" in input) {
96
101
  const daemon = input.read();
97
102
  if (!daemon || !name) return Promise.resolve(null);
103
+ if (server) {
104
+ if (!daemon.authToken || input.endpoint().state !== "ready") return Promise.resolve(null);
105
+ const epoch = input.endpoint().epoch;
106
+ const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
107
+ const client = createTmuxServerClient(
108
+ { baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl },
109
+ server,
110
+ );
111
+ return client
112
+ .createSession(randomUUID(), {
113
+ displayName: name,
114
+ ...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
115
+ expectedDaemonInstanceId: server.generation,
116
+ })
117
+ .then((result) =>
118
+ input.endpoint().epoch === epoch && input.read()?.instanceId === daemon.instanceId
119
+ ? result
120
+ : null,
121
+ )
122
+ .finally(() => client.dispose());
123
+ }
98
124
  return dispatchOnHandle(input, "workspace.session.create", {
99
125
  displayName: name,
126
+ ...(options?.includeLiveSessionId ? { includeLiveSessionId: true } : {}),
100
127
  expectedDaemonInstanceId: daemon.instanceId,
101
128
  });
102
129
  }
@@ -109,6 +136,34 @@ export function closeFleetSession(
109
136
  target: FleetSessionCloseTarget,
110
137
  ): Promise<ActionResult<"workspace.session.kill"> | null> {
111
138
  if (handle.read()?.instanceId !== target.daemonInstanceId) return Promise.resolve(null);
139
+ if (target.server) {
140
+ const daemon = handle.read();
141
+ if (!daemon?.authToken || handle.endpoint().state !== "ready") return Promise.resolve(null);
142
+ const epoch = handle.endpoint().epoch;
143
+ const baseUrl = canonicalDaemonUrl("http", daemon.bindHostname, daemon.port);
144
+ const client = createTmuxServerClient(
145
+ { baseUrl, ownerToken: daemon.authToken, hostClientId: HOST_CLIENT_ID, origin: baseUrl },
146
+ target.server,
147
+ );
148
+ return client
149
+ .mutate(randomUUID(), {
150
+ verb: "workspace.session.kill",
151
+ workspaceName: target.workspaceName ?? target.sessionName,
152
+ fleetTarget: {
153
+ daemonInstanceId: target.server.generation,
154
+ liveSessionId: target.liveSessionId,
155
+ sessionName: target.sessionName,
156
+ },
157
+ })
158
+ .then((result) =>
159
+ handle.endpoint().epoch === epoch &&
160
+ handle.read()?.instanceId === target.daemonInstanceId &&
161
+ result.verb === "workspace.session.kill"
162
+ ? result
163
+ : null,
164
+ )
165
+ .finally(() => client.dispose());
166
+ }
112
167
  return dispatchOnHandle(handle, "workspace.session.kill", {
113
168
  workspaceName: target.workspaceName ?? target.sessionName,
114
169
  fleetTarget: {
@@ -0,0 +1,118 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { Show } from "solid-js";
3
+ import type { SemanticThemeSnapshot } from "../theme.ts";
4
+ import { TuiButton } from "../ui/button.tsx";
5
+ import { clipTerminal } from "../terminal-text.ts";
6
+ import {
7
+ GUIDED_TOUR_STEPS,
8
+ guidedTourCopy,
9
+ type GuidedTourState,
10
+ type GuidedTourShortcuts,
11
+ } from "./guided-tour.ts";
12
+
13
+ export interface GuidedTourCoachProps {
14
+ readonly state: GuidedTourState;
15
+ readonly theme: SemanticThemeSnapshot;
16
+ readonly width: number;
17
+ readonly height?: number;
18
+ readonly shortcuts?: GuidedTourShortcuts;
19
+ readonly error?: string | null;
20
+ readonly busy?: boolean;
21
+ readonly onReplay?: () => void;
22
+ readonly onPause: () => void;
23
+ readonly onWelcomeRead: () => void;
24
+ readonly onCreatePractice: () => void;
25
+ readonly onOpenCommands: () => void;
26
+ readonly onOpenAppearance: () => void;
27
+ readonly onOpenHome: () => void;
28
+ readonly onOpenPractice: () => void;
29
+ }
30
+ /** Non-modal coach: terminal interaction stays available throughout the tour. */
31
+ export function GuidedTourCoach(props: GuidedTourCoachProps) {
32
+ const copy = () => guidedTourCopy(props.state.step, props.shortcuts);
33
+ const width = () => Math.max(1, props.width);
34
+ const primary = () => {
35
+ switch (props.state.step) {
36
+ case "welcome":
37
+ return { label: "Let's try it", run: props.onWelcomeRead };
38
+ case "practice":
39
+ return { label: "Create practice session", run: props.onCreatePractice };
40
+ case "split":
41
+ case "commands":
42
+ return { label: "Open commands", run: props.onOpenCommands };
43
+ case "theme":
44
+ return { label: "Choose theme", run: props.onOpenAppearance };
45
+ case "leave":
46
+ return { label: "Open Home", run: props.onOpenHome };
47
+ case "reopen":
48
+ return { label: "Reopen practice", run: props.onOpenPractice };
49
+ default:
50
+ return null;
51
+ }
52
+ };
53
+ return (
54
+ <box
55
+ id="guided-tour-coach"
56
+ width={width()}
57
+ height={props.height ?? 10}
58
+ overflow="hidden"
59
+ flexShrink={0}
60
+ flexDirection="column"
61
+ paddingX={1}
62
+ backgroundColor={props.theme.roles.surfaces.panel}
63
+ >
64
+ <text height={1} fg={props.theme.roles.text.primary}>
65
+ <strong>
66
+ {clipTerminal(
67
+ `Learn tmux-ide · ${GUIDED_TOUR_STEPS.indexOf(props.state.step) + 1}/${GUIDED_TOUR_STEPS.length} · ${copy().title}`,
68
+ Math.max(0, width() - 2),
69
+ )}
70
+ </strong>
71
+ </text>
72
+ <scrollbox flexGrow={1} width="100%">
73
+ <text fg={props.theme.roles.text.secondary}>{copy().body}</text>
74
+ </scrollbox>
75
+ <Show when={props.error}>
76
+ <text fg={props.theme.roles.text.primary}>{props.error}</text>
77
+ </Show>
78
+ <box flexDirection="row" flexWrap="wrap" flexShrink={0} gap={1}>
79
+ <Show
80
+ when={props.state.practice && ["split", "focus", "resize"].includes(props.state.step)}
81
+ >
82
+ <TuiButton
83
+ theme={props.theme}
84
+ label="Open practice"
85
+ onPress={props.onOpenPractice}
86
+ width={Math.min(Math.max(1, width() - 2), 17)}
87
+ />
88
+ </Show>
89
+ <Show when={props.onReplay && props.error}>
90
+ <TuiButton
91
+ theme={props.theme}
92
+ label="Restart tour"
93
+ onPress={props.onReplay}
94
+ width={Math.min(Math.max(1, width() - 2), 16)}
95
+ />
96
+ </Show>
97
+ <Show when={primary()}>
98
+ {(action) => (
99
+ <TuiButton
100
+ theme={props.theme}
101
+ label={action().label}
102
+ width={Math.min(Math.max(1, width() - 2), action().label.length + 4)}
103
+ loading={props.busy}
104
+ onPress={action().run}
105
+ />
106
+ )}
107
+ </Show>
108
+ <TuiButton
109
+ theme={props.theme}
110
+ label={props.state.step === "complete" ? "Done" : "Pause tour"}
111
+ width={Math.min(Math.max(1, width() - 2), 14)}
112
+ variant="ghost"
113
+ onPress={props.onPause}
114
+ />
115
+ </box>
116
+ </box>
117
+ );
118
+ }
@@ -0,0 +1,72 @@
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.ts";
5
+ import { stateHome } from "../../../lib/state-home.ts";
6
+ import { GUIDED_TOUR_STEPS, initialGuidedTourState, type GuidedTourState } from "./guided-tour.ts";
7
+
8
+ export function guidedTourStatePath(): string {
9
+ return runtimeOwnedPath(join(stateHome(), "guided-tour.json"));
10
+ }
11
+ export function parseGuidedTourState(value: unknown): GuidedTourState {
12
+ if (!value || typeof value !== "object") return initialGuidedTourState();
13
+ const raw = value as Record<string, unknown>;
14
+ if (
15
+ raw.version !== 1 ||
16
+ !GUIDED_TOUR_STEPS.includes(raw.step as GuidedTourState["step"]) ||
17
+ typeof raw.active !== "boolean"
18
+ )
19
+ return initialGuidedTourState();
20
+ let practice: GuidedTourState["practice"] = null;
21
+ if (raw.practice && typeof raw.practice === "object") {
22
+ const p = raw.practice as Record<string, unknown>;
23
+ if (
24
+ [p.machineId, p.serverId, p.generation, p.sessionId, p.sessionName].every(
25
+ (v) => typeof v === "string" && v.length > 0,
26
+ )
27
+ ) {
28
+ practice = {
29
+ machineId: p.machineId as string,
30
+ serverId: p.serverId as string,
31
+ generation: p.generation as string,
32
+ sessionId: p.sessionId as string,
33
+ sessionName: p.sessionName as string,
34
+ };
35
+ }
36
+ }
37
+ const step = raw.step as GuidedTourState["step"];
38
+ return {
39
+ version: 1,
40
+ step: !practice && !["welcome", "practice", "complete"].includes(step) ? "practice" : step,
41
+ active: raw.active,
42
+ practice,
43
+ };
44
+ }
45
+ export function readGuidedTourState(path = guidedTourStatePath()): GuidedTourState {
46
+ try {
47
+ return parseGuidedTourState(JSON.parse(readFileSync(path, "utf8")));
48
+ } catch {
49
+ return initialGuidedTourState();
50
+ }
51
+ }
52
+ /** Atomic, best-effort persistence; failure must not prevent using terminals. */
53
+ export function writeGuidedTourState(
54
+ state: GuidedTourState,
55
+ path = guidedTourStatePath(),
56
+ ): boolean {
57
+ const temporary = `${path}.${randomUUID()}.tmp`;
58
+ try {
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ writeFileSync(temporary, JSON.stringify(state), { mode: 0o600 });
61
+ renameSync(temporary, path);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ } finally {
66
+ try {
67
+ rmSync(temporary, { force: true });
68
+ } catch {
69
+ /* Best effort. */
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,147 @@
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
+ ] as const;
14
+ export type GuidedTourStep = (typeof GUIDED_TOUR_STEPS)[number];
15
+ export interface GuidedTourPractice {
16
+ readonly machineId: string;
17
+ readonly serverId: string;
18
+ readonly generation: string;
19
+ readonly sessionId: string;
20
+ readonly sessionName: string;
21
+ }
22
+ export interface GuidedTourState {
23
+ readonly version: 1;
24
+ readonly step: GuidedTourStep;
25
+ readonly active: boolean;
26
+ readonly practice: GuidedTourPractice | null;
27
+ }
28
+ export type GuidedTourEvent =
29
+ | { readonly type: "start" | "pause" | "resume" | "replay" | "welcome-read" }
30
+ | { readonly type: "practice-created"; readonly practice: GuidedTourPractice }
31
+ | { readonly type: "practice-missing" }
32
+ | {
33
+ readonly type: "split-confirmed" | "focus-confirmed" | "resize-confirmed" | "reopened";
34
+ readonly practice: GuidedTourPractice;
35
+ }
36
+ | { readonly type: "commands-opened" | "theme-changed" | "home-opened" };
37
+
38
+ export function initialGuidedTourState(): GuidedTourState {
39
+ return { version: 1, step: "welcome", active: false, practice: null };
40
+ }
41
+ export function sameGuidedTourPractice(
42
+ a: GuidedTourPractice | null,
43
+ b: GuidedTourPractice,
44
+ ): boolean {
45
+ return (
46
+ a !== null &&
47
+ a.machineId === b.machineId &&
48
+ a.serverId === b.serverId &&
49
+ a.generation === b.generation &&
50
+ a.sessionId === b.sessionId
51
+ );
52
+ }
53
+ /** Call confirmation events only after an observed change, never on request dispatch. */
54
+ export function reduceGuidedTour(state: GuidedTourState, event: GuidedTourEvent): GuidedTourState {
55
+ if (event.type === "replay") return { ...initialGuidedTourState(), active: true };
56
+ if (event.type === "pause") return state.active ? { ...state, active: false } : state;
57
+ if (event.type === "start" || event.type === "resume") {
58
+ return state.step === "complete" ? state : { ...state, active: true };
59
+ }
60
+ if (event.type === "practice-created" && state.step === "practice") {
61
+ return { ...state, step: "split", practice: event.practice };
62
+ }
63
+ if (!state.active) return state;
64
+ if (event.type === "practice-missing" && state.practice && state.step !== "complete") {
65
+ return { ...state, step: "practice", practice: null };
66
+ }
67
+ if (event.type === "welcome-read" && state.step === "welcome")
68
+ return { ...state, step: "practice" };
69
+ if ("practice" in event && !sameGuidedTourPractice(state.practice, event.practice)) return state;
70
+ const expected: Partial<Record<GuidedTourStep, GuidedTourEvent["type"]>> = {
71
+ split: "split-confirmed",
72
+ focus: "focus-confirmed",
73
+ resize: "resize-confirmed",
74
+ commands: "commands-opened",
75
+ theme: "theme-changed",
76
+ leave: "home-opened",
77
+ reopen: "reopened",
78
+ };
79
+ if (expected[state.step] !== event.type) return state;
80
+ const step = GUIDED_TOUR_STEPS[GUIDED_TOUR_STEPS.indexOf(state.step) + 1]!;
81
+ return { ...state, step };
82
+ }
83
+
84
+ export interface GuidedTourShortcuts {
85
+ readonly commands?: string;
86
+ readonly home?: string;
87
+ readonly split?: string;
88
+ readonly focus?: string;
89
+ }
90
+ export function guidedTourCopy(
91
+ step: GuidedTourStep,
92
+ keys: GuidedTourShortcuts = {},
93
+ ): { title: string; body: string } {
94
+ const shortcut = (key: string | undefined) => (key ? ` (${key})` : "");
95
+ switch (step) {
96
+ case "welcome":
97
+ return {
98
+ title: "Meet tmux-ide",
99
+ 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.",
100
+ };
101
+ case "practice":
102
+ return {
103
+ title: "Your practice space",
104
+ body: "Create a separate local practice session. The tour leaves your existing sessions alone. You can pause now and resume from Home.",
105
+ };
106
+ case "split":
107
+ return {
108
+ title: "Make room",
109
+ 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.`,
110
+ };
111
+ case "focus":
112
+ return {
113
+ title: "Move between panes",
114
+ body: `Select the other practice pane by clicking it or using your pane navigation shortcut${shortcut(keys.focus)}. Each pane is an independent terminal.`,
115
+ };
116
+ case "resize":
117
+ return {
118
+ title: "Find a comfortable layout",
119
+ body: "Drag the divider between the practice panes. Continue working while tmux updates the layout; the tour advances after the changed dimensions arrive.",
120
+ };
121
+ case "commands":
122
+ return {
123
+ title: "Find an action",
124
+ body: `Open Commands${shortcut(keys.commands)} to discover actions and their current shortcuts. Close the palette when you are ready to continue.`,
125
+ };
126
+ case "theme":
127
+ return {
128
+ title: "Make it yours",
129
+ body: "Choose a different theme from Home. Automatic contrast correction improves hard-to-read terminal colors; you can turn it off in settings.",
130
+ };
131
+ case "leave":
132
+ return {
133
+ title: "Leave without losing work",
134
+ body: `Open Home${shortcut(keys.home)}. Your practice terminals keep running in tmux while you look at agents or other sessions.`,
135
+ };
136
+ case "reopen":
137
+ return {
138
+ title: "Pick up where you left off",
139
+ body: "Reopen your practice session. tmux keeps your terminals alive independently of which page or session you are viewing.",
140
+ };
141
+ case "complete":
142
+ return {
143
+ title: "Ready to explore",
144
+ 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.",
145
+ };
146
+ }
147
+ }
@@ -316,7 +316,7 @@ function buildProductionBundle(
316
316
  requestTerminalRuntimeInventoryRefresh: () => {
317
317
  connection.transport.refreshTerminalRuntimeInventory();
318
318
  },
319
- actions: productionOwnerActions(readDaemon),
319
+ actions: connection.ownerActions ?? productionOwnerActions(readDaemon),
320
320
  },
321
321
  });
322
322
  fastLane = createOpenTuiWorkspaceTerminalFastLane(
@@ -75,6 +75,9 @@ export async function resizeTerminalPane(
75
75
  });
76
76
  };
77
77
  try {
78
+ // Geometry acquisition is itself a mutation of the ownership lease. A stale
79
+ // gesture must not take it from another client even when resize is refused.
80
+ if (!isCurrent()) return fail({ stage: "pre-dispatch", reason: "generation-replaced" });
78
81
  if (expected.client.ownsRuntimeAuthority?.("geometry") !== true) {
79
82
  let lease;
80
83
  try {
@@ -1,3 +1,4 @@
1
+ import type { WindowLinkTarget } from "@tmux-ide/contracts";
1
2
  import type { WorkspaceClientDispatch } from "@tmux-ide/daemon-client/workspace-client-types";
2
3
 
3
4
  interface PaneSelectionClient {
@@ -40,6 +41,7 @@ export async function selectTerminalPane(
40
41
  semanticPaneId: string,
41
42
  operationId?: string,
42
43
  onFailure?: (failure: PaneSelectionFailure) => void,
44
+ windowLink?: WindowLinkTarget,
43
45
  ): Promise<PaneSelectionReceipt | null> {
44
46
  const fail = (failure: PaneSelectionFailure): null => {
45
47
  try {
@@ -95,6 +97,7 @@ export async function selectTerminalPane(
95
97
  verb: "workspace.pane.select",
96
98
  workspaceName: expected.workspaceName,
97
99
  semanticPaneId,
100
+ ...(windowLink ? { windowLink } : {}),
98
101
  },
99
102
  });
100
103
  } catch (error) {