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.
- package/bin/cli.js +46791 -43589
- package/package.json +2 -2
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +2 -2
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +35 -0
- package/packages/contracts/src/__tests__/issue-error.test.ts +1 -1
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +3 -1
- package/packages/contracts/src/__tests__/pane-stream.test.ts +17 -5
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +166 -0
- package/packages/contracts/src/__tests__/window-links.test.ts +243 -0
- package/packages/contracts/src/actions-contract.ts +12 -0
- package/packages/contracts/src/daemon-resource-request.test.ts +1 -1
- package/packages/contracts/src/daemon-wire.ts +10 -5
- package/packages/contracts/src/fleet-client-state.ts +3 -0
- package/packages/contracts/src/fleet-lifecycle.ts +5 -0
- package/packages/contracts/src/index.ts +2 -0
- package/packages/contracts/src/interaction-receipts.ts +39 -0
- package/packages/contracts/src/issue-error.ts +1 -1
- package/packages/contracts/src/multiplexer-verbs.ts +25 -1
- package/packages/contracts/src/pane-stream.ts +51 -8
- package/packages/contracts/src/tmux-server-scope.ts +172 -0
- package/packages/contracts/src/window-links.ts +76 -0
- package/packages/contracts/src/workspace-multiplexer.ts +54 -0
- package/packages/core/src/interaction-receipts.ts +4 -0
- package/packages/daemon/dist/command-center/actions/command-definitions.js +6 -0
- package/packages/daemon/dist/command-center/actions/handlers/workspace-multiplexer.js +2 -0
- package/packages/daemon/dist/command-center/actions/registry.js +13 -1
- package/packages/daemon/dist/command-center/discovery.js +2 -9
- package/packages/daemon/dist/command-center/server.js +21 -1
- package/packages/daemon/dist/command-center/tmux-server-native-backing.js +37 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +351 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +64 -8
- package/packages/daemon/dist/lib/canonical-daemon.js +5 -2
- package/packages/daemon/dist/lib/daemon-embed.js +198 -30
- package/packages/daemon/dist/lib/embedded-tmux-server-owners.js +147 -0
- package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +31 -11
- package/packages/daemon/dist/lib/headless-daemon.js +10 -1
- package/packages/daemon/dist/lib/runtime-namespace.js +17 -3
- package/packages/daemon/dist/lib/tmux-client-execution.js +59 -0
- package/packages/daemon/dist/lib/tmux-named-socket-fence.js +4 -0
- package/packages/daemon/dist/lib/tmux-server-generation-runner.js +62 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +285 -0
- package/packages/daemon/dist/lib/tmux-server-owners.js +192 -0
- package/packages/daemon/dist/lib/tmux-server-proof.js +82 -0
- package/packages/daemon/dist/lib/tmux-server-registration.js +85 -0
- package/packages/daemon/dist/lib/tmux-server-session-create.js +43 -0
- package/packages/daemon/dist/lib/tmux-server-session-open.js +101 -0
- package/packages/daemon/dist/lib/tmux-servers-cli.js +134 -0
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +55 -0
- package/packages/daemon/dist/lib/tmux-window-link-guard.js +88 -0
- package/packages/daemon/dist/lib/workspace-config-loader.js +3 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +82 -1
- package/packages/daemon/dist/lib/workspace-pane-creation.js +13 -57
- package/packages/daemon/dist/lib/workspace-promotion.js +91 -12
- package/packages/daemon/dist/lib/workspace-registry.js +4 -0
- package/packages/daemon/dist/server/pane-stream-upgrade.js +20 -6
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +61 -17
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +9 -1
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +11 -2
- package/packages/daemon/dist/terminal/mirror/control-channel.js +65 -6
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +44 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +210 -50
- package/packages/daemon/dist/terminal/mirror/window-link-authority.js +160 -0
- package/packages/daemon/dist/terminal/pane-stream/lease-manager.js +3 -3
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +40 -6
- package/packages/daemon/dist/terminal/protocol/live-session-identity.js +9 -0
- package/packages/daemon/dist/terminal/protocol/native-backing-client.js +2 -1
- package/packages/daemon/dist/terminal/session-runtime/interaction-receipt-facts.js +9 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +6 -0
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-resource-changes.js +1 -0
- package/packages/daemon/dist/tui/mirror/application-shell-daemon-connection.js +7 -2
- package/packages/daemon/dist/tui/mirror/application-shell-server-connection.js +114 -0
- package/packages/daemon/dist/tui/mirror/menu-model.js +1 -1
- package/packages/daemon/dist/tui/mirror/open-tui-verified-routing.js +34 -4
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +29 -8
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog-owner.js +11 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-home-catalog.js +140 -75
- package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +211 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
- package/packages/daemon/dist/tui/mirror/runtime/application-root-configuration.js +9 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-route-connection.js +95 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +158 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace-policy.js +43 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +78 -25
- package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
- package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +44 -1
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
- package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +6 -3
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +11 -2
- package/packages/daemon/dist/tui/mirror/workspace/terminal-window-strip.jsx +2 -1
- package/packages/daemon/src/command-center/actions/command-definitions.ts +6 -0
- package/packages/daemon/src/command-center/actions/errors.ts +4 -0
- package/packages/daemon/src/command-center/actions/handlers/workspace-multiplexer.ts +2 -0
- package/packages/daemon/src/command-center/actions/registry.ts +14 -0
- package/packages/daemon/src/command-center/discovery.ts +2 -10
- package/packages/daemon/src/command-center/server.ts +29 -1
- package/packages/daemon/src/command-center/tmux-server-native-backing.ts +49 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +479 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +111 -4
- package/packages/daemon/src/lib/canonical-daemon.ts +15 -3
- package/packages/daemon/src/lib/daemon-embed.ts +241 -28
- package/packages/daemon/src/lib/embedded-tmux-server-owners.ts +190 -0
- package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +33 -9
- package/packages/daemon/src/lib/headless-daemon.ts +18 -1
- package/packages/daemon/src/lib/runtime-namespace.ts +24 -3
- package/packages/daemon/src/lib/tmux-client-execution.ts +56 -0
- package/packages/daemon/src/lib/tmux-named-socket-fence.ts +5 -0
- package/packages/daemon/src/lib/tmux-server-generation-runner.ts +81 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +336 -0
- package/packages/daemon/src/lib/tmux-server-owners.ts +228 -0
- package/packages/daemon/src/lib/tmux-server-proof.ts +95 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +105 -0
- package/packages/daemon/src/lib/tmux-server-session-create.ts +52 -0
- package/packages/daemon/src/lib/tmux-server-session-open.ts +106 -0
- package/packages/daemon/src/lib/tmux-servers-cli.ts +186 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +62 -0
- package/packages/daemon/src/lib/tmux-window-link-guard.ts +112 -0
- package/packages/daemon/src/lib/workspace-config-loader.ts +3 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +121 -1
- package/packages/daemon/src/lib/workspace-pane-creation.ts +16 -55
- package/packages/daemon/src/lib/workspace-promotion.ts +98 -12
- package/packages/daemon/src/lib/workspace-registry.ts +5 -0
- package/packages/daemon/src/server/pane-stream-upgrade.ts +25 -5
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +70 -16
- package/packages/daemon/src/terminal/mirror/control-channel.ts +64 -7
- package/packages/daemon/src/terminal/mirror/events.ts +2 -0
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +56 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +245 -50
- package/packages/daemon/src/terminal/mirror/window-link-authority.ts +203 -0
- package/packages/daemon/src/terminal/pane-stream/lease-manager.ts +3 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +50 -6
- package/packages/daemon/src/terminal/protocol/live-session-identity.ts +14 -0
- package/packages/daemon/src/terminal/protocol/native-backing-client.ts +4 -1
- package/packages/daemon/src/terminal/session-runtime/interaction-receipt-facts.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +9 -0
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +2 -2
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-resource-changes.ts +1 -0
- package/packages/daemon/src/tui/mirror/application-shell-daemon-connection.ts +28 -2
- package/packages/daemon/src/tui/mirror/application-shell-server-connection.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +1 -1
- package/packages/daemon/src/tui/mirror/open-tui-verified-routing.ts +41 -3
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +47 -6
- package/packages/daemon/src/tui/mirror/runtime/application-entry.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +10 -4
- package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
- package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
- package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog-owner.ts +13 -3
- package/packages/daemon/src/tui/mirror/runtime/application-home-catalog.ts +172 -100
- package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +285 -36
- package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +124 -40
- package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
- package/packages/daemon/src/tui/mirror/runtime/application-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +40 -40
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +10 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +177 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-policy.ts +62 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +87 -30
- package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
- package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +55 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
- package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +15 -4
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +13 -2
- package/packages/daemon/src/tui/mirror/workspace/terminal-window-strip.tsx +5 -1
- package/packages/daemon-client/package.json +3 -1
- package/packages/daemon-client/src/index.ts +2 -0
- package/packages/daemon-client/src/pane-stream-client.test.ts +108 -38
- package/packages/daemon-client/src/pane-stream-client.ts +26 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +114 -0
- package/packages/daemon-client/src/scoped-tmux-server-transport.ts +385 -0
- package/packages/daemon-client/src/tmux-server-client.test.ts +279 -0
- package/packages/daemon-client/src/tmux-server-client.ts +287 -0
|
@@ -109,14 +109,18 @@ function layoutKey(frame) {
|
|
|
109
109
|
return null;
|
|
110
110
|
return `panes:${panes.join("\u0000")}`;
|
|
111
111
|
}
|
|
112
|
-
function layoutSnapshot(windows, currentWindowKey = null) {
|
|
112
|
+
function layoutSnapshot(windows, currentWindowKey = null, windowLinks = null) {
|
|
113
113
|
const selectedKey = currentWindowKey ??
|
|
114
114
|
[...windows.entries()].find(([, frame]) => frame.currentWindow)?.[0] ??
|
|
115
115
|
null;
|
|
116
116
|
const frames = Object.freeze([...windows.entries()].map(([key, frame]) => frame.currentWindow === (key === selectedKey)
|
|
117
117
|
? frame
|
|
118
118
|
: freezeLayout({ ...frame, currentWindow: key === selectedKey })));
|
|
119
|
+
const links = windowLinks?.links.map((link) => Object.freeze({ ...link }));
|
|
120
|
+
if (links)
|
|
121
|
+
Object.freeze(links);
|
|
119
122
|
return Object.freeze({
|
|
123
|
+
windowLinks: windowLinks && links ? Object.freeze({ ...windowLinks, links }) : null,
|
|
120
124
|
current: frames.find((frame) => frame.currentWindow) ?? null,
|
|
121
125
|
windows: frames,
|
|
122
126
|
});
|
|
@@ -144,7 +148,8 @@ function layoutFrameSemanticallyEqual(left, right) {
|
|
|
144
148
|
}));
|
|
145
149
|
}
|
|
146
150
|
function layoutSnapshotsSemanticallyEqual(left, right) {
|
|
147
|
-
return (left.
|
|
151
|
+
return (JSON.stringify(left.windowLinks ?? null) === JSON.stringify(right.windowLinks ?? null) &&
|
|
152
|
+
left.windows.length === right.windows.length &&
|
|
148
153
|
left.windows.every((window, index) => {
|
|
149
154
|
const candidate = right.windows[index];
|
|
150
155
|
return candidate !== undefined && layoutFrameSemanticallyEqual(window, candidate);
|
|
@@ -702,6 +707,8 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
702
707
|
if (coherentSettled ||
|
|
703
708
|
closed ||
|
|
704
709
|
!physicalReady ||
|
|
710
|
+
(routing.liveSessionId !== undefined &&
|
|
711
|
+
latestLayoutSnapshot.windowLinks?.liveSessionId !== routing.liveSessionId) ||
|
|
705
712
|
!layoutExactlyCoversPanes(latestLayoutSnapshot, panes) ||
|
|
706
713
|
!latestLayoutSnapshot.current?.panes.every(({ pane }) => typeof pane === "string" && canonicalSeedPanes.has(pane))) {
|
|
707
714
|
return;
|
|
@@ -866,7 +873,7 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
866
873
|
: {}),
|
|
867
874
|
signal: options.signal,
|
|
868
875
|
stream: {
|
|
869
|
-
protocolVersion:
|
|
876
|
+
protocolVersion: 2,
|
|
870
877
|
workspaceName: inventory.workspaceName,
|
|
871
878
|
panes: [...panes],
|
|
872
879
|
viewerMode: "interactive",
|
|
@@ -981,13 +988,16 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
981
988
|
});
|
|
982
989
|
return;
|
|
983
990
|
}
|
|
991
|
+
if (latestLayoutSnapshot.windowLinks &&
|
|
992
|
+
!latestLayoutSnapshot.windowLinks.links.some((link) => link.semanticWindowId === retained.semanticWindowId))
|
|
993
|
+
return;
|
|
984
994
|
layoutsByWindow.set(key, retained);
|
|
985
995
|
// tmux reports a switch as incumbent=false followed by target=true.
|
|
986
996
|
// Retain the incumbent through that pair and publish one unique current
|
|
987
997
|
// window only when the positive target frame arrives.
|
|
988
|
-
if (retained.currentWindow)
|
|
998
|
+
if (!latestLayoutSnapshot.windowLinks && retained.currentWindow)
|
|
989
999
|
currentWindowKey = key;
|
|
990
|
-
const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey);
|
|
1000
|
+
const candidate = layoutSnapshot(layoutsByWindow, currentWindowKey, latestLayoutSnapshot.windowLinks ?? null);
|
|
991
1001
|
if (!layoutExactlyCoversPanes(candidate, panes)) {
|
|
992
1002
|
if (lastRejectedLayoutSnapshot &&
|
|
993
1003
|
layoutSnapshotsSemanticallyEqual(candidate, lastRejectedLayoutSnapshot))
|
|
@@ -1023,6 +1033,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1023
1033
|
onLayoutSnapshot: (snapshot) => {
|
|
1024
1034
|
if (closed)
|
|
1025
1035
|
return;
|
|
1036
|
+
if (routing.liveSessionId !== undefined &&
|
|
1037
|
+
snapshot.windowLinks.liveSessionId !== routing.liveSessionId)
|
|
1038
|
+
return failConnection(new Error("Selected live session was replaced before terminal activation"));
|
|
1026
1039
|
const replacement = new Map();
|
|
1027
1040
|
let replacementCurrent = null;
|
|
1028
1041
|
for (const frame of snapshot.layouts) {
|
|
@@ -1030,14 +1043,15 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1030
1043
|
const key = layoutKey(retained);
|
|
1031
1044
|
if (key === null || replacement.has(key))
|
|
1032
1045
|
return failConnection(new Error("invalid layout snapshot"));
|
|
1033
|
-
|
|
1046
|
+
const previous = layoutsByWindow.get(key);
|
|
1047
|
+
replacement.set(key, previous && layoutFrameSemanticallyEqual(previous, retained) ? previous : retained);
|
|
1034
1048
|
if (retained.currentWindow) {
|
|
1035
1049
|
if (replacementCurrent !== null)
|
|
1036
1050
|
return failConnection(new Error("invalid layout snapshot current window"));
|
|
1037
1051
|
replacementCurrent = key;
|
|
1038
1052
|
}
|
|
1039
1053
|
}
|
|
1040
|
-
const candidate = layoutSnapshot(replacement, replacementCurrent);
|
|
1054
|
+
const candidate = layoutSnapshot(replacement, replacementCurrent, snapshot.windowLinks);
|
|
1041
1055
|
if (!layoutExactlyCoversPanes(candidate, panes))
|
|
1042
1056
|
return failConnection(new Error("layout snapshot does not cover terminal inventory"));
|
|
1043
1057
|
layoutsByWindow.clear();
|
|
@@ -1048,6 +1062,11 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1048
1062
|
if (layoutSnapshotsSemanticallyEqual(candidate, latestLayoutSnapshot))
|
|
1049
1063
|
return;
|
|
1050
1064
|
latestLayoutSnapshot = candidate;
|
|
1065
|
+
options.onDiagnostic?.("layout", {
|
|
1066
|
+
windows: candidate.windows.length,
|
|
1067
|
+
current: candidate.current !== null,
|
|
1068
|
+
links: candidate.windowLinks?.links.length ?? 0,
|
|
1069
|
+
});
|
|
1051
1070
|
settleCoherent();
|
|
1052
1071
|
for (const listener of [...layoutListeners]) {
|
|
1053
1072
|
try {
|
|
@@ -1198,7 +1217,9 @@ export async function connectOpenTuiWorkspaceRuntimePort(options) {
|
|
|
1198
1217
|
submitIntent: async (operationId, intent) => {
|
|
1199
1218
|
// Structural commands can affect hidden panes. Do not defer user intent
|
|
1200
1219
|
// until a different topology may be current; fail closed during seeding.
|
|
1201
|
-
if (closed ||
|
|
1220
|
+
if (closed ||
|
|
1221
|
+
!latestLayoutSnapshot.windowLinks ||
|
|
1222
|
+
[...endpoints.values()].some((endpoint) => !endpoint.inputReady))
|
|
1202
1223
|
throw new Error("Terminal inventory is still receiving canonical state");
|
|
1203
1224
|
return (await opened.submitIntent(operationId, intent)) ?? undefined;
|
|
1204
1225
|
},
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* @jsxImportSource @opentui/solid */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createEffect, createMemo, Show } from "solid-js";
|
|
4
|
+
import { createApplicationGuidedTourOwner } from "./application-guided-tour-owner.js";
|
|
5
|
+
import { GuidedTourCoach } from "./guided-tour-coach.jsx";
|
|
6
|
+
import { createFleetSession } from "./fleet-lifecycle-client.js";
|
|
7
|
+
import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
|
|
8
|
+
export function createApplicationGuidedTourIntegration(options) {
|
|
9
|
+
const { machines, lifecycle, generation, generationMachineId, layoutSnapshot, sessionName, focusedPane, activeSurface, appearance, paletteOpen, paletteCommands, dimensions, blocked, } = options;
|
|
10
|
+
const practiceRows = () => machines.catalog
|
|
11
|
+
.getSnapshot()
|
|
12
|
+
.groups.filter((group) => group.id === "local" || group.routeIds?.includes("local"))
|
|
13
|
+
.flatMap((group) => group.sessions)
|
|
14
|
+
.filter((row) => !row.disabled);
|
|
15
|
+
const practiceIdentity = (row) => {
|
|
16
|
+
const daemon = applicationMachineAuthorityManager.getMachine("local")?.read();
|
|
17
|
+
if (!row.liveSessionId || !daemon)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
machineId: "local",
|
|
21
|
+
serverId: row.server?.serverId ?? "default",
|
|
22
|
+
generation: row.server?.generation ?? daemon.instanceId,
|
|
23
|
+
sessionId: row.liveSessionId,
|
|
24
|
+
sessionName: row.name,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const tour = createApplicationGuidedTourOwner({
|
|
28
|
+
async createPractice() {
|
|
29
|
+
const handle = applicationMachineAuthorityManager.getMachine("local");
|
|
30
|
+
if (!handle || handle.endpoint().state !== "ready")
|
|
31
|
+
throw new Error("Connect this machine before creating a practice session.");
|
|
32
|
+
const created = await createFleetSession(handle, `tmux-ide-practice-${randomUUID().slice(0, 8)}`, undefined, { includeLiveSessionId: true });
|
|
33
|
+
if (!created || created.outcome !== "created")
|
|
34
|
+
throw new Error("Could not create a fresh practice session. Try again.");
|
|
35
|
+
if (!created.liveSessionId)
|
|
36
|
+
throw new Error("Update the local tmux-ide daemon to use the guided practice session, then restart the tour.");
|
|
37
|
+
return await new Promise((resolve, reject) => {
|
|
38
|
+
let done = false;
|
|
39
|
+
let stop = () => { };
|
|
40
|
+
const abort = () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
stop();
|
|
43
|
+
reject(new Error("Tour closed."));
|
|
44
|
+
};
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
47
|
+
stop();
|
|
48
|
+
reject(new Error("Practice session was created but has not appeared yet. Find it in the local session list."));
|
|
49
|
+
}, 10_000);
|
|
50
|
+
lifecycle.signal.addEventListener("abort", abort, { once: true });
|
|
51
|
+
const check = () => {
|
|
52
|
+
const rows = practiceRows().filter((row) => row.name === created.workspaceName && row.liveSessionId === created.liveSessionId);
|
|
53
|
+
const identity = rows.length === 1 ? practiceIdentity(rows[0]) : null;
|
|
54
|
+
if (!identity || identity.generation !== created.daemonInstanceId)
|
|
55
|
+
return;
|
|
56
|
+
done = true;
|
|
57
|
+
lifecycle.signal.removeEventListener("abort", abort);
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
stop();
|
|
60
|
+
resolve(identity);
|
|
61
|
+
};
|
|
62
|
+
stop = machines.catalog.subscribe(check);
|
|
63
|
+
if (done)
|
|
64
|
+
stop();
|
|
65
|
+
else if (lifecycle.signal.aborted)
|
|
66
|
+
abort();
|
|
67
|
+
else
|
|
68
|
+
check();
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
openPractice(practice) {
|
|
72
|
+
if (practice.machineId !== "local")
|
|
73
|
+
throw new Error("Practice sessions must belong to this machine. Restart the tour.");
|
|
74
|
+
const row = practiceRows().find((row) => {
|
|
75
|
+
const identity = practiceIdentity(row);
|
|
76
|
+
return (identity &&
|
|
77
|
+
identity.serverId === practice.serverId &&
|
|
78
|
+
identity.generation === practice.generation &&
|
|
79
|
+
identity.sessionId === practice.sessionId);
|
|
80
|
+
});
|
|
81
|
+
if (!row)
|
|
82
|
+
throw new Error("That practice session is unavailable or has been replaced. Replay the tour to create a new one.");
|
|
83
|
+
machines.sidebar.onOpen("local", row.name, "mouse", row.id);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const committedTourTheme = createMemo((previous) => appearance.pickerOpen()
|
|
87
|
+
? (previous ?? JSON.stringify(appearance.theme().canonical))
|
|
88
|
+
: JSON.stringify(appearance.theme().canonical));
|
|
89
|
+
createEffect(() => {
|
|
90
|
+
const current = generation();
|
|
91
|
+
const connection = current?.status === "live" ? current.connection : null;
|
|
92
|
+
const machineId = generationMachineId();
|
|
93
|
+
const practice = connection?.liveSessionId && current?.daemonGeneration && machineId
|
|
94
|
+
? {
|
|
95
|
+
machineId,
|
|
96
|
+
serverId: connection.server?.serverId ?? "default",
|
|
97
|
+
generation: connection.server?.generation ?? current.daemonGeneration,
|
|
98
|
+
sessionId: connection.liveSessionId,
|
|
99
|
+
sessionName: sessionName() ?? "",
|
|
100
|
+
}
|
|
101
|
+
: null;
|
|
102
|
+
tour.observe({
|
|
103
|
+
practice,
|
|
104
|
+
panes: (layoutSnapshot().current?.panes ?? []).flatMap((pane) => pane.pane ? [{ id: pane.pane, width: pane.width, height: pane.height }] : []),
|
|
105
|
+
focusedPane: focusedPane(),
|
|
106
|
+
surface: activeSurface(),
|
|
107
|
+
paletteOpen: paletteOpen(),
|
|
108
|
+
theme: committedTourTheme(),
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
...tour,
|
|
113
|
+
Coach: () => (<Show when={tour.state().active && !blocked()}>
|
|
114
|
+
<box position="absolute" right={1} bottom={2} width={Math.max(1, Math.min(68, dimensions().width - 2))} zIndex={30}>
|
|
115
|
+
<GuidedTourCoach height={Math.max(4, Math.min(10, dimensions().height - 4))} state={tour.state()} theme={appearance.theme()} width={Math.max(1, Math.min(68, dimensions().width - 2))} shortcuts={{ commands: "F5", home: "F1" }} error={tour.error()} busy={tour.busy()} onPause={tour.pause} onReplay={tour.replay} onWelcomeRead={tour.welcomeRead} onCreatePractice={() => void tour.createPractice()} onOpenCommands={() => paletteCommands.setOpen(true, "mouse")} onOpenAppearance={appearance.openPicker} onOpenHome={() => paletteCommands.openSurface("home", "mouse")} onOpenPractice={() => void tour.openPractice()}/>
|
|
116
|
+
</box>
|
|
117
|
+
</Show>),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "solid-js";
|
|
2
|
+
import { initialGuidedTourState, reduceGuidedTour, sameGuidedTourPractice, } from "./guided-tour.js";
|
|
3
|
+
import { readGuidedTourState, writeGuidedTourState } from "./guided-tour-storage.js";
|
|
4
|
+
/** Compare confirmed snapshots; unrelated sessions and request attempts never count. */
|
|
5
|
+
export function observedGuidedTourEvent(state, previous, current) {
|
|
6
|
+
if (!state.active || !previous)
|
|
7
|
+
return null;
|
|
8
|
+
if (state.step === "commands" && current.paletteOpen && !previous.paletteOpen)
|
|
9
|
+
return { type: "commands-opened" };
|
|
10
|
+
if (state.step === "theme" && current.theme !== previous.theme)
|
|
11
|
+
return { type: "theme-changed" };
|
|
12
|
+
if (state.step === "leave" && current.surface === "home")
|
|
13
|
+
return { type: "home-opened" };
|
|
14
|
+
if (!current.practice ||
|
|
15
|
+
!sameGuidedTourPractice(state.practice, current.practice) ||
|
|
16
|
+
current.surface !== "terminals")
|
|
17
|
+
return null;
|
|
18
|
+
if (state.step === "reopen" &&
|
|
19
|
+
(previous.surface !== "terminals" ||
|
|
20
|
+
!sameGuidedTourPractice(previous.practice, current.practice)))
|
|
21
|
+
return { type: "reopened", practice: current.practice };
|
|
22
|
+
if (!previous.practice || !sameGuidedTourPractice(previous.practice, current.practice))
|
|
23
|
+
return null;
|
|
24
|
+
if (state.step === "split" &&
|
|
25
|
+
previous.panes.length > 0 &&
|
|
26
|
+
current.panes.length > previous.panes.length &&
|
|
27
|
+
previous.panes.every((old) => current.panes.some((p) => p.id === old.id)) &&
|
|
28
|
+
current.panes.some((p) => !previous.panes.some((old) => old.id === p.id)))
|
|
29
|
+
return { type: "split-confirmed", practice: current.practice };
|
|
30
|
+
if (state.step === "focus" &&
|
|
31
|
+
previous.focusedPane &&
|
|
32
|
+
current.focusedPane &&
|
|
33
|
+
previous.focusedPane !== current.focusedPane &&
|
|
34
|
+
current.panes.some((p) => p.id === current.focusedPane) &&
|
|
35
|
+
current.panes.some((p) => p.id === previous.focusedPane))
|
|
36
|
+
return { type: "focus-confirmed", practice: current.practice };
|
|
37
|
+
// A divider reallocates space between the same panes: one grows while
|
|
38
|
+
// another shrinks on the same axis. Whole-app growth/shrink is not practice.
|
|
39
|
+
const samePanes = current.panes.length >= 2 &&
|
|
40
|
+
current.panes.length === previous.panes.length &&
|
|
41
|
+
previous.panes.every((old) => current.panes.some((pane) => pane.id === old.id));
|
|
42
|
+
const reallocated = ["width", "height"].some((axis) => {
|
|
43
|
+
const changes = current.panes.map((pane) => pane[axis] - (previous.panes.find((old) => old.id === pane.id)?.[axis] ?? pane[axis]));
|
|
44
|
+
return changes.some((change) => change > 0) && changes.some((change) => change < 0);
|
|
45
|
+
});
|
|
46
|
+
if (state.step === "resize" && samePanes && reallocated)
|
|
47
|
+
return { type: "resize-confirmed", practice: current.practice };
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
export function createApplicationGuidedTourOwner(options) {
|
|
51
|
+
const [state, setState] = createSignal((options.read ?? readGuidedTourState)());
|
|
52
|
+
const [busy, setBusy] = createSignal(false);
|
|
53
|
+
const [error, setError] = createSignal(null);
|
|
54
|
+
let previous = null;
|
|
55
|
+
let disposed = false;
|
|
56
|
+
onCleanup(() => {
|
|
57
|
+
disposed = true;
|
|
58
|
+
});
|
|
59
|
+
const send = (event) => {
|
|
60
|
+
const next = reduceGuidedTour(state(), event);
|
|
61
|
+
if (next === state())
|
|
62
|
+
return;
|
|
63
|
+
setState(next);
|
|
64
|
+
if (!(options.write ?? writeGuidedTourState)(next))
|
|
65
|
+
setError("Progress could not be saved. You can still continue this tour.");
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
state,
|
|
69
|
+
busy,
|
|
70
|
+
error,
|
|
71
|
+
label: () => state().step === "complete"
|
|
72
|
+
? "Replay walkthrough"
|
|
73
|
+
: state().step === initialGuidedTourState().step
|
|
74
|
+
? "Learn tmux-ide"
|
|
75
|
+
: "Resume walkthrough",
|
|
76
|
+
open() {
|
|
77
|
+
previous = null;
|
|
78
|
+
send({ type: state().step === "complete" ? "replay" : "resume" });
|
|
79
|
+
},
|
|
80
|
+
replay() {
|
|
81
|
+
previous = null;
|
|
82
|
+
send({ type: "replay" });
|
|
83
|
+
},
|
|
84
|
+
pause() {
|
|
85
|
+
send({ type: "pause" });
|
|
86
|
+
},
|
|
87
|
+
welcomeRead() {
|
|
88
|
+
send({ type: "welcome-read" });
|
|
89
|
+
},
|
|
90
|
+
observe(current) {
|
|
91
|
+
const event = observedGuidedTourEvent(state(), previous, current);
|
|
92
|
+
previous = current;
|
|
93
|
+
if (event)
|
|
94
|
+
send(event);
|
|
95
|
+
},
|
|
96
|
+
async createPractice() {
|
|
97
|
+
if (busy() || state().step !== "practice")
|
|
98
|
+
return;
|
|
99
|
+
setBusy(true);
|
|
100
|
+
setError(null);
|
|
101
|
+
try {
|
|
102
|
+
const practice = await options.createPractice();
|
|
103
|
+
if (disposed)
|
|
104
|
+
return;
|
|
105
|
+
send({ type: "practice-created", practice });
|
|
106
|
+
if (state().active)
|
|
107
|
+
await options.openPractice(practice);
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
if (!disposed)
|
|
111
|
+
setError(cause instanceof Error ? cause.message : "Could not create the practice session.");
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (!disposed)
|
|
115
|
+
setBusy(false);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
async openPractice() {
|
|
119
|
+
const practice = state().practice;
|
|
120
|
+
if (!practice || busy())
|
|
121
|
+
return;
|
|
122
|
+
setError(null);
|
|
123
|
+
try {
|
|
124
|
+
if (state().active)
|
|
125
|
+
await options.openPractice(practice);
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
setError(cause instanceof Error ? cause.message : "Practice session is unavailable.");
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -23,7 +23,8 @@ export function HomeAgentRoster(props) {
|
|
|
23
23
|
const [hovered, setHovered] = createSignal(null);
|
|
24
24
|
const width = () => Math.max(0, Math.floor(props.width));
|
|
25
25
|
const height = () => Math.max(0, Math.floor(props.height));
|
|
26
|
-
const stale = (row) =>
|
|
26
|
+
const stale = (row) => row.disabled ||
|
|
27
|
+
(props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
|
|
27
28
|
(props.snapshot.unavailableSessionKeys ?? []).includes(row.sessionKey);
|
|
28
29
|
const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
|
|
29
30
|
props.onRetry) ||
|
|
@@ -50,8 +51,8 @@ export function HomeAgentRoster(props) {
|
|
|
50
51
|
if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
|
|
51
52
|
return "No agents reported";
|
|
52
53
|
const count = props.snapshot.rows.length;
|
|
53
|
-
const attention = props.snapshot.rows.filter((row) => row.attention || row.activity === "waiting" || row.activity === "failed").length;
|
|
54
|
-
const working = props.snapshot.rows.filter((row) => row.activity === "running").length;
|
|
54
|
+
const attention = props.snapshot.rows.filter((row) => !stale(row) && (row.attention || row.activity === "waiting" || row.activity === "failed")).length;
|
|
55
|
+
const working = props.snapshot.rows.filter((row) => !stale(row) && row.activity === "running").length;
|
|
55
56
|
return `${count} observed ${count === 1 ? "agent" : "agents"} · ${attention} ${attention === 1 ? "needs" : "need"} attention · ${working} working`;
|
|
56
57
|
};
|
|
57
58
|
const coverage = () => `Scope: ${props.snapshot.observedSessions} of ${props.snapshot.totalSessions} sessions observed${props.snapshot.phase === "partial" ? " · partial" : ""}${props.snapshot.loadingSessions ? ` · ${props.snapshot.loadingSessions} loading` : ""}${props.snapshot.unavailableSessions ? ` · ${props.snapshot.unavailableSessions} unavailable` : ""}${props.snapshot.truncatedSessions ? ` · ${props.snapshot.truncatedSessions} not loaded` : ""}`;
|
|
@@ -69,15 +70,22 @@ export function HomeAgentRoster(props) {
|
|
|
69
70
|
return "Enlarge the terminal to view agents.";
|
|
70
71
|
const selected = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
|
|
71
72
|
if (selected && stale(selected))
|
|
72
|
-
return `${selected.sessionName} · last observed; waiting for fresh signals`;
|
|
73
|
-
if (width() < 44 && selected)
|
|
74
|
-
return `${selected.
|
|
73
|
+
return `${[selected.machineLabel, selected.serverLabel, selected.sessionName].filter(Boolean).join(" / ")} · last observed; waiting for fresh signals`;
|
|
74
|
+
if (selected?.machineLabel || (width() < 44 && selected))
|
|
75
|
+
return `${[selected?.machineLabel, selected?.serverLabel, selected?.sessionName].filter(Boolean).join(" / ")} · Enter open`;
|
|
75
76
|
return `${offset() + 1}–${Math.min(props.snapshot.rows.length, offset() + visibleCount())} of ${props.snapshot.rows.length} · ↑↓ select · Enter open`;
|
|
76
77
|
};
|
|
77
78
|
useKeyboardRoute((event) => {
|
|
78
79
|
if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta)
|
|
79
80
|
return false;
|
|
80
81
|
const key = event.name.toLowerCase();
|
|
82
|
+
const filter = key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
|
|
83
|
+
if (filter) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
filter();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
81
89
|
const retry = key === "r" &&
|
|
82
90
|
(props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
|
|
83
91
|
props.onRetry;
|
|
@@ -120,10 +128,12 @@ export function HomeAgentRoster(props) {
|
|
|
120
128
|
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
|
|
121
129
|
{clipTerminal(coverage(), width())}
|
|
122
130
|
</text>
|
|
123
|
-
<
|
|
131
|
+
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
|
|
132
|
+
{clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "", width())}
|
|
133
|
+
</text>
|
|
124
134
|
<Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0}/>}>
|
|
125
135
|
<text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
|
|
126
|
-
{clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells("SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
|
|
136
|
+
{clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
|
|
127
137
|
</text>
|
|
128
138
|
<box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
|
|
129
139
|
<For each={keys()}>
|
|
@@ -131,7 +141,9 @@ export function HomeAgentRoster(props) {
|
|
|
131
141
|
const row = () => byKey().get(key);
|
|
132
142
|
return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
|
|
133
143
|
<NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
|
|
134
|
-
padCells(row().
|
|
144
|
+
padCells([row().machineLabel, row().serverLabel, row().sessionName]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={!stale(row()) && row().attention} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
|
|
135
147
|
if (!props.inputActive || row().paneId === null || stale(row()))
|
|
136
148
|
return;
|
|
137
149
|
props.onSelect(key);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.js";
|
|
2
|
-
import { ApplicationShellResourceV2SchemaZ, DaemonEventClientFrameSchemaZ, DaemonEventServerFrameSchemaZ, WorkspaceCatalogResourceV3SchemaZ, } from "@tmux-ide/contracts";
|
|
2
|
+
import { ApplicationShellResourceV2SchemaZ, ApplicationShellProjectionInputV2SchemaZ, TmuxServerScopeSchemaZ, DaemonEventClientFrameSchemaZ, DaemonEventServerFrameSchemaZ, WorkspaceCatalogResourceV3SchemaZ, } from "@tmux-ide/contracts";
|
|
3
3
|
import WebSocket from "ws";
|
|
4
4
|
import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.js";
|
|
5
5
|
export function sameHomeAgentDaemon(expected, actual) {
|
|
@@ -35,6 +35,21 @@ export function createApplicationHomeAgentTransport(options = {}) {
|
|
|
35
35
|
throw new Error(`Agent observation returned HTTP ${response.status}.`);
|
|
36
36
|
return response.json();
|
|
37
37
|
};
|
|
38
|
+
if (session.server) {
|
|
39
|
+
const server = TmuxServerScopeSchemaZ.parse(session.server);
|
|
40
|
+
const base = `/api/v1/tmux-servers/${server.serverId}/${server.generation}`;
|
|
41
|
+
const raw = await get(`${base}/application-shell/${encodeURIComponent(session.workspaceName ?? session.name)}?liveSessionId=${encodeURIComponent(session.liveSessionId ?? "")}`);
|
|
42
|
+
const actual = TmuxServerScopeSchemaZ.parse(raw.server);
|
|
43
|
+
if (raw.version !== 1 ||
|
|
44
|
+
actual.serverId !== server.serverId ||
|
|
45
|
+
actual.generation !== server.generation)
|
|
46
|
+
throw new Error("Agent server incarnation changed.");
|
|
47
|
+
return {
|
|
48
|
+
version: 2,
|
|
49
|
+
daemon,
|
|
50
|
+
resource: ApplicationShellProjectionInputV2SchemaZ.parse(raw.resource),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
38
53
|
const shell = ApplicationShellResourceV2SchemaZ.parse(await get(`/api/project/${encodeURIComponent(session.name)}/application-shell?version=2`));
|
|
39
54
|
if (!sameHomeAgentDaemon(daemon, shell.daemon))
|
|
40
55
|
throw new Error("Agent daemon changed.");
|
|
@@ -48,6 +63,53 @@ export function createApplicationHomeAgentTransport(options = {}) {
|
|
|
48
63
|
return shell;
|
|
49
64
|
},
|
|
50
65
|
connect(daemon, sessions, handlers) {
|
|
66
|
+
const scoped = sessions.filter((session) => session.server);
|
|
67
|
+
if (scoped.length > 0) {
|
|
68
|
+
// Scoped session events currently carry topology, not agent activity.
|
|
69
|
+
// Refresh metadata with a bounded cadence; never attach terminal streams.
|
|
70
|
+
// Keep legacy observations independent from scoped owner availability.
|
|
71
|
+
const pushed = sessions.filter((session) => !session.server || session.server.generation === daemon.instanceId);
|
|
72
|
+
let closed = false;
|
|
73
|
+
let pushHealthy = false;
|
|
74
|
+
const interval = setInterval(() => {
|
|
75
|
+
if (!closed)
|
|
76
|
+
for (const session of scoped) {
|
|
77
|
+
if (!pushHealthy || session.server?.generation !== daemon.instanceId)
|
|
78
|
+
handlers.invalidate(session.id);
|
|
79
|
+
}
|
|
80
|
+
}, 5_000);
|
|
81
|
+
interval.unref?.();
|
|
82
|
+
const connection = pushed.length
|
|
83
|
+
? createApplicationHomeAgentTransport(options).connect(daemon, pushed.map((session) => ({ ...session, server: undefined })), {
|
|
84
|
+
...handlers,
|
|
85
|
+
// The root event stream is advisory for scoped reads. Failure must
|
|
86
|
+
// not stall independent scoped owners or suppress their rows.
|
|
87
|
+
ready: (unavailable) => {
|
|
88
|
+
pushHealthy = unavailable.length === 0;
|
|
89
|
+
if (!closed)
|
|
90
|
+
for (const session of pushed)
|
|
91
|
+
handlers.invalidate(session.id);
|
|
92
|
+
},
|
|
93
|
+
unavailable: () => {
|
|
94
|
+
pushHealthy = false;
|
|
95
|
+
if (!closed)
|
|
96
|
+
for (const session of pushed)
|
|
97
|
+
handlers.invalidate(session.id);
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
: null;
|
|
101
|
+
queueMicrotask(() => {
|
|
102
|
+
if (!closed)
|
|
103
|
+
handlers.ready([]);
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
close() {
|
|
107
|
+
closed = true;
|
|
108
|
+
clearInterval(interval);
|
|
109
|
+
connection?.close();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
51
113
|
let closed = false;
|
|
52
114
|
let failed = false;
|
|
53
115
|
const sockets = [];
|
|
@@ -159,7 +159,7 @@ export function createApplicationHomeNavigationOwner(options) {
|
|
|
159
159
|
const paneRename = createApplicationPaneRenameOwner(options.interaction.renamePane, options.setNote);
|
|
160
160
|
const homeAgents = createApplicationHomeAgentsOwner({
|
|
161
161
|
catalog: options.catalog.snapshot,
|
|
162
|
-
active: () => options.activeSurface() === "home",
|
|
162
|
+
active: () => !options.fleetHome && options.activeSurface() === "home",
|
|
163
163
|
inputActive: () => options.activeSurface() === "home" &&
|
|
164
164
|
!paneRename.draft() &&
|
|
165
165
|
!options.appearanceOpen?.() &&
|
|
@@ -225,5 +225,11 @@ export function createApplicationHomeNavigationOwner(options) {
|
|
|
225
225
|
onNavigationIntent: homeAgents.cancel,
|
|
226
226
|
});
|
|
227
227
|
const paletteCommandList = paletteCommands.commands;
|
|
228
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
homeAgents: options.fleetHome ? { ...homeAgents, presentation: options.fleetHome } : homeAgents,
|
|
230
|
+
paneRename,
|
|
231
|
+
paletteCommands,
|
|
232
|
+
paletteCommandList,
|
|
233
|
+
openAgent,
|
|
234
|
+
};
|
|
229
235
|
}
|
|
@@ -14,11 +14,13 @@ export function projectHomeAgentRows(session, shell) {
|
|
|
14
14
|
return shell.resource.workspace.sidebar.agents.map((agent) => ({
|
|
15
15
|
key: `${session.id}\u0000${agent.id}`,
|
|
16
16
|
sessionKey: session.id,
|
|
17
|
+
server: session.server,
|
|
18
|
+
serverLabel: session.serverLabel,
|
|
17
19
|
sessionName: session.name,
|
|
18
20
|
// Older catalog fixtures/callers retain a generation-qualified incarnation
|
|
19
21
|
// key even when they do not expose the separately named wire field.
|
|
20
22
|
liveSessionId: session.liveSessionId ?? session.id,
|
|
21
|
-
daemonInstanceId: shell.daemon.instanceId,
|
|
23
|
+
daemonInstanceId: session.server?.generation ?? shell.daemon.instanceId,
|
|
22
24
|
agentId: agent.id,
|
|
23
25
|
paneId: agent.paneId,
|
|
24
26
|
windowId: shell.resource.terminalInventory?.resources.find((resource) => resource.attachability.status === "available" &&
|
|
@@ -56,8 +56,12 @@ export function createApplicationHomeCatalogOwner(options) {
|
|
|
56
56
|
const currentSessions = next.sessions;
|
|
57
57
|
if (automaticOpen && next.phase === "live") {
|
|
58
58
|
automaticOpen = false;
|
|
59
|
-
if (currentSessions.length === 1)
|
|
60
|
-
|
|
59
|
+
if (currentSessions.length === 1) {
|
|
60
|
+
const session = currentSessions[0];
|
|
61
|
+
void (options.openCatalogSession
|
|
62
|
+
? options.openCatalogSession(session)
|
|
63
|
+
: options.startGeneration(session.name));
|
|
64
|
+
}
|
|
61
65
|
}
|
|
62
66
|
if (currentSessions.length === 0) {
|
|
63
67
|
if (current !== null)
|
|
@@ -93,9 +97,11 @@ export function createApplicationHomeCatalogOwner(options) {
|
|
|
93
97
|
return true;
|
|
94
98
|
}
|
|
95
99
|
if (name === "return" || name === "enter") {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
void options.
|
|
100
|
+
const session = sessions()[selectedSessionIndex()];
|
|
101
|
+
if (session)
|
|
102
|
+
void (options.openCatalogSession
|
|
103
|
+
? options.openCatalogSession(session)
|
|
104
|
+
: options.startGeneration(session.name));
|
|
99
105
|
return true;
|
|
100
106
|
}
|
|
101
107
|
return false;
|