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,162 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import {
3
+ initialGuidedTourState,
4
+ reduceGuidedTour,
5
+ sameGuidedTourPractice,
6
+ type GuidedTourEvent,
7
+ type GuidedTourPractice,
8
+ type GuidedTourState,
9
+ } from "./guided-tour.ts";
10
+ import { readGuidedTourState, writeGuidedTourState } from "./guided-tour-storage.ts";
11
+
12
+ export interface GuidedTourObservation {
13
+ readonly practice: GuidedTourPractice | null;
14
+ readonly panes: readonly {
15
+ readonly id: string;
16
+ readonly width: number;
17
+ readonly height: number;
18
+ }[];
19
+ readonly focusedPane: string | null;
20
+ readonly surface: "home" | "terminals";
21
+ readonly paletteOpen: boolean;
22
+ readonly theme: string;
23
+ }
24
+ /** Compare confirmed snapshots; unrelated sessions and request attempts never count. */
25
+ export function observedGuidedTourEvent(
26
+ state: GuidedTourState,
27
+ previous: GuidedTourObservation | null,
28
+ current: GuidedTourObservation,
29
+ ): GuidedTourEvent | null {
30
+ if (!state.active || !previous) return null;
31
+ if (state.step === "commands" && current.paletteOpen && !previous.paletteOpen)
32
+ return { type: "commands-opened" };
33
+ if (state.step === "theme" && current.theme !== previous.theme) return { type: "theme-changed" };
34
+ if (state.step === "leave" && current.surface === "home") return { type: "home-opened" };
35
+ if (
36
+ !current.practice ||
37
+ !sameGuidedTourPractice(state.practice, current.practice) ||
38
+ current.surface !== "terminals"
39
+ )
40
+ return null;
41
+ if (
42
+ state.step === "reopen" &&
43
+ (previous.surface !== "terminals" ||
44
+ !sameGuidedTourPractice(previous.practice, current.practice))
45
+ )
46
+ return { type: "reopened", practice: current.practice };
47
+ if (!previous.practice || !sameGuidedTourPractice(previous.practice, current.practice))
48
+ return null;
49
+ if (
50
+ state.step === "split" &&
51
+ previous.panes.length > 0 &&
52
+ current.panes.length > previous.panes.length &&
53
+ previous.panes.every((old) => current.panes.some((p) => p.id === old.id)) &&
54
+ current.panes.some((p) => !previous.panes.some((old) => old.id === p.id))
55
+ )
56
+ return { type: "split-confirmed", practice: current.practice };
57
+ if (
58
+ state.step === "focus" &&
59
+ previous.focusedPane &&
60
+ current.focusedPane &&
61
+ previous.focusedPane !== current.focusedPane &&
62
+ current.panes.some((p) => p.id === current.focusedPane) &&
63
+ current.panes.some((p) => p.id === previous.focusedPane)
64
+ )
65
+ return { type: "focus-confirmed", practice: current.practice };
66
+ // A divider reallocates space between the same panes: one grows while
67
+ // another shrinks on the same axis. Whole-app growth/shrink is not practice.
68
+ const samePanes =
69
+ current.panes.length >= 2 &&
70
+ current.panes.length === previous.panes.length &&
71
+ previous.panes.every((old) => current.panes.some((pane) => pane.id === old.id));
72
+ const reallocated = (["width", "height"] as const).some((axis) => {
73
+ const changes = current.panes.map(
74
+ (pane) =>
75
+ pane[axis] - (previous.panes.find((old) => old.id === pane.id)?.[axis] ?? pane[axis]),
76
+ );
77
+ return changes.some((change) => change > 0) && changes.some((change) => change < 0);
78
+ });
79
+ if (state.step === "resize" && samePanes && reallocated)
80
+ return { type: "resize-confirmed", practice: current.practice };
81
+ return null;
82
+ }
83
+ export function createApplicationGuidedTourOwner(options: {
84
+ createPractice(): Promise<GuidedTourPractice>;
85
+ openPractice(practice: GuidedTourPractice): Promise<void> | void;
86
+ read?: () => GuidedTourState;
87
+ write?: (state: GuidedTourState) => boolean;
88
+ }) {
89
+ const [state, setState] = createSignal((options.read ?? readGuidedTourState)());
90
+ const [busy, setBusy] = createSignal(false);
91
+ const [error, setError] = createSignal<string | null>(null);
92
+ let previous: GuidedTourObservation | null = null;
93
+ let disposed = false;
94
+ onCleanup(() => {
95
+ disposed = true;
96
+ });
97
+ const send = (event: GuidedTourEvent) => {
98
+ const next = reduceGuidedTour(state(), event);
99
+ if (next === state()) return;
100
+ setState(next);
101
+ if (!(options.write ?? writeGuidedTourState)(next))
102
+ setError("Progress could not be saved. You can still continue this tour.");
103
+ };
104
+ return {
105
+ state,
106
+ busy,
107
+ error,
108
+ label: () =>
109
+ state().step === "complete"
110
+ ? "Replay walkthrough"
111
+ : state().step === initialGuidedTourState().step
112
+ ? "Learn tmux-ide"
113
+ : "Resume walkthrough",
114
+ open() {
115
+ previous = null;
116
+ send({ type: state().step === "complete" ? "replay" : "resume" });
117
+ },
118
+ replay() {
119
+ previous = null;
120
+ send({ type: "replay" });
121
+ },
122
+ pause() {
123
+ send({ type: "pause" });
124
+ },
125
+ welcomeRead() {
126
+ send({ type: "welcome-read" });
127
+ },
128
+ observe(current: GuidedTourObservation) {
129
+ const event = observedGuidedTourEvent(state(), previous, current);
130
+ previous = current;
131
+ if (event) send(event);
132
+ },
133
+ async createPractice() {
134
+ if (busy() || state().step !== "practice") return;
135
+ setBusy(true);
136
+ setError(null);
137
+ try {
138
+ const practice = await options.createPractice();
139
+ if (disposed) return;
140
+ send({ type: "practice-created", practice });
141
+ if (state().active) await options.openPractice(practice);
142
+ } catch (cause) {
143
+ if (!disposed)
144
+ setError(
145
+ cause instanceof Error ? cause.message : "Could not create the practice session.",
146
+ );
147
+ } finally {
148
+ if (!disposed) setBusy(false);
149
+ }
150
+ },
151
+ async openPractice() {
152
+ const practice = state().practice;
153
+ if (!practice || busy()) return;
154
+ setError(null);
155
+ try {
156
+ if (state().active) await options.openPractice(practice);
157
+ } catch (cause) {
158
+ setError(cause instanceof Error ? cause.message : "Practice session is unavailable.");
159
+ }
160
+ },
161
+ };
162
+ }
@@ -14,6 +14,9 @@ import {
14
14
  import type { HomeAgentSelectionSnapshot } from "./application-home-agent-selection.ts";
15
15
 
16
16
  export interface HomeAgentRosterProps {
17
+ readonly filterLabel?: string;
18
+ readonly onCycleMachine?: () => void;
19
+ readonly onToggleAttention?: () => void;
17
20
  readonly theme: SemanticThemeSnapshot;
18
21
  readonly width: number;
19
22
  readonly height: number;
@@ -46,6 +49,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
46
49
  const width = () => Math.max(0, Math.floor(props.width));
47
50
  const height = () => Math.max(0, Math.floor(props.height));
48
51
  const stale = (row: HomeAgentRow) =>
52
+ row.disabled ||
49
53
  (props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
50
54
  (props.snapshot.unavailableSessionKeys ?? []).includes(row.sessionKey);
51
55
  const showRecovery = () =>
@@ -83,9 +87,12 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
83
87
  return "No agents reported";
84
88
  const count = props.snapshot.rows.length;
85
89
  const attention = props.snapshot.rows.filter(
86
- (row) => row.attention || row.activity === "waiting" || row.activity === "failed",
90
+ (row) =>
91
+ !stale(row) && (row.attention || row.activity === "waiting" || row.activity === "failed"),
92
+ ).length;
93
+ const working = props.snapshot.rows.filter(
94
+ (row) => !stale(row) && row.activity === "running",
87
95
  ).length;
88
- const working = props.snapshot.rows.filter((row) => row.activity === "running").length;
89
96
  return `${count} observed ${count === 1 ? "agent" : "agents"} · ${attention} ${attention === 1 ? "needs" : "need"} attention · ${working} working`;
90
97
  };
91
98
  const coverage = () =>
@@ -102,13 +109,22 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
102
109
  if (visibleCount() === 0) return "Enlarge the terminal to view agents.";
103
110
  const selected = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
104
111
  if (selected && stale(selected))
105
- return `${selected.sessionName} · last observed; waiting for fresh signals`;
106
- if (width() < 44 && selected) return `${selected.sessionName} · Enter open`;
112
+ return `${[selected.machineLabel, selected.serverLabel, selected.sessionName].filter(Boolean).join(" / ")} · last observed; waiting for fresh signals`;
113
+ if (selected?.machineLabel || (width() < 44 && selected))
114
+ return `${[selected?.machineLabel, selected?.serverLabel, selected?.sessionName].filter(Boolean).join(" / ")} · Enter open`;
107
115
  return `${offset() + 1}–${Math.min(props.snapshot.rows.length, offset() + visibleCount())} of ${props.snapshot.rows.length} · ↑↓ select · Enter open`;
108
116
  };
109
117
  useKeyboardRoute((event) => {
110
118
  if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta) return false;
111
119
  const key = event.name.toLowerCase();
120
+ const filter =
121
+ key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
122
+ if (filter) {
123
+ event.preventDefault();
124
+ event.stopPropagation();
125
+ filter();
126
+ return true;
127
+ }
112
128
  const retry =
113
129
  key === "r" &&
114
130
  (props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
@@ -160,14 +176,19 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
160
176
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
161
177
  {clipTerminal(coverage(), width())}
162
178
  </text>
163
- <box height={1} flexShrink={0} />
179
+ <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
180
+ {clipTerminal(
181
+ props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "",
182
+ width(),
183
+ )}
184
+ </text>
164
185
  <Show
165
186
  when={props.snapshot.rows.length > 0}
166
187
  fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0} />}
167
188
  >
168
189
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
169
190
  {clipTerminal(
170
- ` ${padCells("AGENT", columns().agent)}${padCells("SESSION", columns().session)} ${padCells("STATUS", columns().status)}`,
191
+ ` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`,
171
192
  width(),
172
193
  )}
173
194
  </text>
@@ -195,10 +216,15 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
195
216
  width={width()}
196
217
  label={
197
218
  padCells(row().name, columns().agent) +
198
- padCells(row().sessionName, columns().session)
219
+ padCells(
220
+ [row().machineLabel, row().serverLabel, row().sessionName]
221
+ .filter(Boolean)
222
+ .join(" / "),
223
+ columns().session,
224
+ )
199
225
  }
200
226
  detail={padCells(
201
- `${row().attention ? "! " : ""}${homeAgentStatusLabel(row().activity)}${stale(row()) ? "*" : ""}`,
227
+ `${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`,
202
228
  columns().status,
203
229
  )}
204
230
  detailAlign="end"
@@ -206,8 +232,8 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
206
232
  selected={props.selection.selectedKey === key}
207
233
  focused={props.inputActive && props.selection.selectedKey === key}
208
234
  hovered={hovered() === key}
209
- attention={row().attention}
210
- status={statusTone(row())}
235
+ attention={!stale(row()) && row().attention}
236
+ status={stale(row()) ? "unknown" : statusTone(row())}
211
237
  disabled={row().paneId === null || stale(row())}
212
238
  onActivate={(source) => {
213
239
  if (!props.inputActive || row().paneId === null || stale(row())) return;
@@ -1,6 +1,8 @@
1
1
  import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.ts";
2
2
  import {
3
3
  ApplicationShellResourceV2SchemaZ,
4
+ ApplicationShellProjectionInputV2SchemaZ,
5
+ TmuxServerScopeSchemaZ,
4
6
  DaemonEventClientFrameSchemaZ,
5
7
  DaemonEventServerFrameSchemaZ,
6
8
  WorkspaceCatalogResourceV3SchemaZ,
@@ -66,6 +68,25 @@ export function createApplicationHomeAgentTransport(
66
68
  if (!response.ok) throw new Error(`Agent observation returned HTTP ${response.status}.`);
67
69
  return response.json();
68
70
  };
71
+ if (session.server) {
72
+ const server = TmuxServerScopeSchemaZ.parse(session.server);
73
+ const base = `/api/v1/tmux-servers/${server.serverId}/${server.generation}`;
74
+ const raw = await get(
75
+ `${base}/application-shell/${encodeURIComponent(session.workspaceName ?? session.name)}?liveSessionId=${encodeURIComponent(session.liveSessionId ?? "")}`,
76
+ );
77
+ const actual = TmuxServerScopeSchemaZ.parse(raw.server);
78
+ if (
79
+ raw.version !== 1 ||
80
+ actual.serverId !== server.serverId ||
81
+ actual.generation !== server.generation
82
+ )
83
+ throw new Error("Agent server incarnation changed.");
84
+ return {
85
+ version: 2,
86
+ daemon,
87
+ resource: ApplicationShellProjectionInputV2SchemaZ.parse(raw.resource),
88
+ };
89
+ }
69
90
  const shell = ApplicationShellResourceV2SchemaZ.parse(
70
91
  await get(`/api/project/${encodeURIComponent(session.name)}/application-shell?version=2`),
71
92
  );
@@ -87,6 +108,54 @@ export function createApplicationHomeAgentTransport(
87
108
  return shell;
88
109
  },
89
110
  connect(daemon, sessions, handlers) {
111
+ const scoped = sessions.filter((session) => session.server);
112
+ if (scoped.length > 0) {
113
+ // Scoped session events currently carry topology, not agent activity.
114
+ // Refresh metadata with a bounded cadence; never attach terminal streams.
115
+ // Keep legacy observations independent from scoped owner availability.
116
+ const pushed = sessions.filter(
117
+ (session) => !session.server || session.server.generation === daemon.instanceId,
118
+ );
119
+ let closed = false;
120
+ let pushHealthy = false;
121
+ const interval = setInterval(() => {
122
+ if (!closed)
123
+ for (const session of scoped) {
124
+ if (!pushHealthy || session.server?.generation !== daemon.instanceId)
125
+ handlers.invalidate(session.id);
126
+ }
127
+ }, 5_000);
128
+ interval.unref?.();
129
+ const connection = pushed.length
130
+ ? createApplicationHomeAgentTransport(options).connect(
131
+ daemon,
132
+ pushed.map((session) => ({ ...session, server: undefined })),
133
+ {
134
+ ...handlers,
135
+ // The root event stream is advisory for scoped reads. Failure must
136
+ // not stall independent scoped owners or suppress their rows.
137
+ ready: (unavailable) => {
138
+ pushHealthy = unavailable.length === 0;
139
+ if (!closed) for (const session of pushed) handlers.invalidate(session.id);
140
+ },
141
+ unavailable: () => {
142
+ pushHealthy = false;
143
+ if (!closed) for (const session of pushed) handlers.invalidate(session.id);
144
+ },
145
+ },
146
+ )
147
+ : null;
148
+ queueMicrotask(() => {
149
+ if (!closed) handlers.ready([]);
150
+ });
151
+ return {
152
+ close() {
153
+ closed = true;
154
+ clearInterval(interval);
155
+ connection?.close();
156
+ },
157
+ };
158
+ }
90
159
  let closed = false;
91
160
  let failed = false;
92
161
  const sockets: HomeAgentEventSocket[] = [];
@@ -27,6 +27,9 @@ import {
27
27
 
28
28
  export type ApplicationHomeAgentPresentation = Pick<
29
29
  ApplicationHomeSurfaceProps,
30
+ | "agentFilterLabel"
31
+ | "onCycleAgentMachine"
32
+ | "onToggleAgentAttention"
30
33
  | "agentRoster"
31
34
  | "agentSelection"
32
35
  | "agentInputActive"
@@ -197,6 +200,7 @@ export function createApplicationHomeAgentsOwner(options: {
197
200
 
198
201
  /** Compose Home navigation with competing chrome intents; physical input stays in the root. */
199
202
  export function createApplicationHomeNavigationOwner(options: {
203
+ readonly fleetHome?: ApplicationHomeAgentPresentation;
200
204
  readonly fleetCommands?: () => readonly ApplicationPaletteCommand[];
201
205
  readonly openFleet?: (
202
206
  command: Exclude<ApplicationPaletteCommand, string>,
@@ -236,7 +240,7 @@ export function createApplicationHomeNavigationOwner(options: {
236
240
  );
237
241
  const homeAgents = createApplicationHomeAgentsOwner({
238
242
  catalog: options.catalog.snapshot,
239
- active: () => options.activeSurface() === "home",
243
+ active: () => !options.fleetHome && options.activeSurface() === "home",
240
244
  inputActive: () =>
241
245
  options.activeSurface() === "home" &&
242
246
  !paneRename.draft() &&
@@ -307,5 +311,11 @@ export function createApplicationHomeNavigationOwner(options: {
307
311
  onNavigationIntent: homeAgents.cancel,
308
312
  });
309
313
  const paletteCommandList = paletteCommands.commands;
310
- return { homeAgents, paneRename, paletteCommands, paletteCommandList, openAgent };
314
+ return {
315
+ homeAgents: options.fleetHome ? { ...homeAgents, presentation: options.fleetHome } : homeAgents,
316
+ paneRename,
317
+ paletteCommands,
318
+ paletteCommandList,
319
+ openAgent,
320
+ };
311
321
  }
@@ -1,10 +1,19 @@
1
- import type { AgentActivity, ApplicationShellResourceV2 } from "@tmux-ide/contracts";
1
+ import type {
2
+ AgentActivity,
3
+ ApplicationShellResourceV2,
4
+ TmuxServerScope,
5
+ } from "@tmux-ide/contracts";
2
6
 
3
7
  import type { ApplicationHomeCatalogSession } from "./application-home-catalog.ts";
4
8
  import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.ts";
5
9
 
6
10
  export interface HomeAgentRow {
7
11
  readonly key: string;
12
+ readonly machineId?: string;
13
+ readonly machineLabel?: string;
14
+ readonly server?: TmuxServerScope;
15
+ readonly serverLabel?: string;
16
+ readonly disabled?: boolean;
8
17
  readonly sessionKey: string;
9
18
  readonly sessionName: string;
10
19
  readonly liveSessionId: string;
@@ -56,11 +65,13 @@ export function projectHomeAgentRows(
56
65
  return shell.resource.workspace.sidebar.agents.map((agent) => ({
57
66
  key: `${session.id}\u0000${agent.id}`,
58
67
  sessionKey: session.id,
68
+ server: session.server,
69
+ serverLabel: session.serverLabel,
59
70
  sessionName: session.name,
60
71
  // Older catalog fixtures/callers retain a generation-qualified incarnation
61
72
  // key even when they do not expose the separately named wire field.
62
73
  liveSessionId: session.liveSessionId ?? session.id,
63
- daemonInstanceId: shell.daemon.instanceId,
74
+ daemonInstanceId: session.server?.generation ?? shell.daemon.instanceId,
64
75
  agentId: agent.id,
65
76
  paneId: agent.paneId,
66
77
  windowId:
@@ -6,6 +6,7 @@ import {
6
6
  moveHomeCatalogSelection,
7
7
  selectedHomeCatalogIndex,
8
8
  type ApplicationHomeCatalog,
9
+ type ApplicationHomeCatalogSession,
9
10
  type ApplicationHomeCatalogSnapshot,
10
11
  } from "./application-home-catalog.ts";
11
12
  import { createFleetSession } from "./fleet-lifecycle-client.ts";
@@ -26,6 +27,7 @@ export interface ApplicationHomeCatalogOwnerOptions {
26
27
  readonly lifecycle: Pick<TuiApplicationLifecycle, "registerCloser">;
27
28
  readonly automaticOpen: boolean;
28
29
  readonly automaticOpenAllowed?: () => boolean;
30
+ readonly openCatalogSession?: (session: ApplicationHomeCatalogSession) => Promise<unknown> | void;
29
31
  readonly startGeneration: (sessionName: string) => Promise<unknown> | void;
30
32
  readonly setNote?: (note: string | null) => void;
31
33
  readonly catalog?: ApplicationHomeCatalog;
@@ -85,7 +87,12 @@ export function createApplicationHomeCatalogOwner(
85
87
  const currentSessions = next.sessions;
86
88
  if (automaticOpen && next.phase === "live") {
87
89
  automaticOpen = false;
88
- if (currentSessions.length === 1) void options.startGeneration(currentSessions[0]!.name);
90
+ if (currentSessions.length === 1) {
91
+ const session = currentSessions[0]!;
92
+ void (options.openCatalogSession
93
+ ? options.openCatalogSession(session)
94
+ : options.startGeneration(session.name));
95
+ }
89
96
  }
90
97
  if (currentSessions.length === 0) {
91
98
  if (current !== null) setSelectedSessionId(null);
@@ -120,8 +127,11 @@ export function createApplicationHomeCatalogOwner(
120
127
  return true;
121
128
  }
122
129
  if (name === "return" || name === "enter") {
123
- const sessionName = sessions()[selectedSessionIndex()]?.name;
124
- if (sessionName) void options.startGeneration(sessionName);
130
+ const session = sessions()[selectedSessionIndex()];
131
+ if (session)
132
+ void (options.openCatalogSession
133
+ ? options.openCatalogSession(session)
134
+ : options.startGeneration(session.name));
125
135
  return true;
126
136
  }
127
137
  return false;