tmux-ide 2.9.0-beta.21 → 2.9.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +52253 -49070
- package/package.json +1 -1
- 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/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 +345 -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 +16 -10
- 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-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-machine-catalog.js +12 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +190 -31
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +91 -37
- 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-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/fleet-lifecycle-client.js +42 -1
- package/packages/daemon/dist/tui/mirror/runtime/open-tui-generation-host.js +1 -1
- package/packages/daemon/dist/tui/mirror/runtime/select-terminal-pane.js +2 -1
- 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 +468 -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 +16 -8
- 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-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-machine-catalog.ts +19 -7
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +263 -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-root-configuration.ts +12 -3
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +24 -24
- package/packages/daemon/src/tui/mirror/runtime/application-route-connection.ts +93 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +4 -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/fleet-lifecycle-client.ts +52 -0
- package/packages/daemon/src/tui/mirror/runtime/open-tui-generation-host.ts +1 -1
- package/packages/daemon/src/tui/mirror/runtime/select-terminal-pane.ts +3 -0
- 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
|
},
|
|
@@ -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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { createWorkspaceEventSupervisor, } from "@tmux-ide/daemon-client/workspace-event-supervisor";
|
|
1
3
|
import { readApplicationDaemonInfo as readCanonicalDaemonInfo } from "./application-daemon-authority.js";
|
|
2
|
-
import {
|
|
4
|
+
import { TmuxServersResourceSchemaZ, TmuxServerSessionsResourceSchemaZ, } from "@tmux-ide/contracts";
|
|
3
5
|
import { createPushResourceSession, } from "@tmux-ide/daemon-client/push-resource-session";
|
|
4
|
-
import { createWorkspaceEventSupervisor, } from "@tmux-ide/daemon-client/workspace-event-supervisor";
|
|
5
|
-
import WebSocket from "ws";
|
|
6
6
|
import { canonicalDaemonUrl } from "../../../lib/canonical-daemon.js";
|
|
7
7
|
function sameDaemon(left, right) {
|
|
8
8
|
return (left?.protocolVersion === right.protocolVersion &&
|
|
@@ -22,7 +22,7 @@ export function closeApplicationHomeCatalogTransport(subscription, supervisor) {
|
|
|
22
22
|
subscription.close();
|
|
23
23
|
supervisor.dispose();
|
|
24
24
|
}
|
|
25
|
-
function createApplicationHomeCatalogAdapter(
|
|
25
|
+
function createApplicationHomeCatalogAdapter(_onTransportRetired) {
|
|
26
26
|
return {
|
|
27
27
|
validateTarget(value) {
|
|
28
28
|
if (!value ||
|
|
@@ -42,10 +42,9 @@ function createApplicationHomeCatalogAdapter(onTransportRetired) {
|
|
|
42
42
|
};
|
|
43
43
|
},
|
|
44
44
|
async fetch(target, _key, signal) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
response = await fetch(url, {
|
|
45
|
+
const base = canonicalDaemonUrl("http", target.daemon.bindHostname, target.daemon.port);
|
|
46
|
+
const read = async (path) => {
|
|
47
|
+
const response = await fetch(base + path, {
|
|
49
48
|
headers: {
|
|
50
49
|
accept: "application/json",
|
|
51
50
|
...(target.daemon.authToken
|
|
@@ -55,75 +54,129 @@ function createApplicationHomeCatalogAdapter(onTransportRetired) {
|
|
|
55
54
|
credentials: "omit",
|
|
56
55
|
cache: "no-store",
|
|
57
56
|
redirect: "error",
|
|
58
|
-
signal,
|
|
57
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3_000)]),
|
|
59
58
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
if (!response.ok)
|
|
60
|
+
throw new Error(`Discovery returned HTTP ${response.status}`);
|
|
61
|
+
return response.json();
|
|
62
|
+
};
|
|
63
|
+
try {
|
|
64
|
+
const inventory = TmuxServersResourceSchemaZ.parse(await read("/api/v1/tmux-servers"));
|
|
65
|
+
const sessions = await Promise.allSettled(inventory.servers.map(async (descriptor) => {
|
|
66
|
+
if (descriptor.state !== "online")
|
|
67
|
+
return [];
|
|
68
|
+
const server = { serverId: descriptor.serverId, generation: descriptor.generation };
|
|
69
|
+
const result = TmuxServerSessionsResourceSchemaZ.parse(await read(`/api/v1/tmux-servers/${server.serverId}/${server.generation}/sessions`));
|
|
70
|
+
if (result.server.serverId !== server.serverId ||
|
|
71
|
+
result.server.generation !== server.generation)
|
|
72
|
+
throw new Error("Server discovery identity changed");
|
|
73
|
+
return result.sessions.map((session) => ({
|
|
74
|
+
id: JSON.stringify([server.serverId, server.generation, session.liveSessionId]),
|
|
75
|
+
server,
|
|
76
|
+
serverLabel: descriptor.label,
|
|
77
|
+
liveSessionId: session.liveSessionId,
|
|
78
|
+
name: session.sessionName,
|
|
79
|
+
...(session.workspaceName ? { workspaceName: session.workspaceName } : {}),
|
|
80
|
+
paneCount: session.paneCount,
|
|
81
|
+
}));
|
|
82
|
+
}));
|
|
68
83
|
return {
|
|
69
|
-
status: "
|
|
70
|
-
|
|
84
|
+
status: "ok",
|
|
85
|
+
resource: {
|
|
86
|
+
kind: "live-catalog",
|
|
87
|
+
daemonInstanceId: target.daemon.instanceId,
|
|
88
|
+
servers: inventory.servers.map((server, index) => sessions[index]?.status === "rejected"
|
|
89
|
+
? {
|
|
90
|
+
serverId: server.serverId,
|
|
91
|
+
label: server.label,
|
|
92
|
+
state: "offline",
|
|
93
|
+
generation: null,
|
|
94
|
+
}
|
|
95
|
+
: server),
|
|
96
|
+
scopedSessions: sessions.flatMap((result) => result.status === "fulfilled" ? result.value : []),
|
|
97
|
+
},
|
|
71
98
|
};
|
|
72
|
-
let body;
|
|
73
|
-
try {
|
|
74
|
-
body = await response.json();
|
|
75
99
|
}
|
|
76
100
|
catch {
|
|
77
101
|
return {
|
|
78
102
|
status: "failed",
|
|
79
|
-
failure: catalogFailure("
|
|
103
|
+
failure: catalogFailure("network", "Server session discovery failed.", true),
|
|
80
104
|
};
|
|
81
105
|
}
|
|
82
|
-
const parsed = WorkspaceCatalogResourceV3SchemaZ.safeParse(body);
|
|
83
|
-
if (!parsed.success || !sameDaemon(target.daemon, parsed.data.daemon))
|
|
84
|
-
return {
|
|
85
|
-
status: "failed",
|
|
86
|
-
failure: catalogFailure("schema", "Live session discovery failed daemon identity validation.", false),
|
|
87
|
-
};
|
|
88
|
-
return { status: "ok", resource: { kind: "live-catalog", value: parsed.data } };
|
|
89
106
|
},
|
|
90
107
|
async connect(target, _interests, handlers, signal) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
headers: target.daemon.authToken
|
|
94
|
-
? { Authorization: `Bearer ${target.daemon.authToken}` }
|
|
95
|
-
: undefined,
|
|
96
|
-
});
|
|
97
|
-
const supervisor = createWorkspaceEventSupervisor({
|
|
98
|
-
socket,
|
|
99
|
-
daemon: target.daemon,
|
|
100
|
-
workspaceName: target.workspaceName,
|
|
101
|
-
sessionName: target.workspaceName,
|
|
102
|
-
fetchTerminalRuntimeInventory: () => Promise.reject(new Error("Home catalog owns no terminal inventory.")),
|
|
103
|
-
onRetired: onTransportRetired,
|
|
104
|
-
});
|
|
105
|
-
const subscription = supervisor.connectWorkspaceCatalog(() => handlers.invalidate(["live-catalog"]), { terminalFirst: false });
|
|
108
|
+
// Root push is an advisory invalidation only: every refresh still resolves
|
|
109
|
+
// the full scoped inventory. The timer discovers changes on other owners.
|
|
106
110
|
let closed = false;
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
let pending = false;
|
|
112
|
+
let closePush = null;
|
|
113
|
+
const invalidate = () => {
|
|
114
|
+
if (!closed)
|
|
115
|
+
handlers.invalidate(["live-catalog"]);
|
|
116
|
+
};
|
|
117
|
+
const startPush = async () => {
|
|
118
|
+
if (closed || pending || closePush)
|
|
109
119
|
return;
|
|
120
|
+
pending = true;
|
|
121
|
+
const socket = new WebSocket(canonicalDaemonUrl("ws", target.daemon.bindHostname, target.daemon.port, "/ws/events?mode=semantic"), {
|
|
122
|
+
headers: target.daemon.authToken
|
|
123
|
+
? { Authorization: `Bearer ${target.daemon.authToken}` }
|
|
124
|
+
: undefined,
|
|
125
|
+
});
|
|
126
|
+
const supervisor = createWorkspaceEventSupervisor({
|
|
127
|
+
socket,
|
|
128
|
+
daemon: target.daemon,
|
|
129
|
+
workspaceName: target.workspaceName,
|
|
130
|
+
sessionName: target.workspaceName,
|
|
131
|
+
fetchTerminalRuntimeInventory: () => Promise.reject(new Error("Home catalog owns no terminal inventory.")),
|
|
132
|
+
onRetired: () => {
|
|
133
|
+
const cleanup = closePush;
|
|
134
|
+
closePush = null;
|
|
135
|
+
cleanup?.();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const subscription = supervisor.connectWorkspaceCatalog(invalidate, {
|
|
139
|
+
terminalFirst: false,
|
|
140
|
+
});
|
|
141
|
+
const cleanup = () => closeApplicationHomeCatalogTransport(subscription, supervisor);
|
|
142
|
+
closePush = cleanup;
|
|
143
|
+
try {
|
|
144
|
+
await subscription.ready;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
if (closePush === cleanup)
|
|
148
|
+
closePush = null;
|
|
149
|
+
cleanup();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
pending = false;
|
|
153
|
+
if (closed) {
|
|
154
|
+
cleanup();
|
|
155
|
+
closePush = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const timer = setInterval(() => {
|
|
160
|
+
invalidate();
|
|
161
|
+
void startPush().catch(() => {
|
|
162
|
+
pending = false;
|
|
163
|
+
});
|
|
164
|
+
}, 5_000);
|
|
165
|
+
const close = () => {
|
|
110
166
|
closed = true;
|
|
167
|
+
clearInterval(timer);
|
|
111
168
|
signal.removeEventListener("abort", close);
|
|
112
|
-
|
|
113
|
-
|
|
169
|
+
const cleanup = closePush;
|
|
170
|
+
closePush = null;
|
|
171
|
+
cleanup?.();
|
|
114
172
|
};
|
|
115
173
|
signal.addEventListener("abort", close, { once: true });
|
|
116
|
-
|
|
117
|
-
await subscription.ready;
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
174
|
+
if (signal.aborted)
|
|
120
175
|
close();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw signal.reason;
|
|
126
|
-
}
|
|
176
|
+
else
|
|
177
|
+
void startPush().catch(() => {
|
|
178
|
+
pending = false;
|
|
179
|
+
});
|
|
127
180
|
return { status: "connected", close };
|
|
128
181
|
},
|
|
129
182
|
rejectionFailure: () => catalogFailure("unavailable", "Live session discovery was rejected.", true),
|
|
@@ -145,29 +198,31 @@ const initialSnapshot = () => ({
|
|
|
145
198
|
sessions: [],
|
|
146
199
|
note: "Discovering live tmux sessions…",
|
|
147
200
|
});
|
|
148
|
-
function projectCatalog(resource) {
|
|
201
|
+
function projectCatalog(resource, scopedSessions) {
|
|
149
202
|
return {
|
|
150
203
|
phase: "live",
|
|
151
204
|
daemonInstanceId: resource.daemon.instanceId,
|
|
152
|
-
sessions:
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
sessions: scopedSessions ??
|
|
206
|
+
resource.liveSessions
|
|
207
|
+
.map(({ liveSessionId, sessionName, paneCount }) => ({
|
|
208
|
+
id: `${resource.daemon.instanceId}:${liveSessionId}`,
|
|
209
|
+
liveSessionId,
|
|
210
|
+
workspaceName: resource.intents.find((intent) => intent.sessionName === sessionName)
|
|
211
|
+
?.workspaceName,
|
|
212
|
+
name: sessionName,
|
|
213
|
+
paneCount,
|
|
214
|
+
}))
|
|
215
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
216
|
+
note: (scopedSessions?.length ?? resource.liveSessions.length) === 0
|
|
163
217
|
? "No live tmux sessions yet. This list updates automatically."
|
|
164
218
|
: null,
|
|
165
219
|
};
|
|
166
220
|
}
|
|
167
221
|
/**
|
|
168
|
-
* Daemon-authoritative
|
|
169
|
-
*
|
|
170
|
-
*
|
|
222
|
+
* Daemon-authoritative fleet metadata. Root push preserves immediate updates,
|
|
223
|
+
* while a five-second refresh discovers independent
|
|
224
|
+
* server owners without opening pane streams. Failed reads use bounded backoff;
|
|
225
|
+
* each failed server is excluded without suppressing healthy siblings.
|
|
171
226
|
*/
|
|
172
227
|
export function createApplicationHomeCatalog(overrides = {}) {
|
|
173
228
|
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
@@ -253,7 +308,17 @@ export function createApplicationHomeCatalog(overrides = {}) {
|
|
|
253
308
|
if (slot?.status === "loaded" && !slot.refreshing && slot.resource.kind === "live-catalog") {
|
|
254
309
|
clearRetry();
|
|
255
310
|
retryAttempt = 0;
|
|
256
|
-
publish(
|
|
311
|
+
publish(slot.resource.value
|
|
312
|
+
? projectCatalog(slot.resource.value)
|
|
313
|
+
: {
|
|
314
|
+
phase: "live",
|
|
315
|
+
daemonInstanceId: slot.resource.daemonInstanceId ?? null,
|
|
316
|
+
sessions: slot.resource.scopedSessions ?? [],
|
|
317
|
+
servers: slot.resource.servers,
|
|
318
|
+
note: slot.resource.scopedSessions?.length
|
|
319
|
+
? null
|
|
320
|
+
: "No live tmux sessions yet. This list updates automatically.",
|
|
321
|
+
});
|
|
257
322
|
return;
|
|
258
323
|
}
|
|
259
324
|
if (slot?.status === "error") {
|
|
@@ -53,6 +53,7 @@ export function createApplicationMachineCatalog(options = {}) {
|
|
|
53
53
|
note: diagnostic && diagnostic.phase !== "ready"
|
|
54
54
|
? fleetConnectionMessage(diagnostic)
|
|
55
55
|
: (entry?.snapshot.note ?? null),
|
|
56
|
+
servers: entry?.snapshot.servers,
|
|
56
57
|
sessions: (entry?.lastSessions ?? []).map((session) => ({
|
|
57
58
|
...session,
|
|
58
59
|
sourceId: session.id,
|
|
@@ -86,7 +87,13 @@ export function createApplicationMachineCatalog(options = {}) {
|
|
|
86
87
|
sessions: group.sessions.map((session) => ({
|
|
87
88
|
...session,
|
|
88
89
|
id: session.liveSessionId && joined.environmentId && !joined.conflict
|
|
89
|
-
? JSON.stringify([
|
|
90
|
+
? JSON.stringify([
|
|
91
|
+
joined.environmentId,
|
|
92
|
+
"session",
|
|
93
|
+
session.server?.serverId,
|
|
94
|
+
session.server?.generation,
|
|
95
|
+
session.liveSessionId,
|
|
96
|
+
])
|
|
90
97
|
: session.id,
|
|
91
98
|
})),
|
|
92
99
|
};
|
|
@@ -152,11 +159,14 @@ export function createApplicationMachineCatalog(options = {}) {
|
|
|
152
159
|
environmentId: entry.environmentId,
|
|
153
160
|
generation: entry.generation,
|
|
154
161
|
seenAt: entry.seenAt,
|
|
155
|
-
sessions: value.sessions
|
|
162
|
+
sessions: value.sessions
|
|
163
|
+
.slice(0, 64)
|
|
164
|
+
.map(({ id, liveSessionId, name, paneCount, server, serverLabel }) => ({
|
|
156
165
|
id,
|
|
157
166
|
...(liveSessionId ? { liveSessionId } : {}),
|
|
158
167
|
name,
|
|
159
168
|
paneCount,
|
|
169
|
+
...(server ? { server, serverLabel } : {}),
|
|
160
170
|
})),
|
|
161
171
|
});
|
|
162
172
|
}
|