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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { WINDOW_LINK_MAX_LINKS, WindowLinkTargetSchemaZ, WindowLinkTopologySchemaZ, WorkspaceCatalogLiveSessionIdSchemaZ, TerminalAttachmentSemanticWindowIdSchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
export class WindowLinkResolutionError extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
constructor(reason) {
|
|
6
|
+
super(reason);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.name = "WindowLinkResolutionError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* One authority per daemon/server/session incarnation. This is an observation
|
|
13
|
+
* model, not an immutable native winlink identity: unobserved unlink/relink ABA
|
|
14
|
+
* is unknowable. Resolved targets still require an execution-time native guard.
|
|
15
|
+
* Not installed into the v1 stream; C2 activates it with the complete link-aware
|
|
16
|
+
* producer/consumer boundary.
|
|
17
|
+
*/
|
|
18
|
+
export class WindowLinkAuthority {
|
|
19
|
+
liveSessionId;
|
|
20
|
+
runtimeSessionId;
|
|
21
|
+
records = new Map();
|
|
22
|
+
revision = 0;
|
|
23
|
+
topology = null;
|
|
24
|
+
disposed = false;
|
|
25
|
+
constructor(liveSessionId, runtimeSessionId) {
|
|
26
|
+
this.liveSessionId = liveSessionId;
|
|
27
|
+
this.runtimeSessionId = runtimeSessionId;
|
|
28
|
+
WorkspaceCatalogLiveSessionIdSchemaZ.parse(liveSessionId);
|
|
29
|
+
if (!/^\$(?:0|[1-9]\d{0,15})$/u.test(runtimeSessionId) ||
|
|
30
|
+
!Number.isSafeInteger(Number(runtimeSessionId.slice(1))))
|
|
31
|
+
throw new TypeError("Invalid native session id");
|
|
32
|
+
}
|
|
33
|
+
/** A malformed fresh observation revokes old actions; no partial topology is published. */
|
|
34
|
+
reconcile(observations) {
|
|
35
|
+
try {
|
|
36
|
+
return this.stageAndPublish(observations);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.invalidate();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
stageAndPublish(observations) {
|
|
44
|
+
if (this.disposed)
|
|
45
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
46
|
+
if (observations.length === 0 || observations.length > WINDOW_LINK_MAX_LINKS)
|
|
47
|
+
throw new TypeError("Invalid window link count");
|
|
48
|
+
const indexes = new Set();
|
|
49
|
+
const semanticByRuntime = new Map();
|
|
50
|
+
const runtimeBySemantic = new Map();
|
|
51
|
+
let activeCount = 0;
|
|
52
|
+
for (const row of observations) {
|
|
53
|
+
if (!Number.isSafeInteger(row.index) ||
|
|
54
|
+
row.index < 0 ||
|
|
55
|
+
indexes.has(row.index) ||
|
|
56
|
+
!/^@(?:0|[1-9]\d{0,15})$/u.test(row.runtimeWindowId) ||
|
|
57
|
+
!Number.isSafeInteger(Number(row.runtimeWindowId.slice(1))) ||
|
|
58
|
+
typeof row.active !== "boolean")
|
|
59
|
+
throw new TypeError("Malformed native window link observation");
|
|
60
|
+
TerminalAttachmentSemanticWindowIdSchemaZ.parse(row.semanticWindowId);
|
|
61
|
+
indexes.add(row.index);
|
|
62
|
+
if (row.active)
|
|
63
|
+
activeCount++;
|
|
64
|
+
const semantic = semanticByRuntime.get(row.runtimeWindowId);
|
|
65
|
+
const runtime = runtimeBySemantic.get(row.semanticWindowId);
|
|
66
|
+
if ((semantic !== undefined && semantic !== row.semanticWindowId) ||
|
|
67
|
+
(runtime !== undefined && runtime !== row.runtimeWindowId))
|
|
68
|
+
throw new TypeError("Window backing identity is inconsistent");
|
|
69
|
+
semanticByRuntime.set(row.runtimeWindowId, row.semanticWindowId);
|
|
70
|
+
runtimeBySemantic.set(row.semanticWindowId, row.runtimeWindowId);
|
|
71
|
+
}
|
|
72
|
+
if (activeCount !== 1)
|
|
73
|
+
throw new TypeError("Expected one active window link");
|
|
74
|
+
const rows = [...observations].sort((left, right) => left.index - right.index);
|
|
75
|
+
const changed = rows.length !== this.records.size ||
|
|
76
|
+
rows.some((row) => {
|
|
77
|
+
const prior = this.records.get(row.index);
|
|
78
|
+
return (prior?.runtimeWindowId !== row.runtimeWindowId ||
|
|
79
|
+
prior?.semanticWindowId !== row.semanticWindowId);
|
|
80
|
+
});
|
|
81
|
+
const revision = this.revision + (changed ? 1 : 0);
|
|
82
|
+
if (!Number.isSafeInteger(revision))
|
|
83
|
+
throw new RangeError("Window link revision exhausted");
|
|
84
|
+
const next = new Map();
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const prior = this.records.get(row.index);
|
|
87
|
+
const retained = prior?.runtimeWindowId === row.runtimeWindowId &&
|
|
88
|
+
prior.semanticWindowId === row.semanticWindowId;
|
|
89
|
+
next.set(row.index, {
|
|
90
|
+
...row,
|
|
91
|
+
linkId: retained ? prior.linkId : `window-link.${randomUUID().replaceAll("-", "")}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const records = [...next.values()];
|
|
95
|
+
const topology = WindowLinkTopologySchemaZ.parse({
|
|
96
|
+
liveSessionId: this.liveSessionId,
|
|
97
|
+
linkRevision: revision,
|
|
98
|
+
activeLinkId: records.find((row) => row.active).linkId,
|
|
99
|
+
links: records.map((row) => ({
|
|
100
|
+
linkId: row.linkId,
|
|
101
|
+
semanticWindowId: row.semanticWindowId,
|
|
102
|
+
displayIndex: row.index,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
this.records = next;
|
|
106
|
+
this.revision = revision;
|
|
107
|
+
this.topology = topology;
|
|
108
|
+
return this.snapshot();
|
|
109
|
+
}
|
|
110
|
+
snapshot() {
|
|
111
|
+
return this.topology
|
|
112
|
+
? { ...this.topology, links: this.topology.links.map((link) => ({ ...link })) }
|
|
113
|
+
: null;
|
|
114
|
+
}
|
|
115
|
+
/** Reconnect/gap/discontinuous observation must retire all previously issued handles. */
|
|
116
|
+
invalidate() {
|
|
117
|
+
this.records.clear();
|
|
118
|
+
this.topology = null;
|
|
119
|
+
this.revision++;
|
|
120
|
+
if (!Number.isSafeInteger(this.revision))
|
|
121
|
+
this.disposed = true;
|
|
122
|
+
}
|
|
123
|
+
dispose() {
|
|
124
|
+
this.invalidate();
|
|
125
|
+
this.disposed = true;
|
|
126
|
+
}
|
|
127
|
+
resolve(target) {
|
|
128
|
+
WindowLinkTargetSchemaZ.parse(target);
|
|
129
|
+
if (target.liveSessionId !== this.liveSessionId)
|
|
130
|
+
throw new WindowLinkResolutionError("window_link_session_mismatch");
|
|
131
|
+
if (!this.topology || this.disposed || target.linkRevision !== this.revision)
|
|
132
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
133
|
+
const record = [...this.records.values()].find((row) => row.linkId === target.linkId);
|
|
134
|
+
if (!record)
|
|
135
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
136
|
+
if (record.semanticWindowId !== target.expectedSemanticWindowId)
|
|
137
|
+
throw new WindowLinkResolutionError("window_link_backing_mismatch");
|
|
138
|
+
return {
|
|
139
|
+
runtimeSessionId: this.runtimeSessionId,
|
|
140
|
+
runtimeWindowId: record.runtimeWindowId,
|
|
141
|
+
index: record.index,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Legacy backing-only selection is safe only when exactly one link applies. */
|
|
145
|
+
uniqueTargetForBacking(semanticWindowId) {
|
|
146
|
+
if (!this.topology || this.disposed)
|
|
147
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
148
|
+
const links = [...this.records.values()].filter((row) => row.semanticWindowId === semanticWindowId);
|
|
149
|
+
if (links.length === 0)
|
|
150
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
151
|
+
if (links.length !== 1)
|
|
152
|
+
throw new WindowLinkResolutionError("window_link_ambiguous");
|
|
153
|
+
return {
|
|
154
|
+
liveSessionId: this.liveSessionId,
|
|
155
|
+
linkId: links[0].linkId,
|
|
156
|
+
expectedSemanticWindowId: semanticWindowId,
|
|
157
|
+
linkRevision: this.revision,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -5,7 +5,7 @@ import { PaneStreamLeaseRequestSchemaZ, } from "@tmux-ide/contracts";
|
|
|
5
5
|
* PaneStreamLeaseManager — in-memory authority for pane-stream leases (m43
|
|
6
6
|
* card 2), following the terminal-attachment lease discipline:
|
|
7
7
|
*
|
|
8
|
-
* - a one-time `
|
|
8
|
+
* - a one-time `ps2_` bearer ticket whose SHA-256 digest is all the manager
|
|
9
9
|
* retains, burned before activation so no concurrent caller can replay it;
|
|
10
10
|
* - the ticket TTL bounds credential DELIVERY (the authenticated redemption
|
|
11
11
|
* frame must arrive in time); execution after delivery gets its own bounded
|
|
@@ -30,7 +30,7 @@ const SessionNameSchemaZ = z
|
|
|
30
30
|
.min(1)
|
|
31
31
|
.max(256)
|
|
32
32
|
.refine((value) => !/[\0\r\n]/u.test(value));
|
|
33
|
-
const TicketPattern = /^
|
|
33
|
+
const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
|
|
34
34
|
export class PaneStreamLeaseError extends Error {
|
|
35
35
|
code;
|
|
36
36
|
constructor(code, message) {
|
|
@@ -95,7 +95,7 @@ export class PaneStreamLeaseManager {
|
|
|
95
95
|
if (ticketBytes.byteLength !== 32) {
|
|
96
96
|
throw new PaneStreamLeaseError("identity-generation-failed", "The secure random source returned an invalid ticket.");
|
|
97
97
|
}
|
|
98
|
-
const redemptionTicket = `
|
|
98
|
+
const redemptionTicket = `ps2_${Buffer.from(ticketBytes).toString("base64url")}`;
|
|
99
99
|
const state = {
|
|
100
100
|
leaseId,
|
|
101
101
|
requestId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
3
4
|
import { canonicalOriginOrNull, digestSecret, digestsEqual, rawDataByteLength, rawDataToBuffer, safeCloseSocket, strictJsonParse, } from "../attachments/admission-util.js";
|
|
@@ -6,7 +7,7 @@ import { terminalDeliveryObservationOrdinal } from "../session-runtime/terminal-
|
|
|
6
7
|
import { PaneStreamLeaseError, } from "./lease-manager.js";
|
|
7
8
|
import { DEFAULT_PANE_STREAM_FLOW_BUDGETS, PaneStreamWireLedger, } from "./wire-ledger.js";
|
|
8
9
|
/**
|
|
9
|
-
* The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `
|
|
10
|
+
* The pane-stream WebSocket endpoint (m43 card 2): redeems a one-time `ps2_`
|
|
10
11
|
* ticket and bridges MirrorService subscriptions to wire frames. Admission is
|
|
11
12
|
* the direct-websocket discipline verbatim — Origin-gated upgrade reservation,
|
|
12
13
|
* digest-only pending tickets, one text redemption frame, delivery-bound TTL —
|
|
@@ -53,7 +54,7 @@ function hasCanonicalInputFramePrefix(raw) {
|
|
|
53
54
|
startsWithBuffer(raw, CANONICAL_TEXT_INPUT_FRAME_PREFIX) ||
|
|
54
55
|
startsWithBuffer(raw, TYPE_FIRST_INPUT_FRAME_PREFIX));
|
|
55
56
|
}
|
|
56
|
-
const TicketPattern = /^
|
|
57
|
+
const TicketPattern = /^ps2_[A-Za-z0-9_-]{43}$/u;
|
|
57
58
|
const BindingIdSchemaZ = z
|
|
58
59
|
.string()
|
|
59
60
|
.min(1)
|
|
@@ -270,7 +271,7 @@ export class PaneStreamAdmissionCoordinator {
|
|
|
270
271
|
if (this.#shuttingDown) {
|
|
271
272
|
return { accepted: false, code: "daemon-shutting-down", httpStatus: 503 };
|
|
272
273
|
}
|
|
273
|
-
if (input.path !==
|
|
274
|
+
if (input.path !== new URL(this.#webSocketUrl).pathname) {
|
|
274
275
|
return { accepted: false, code: "invalid-path", httpStatus: 404 };
|
|
275
276
|
}
|
|
276
277
|
if (input.protocols.length !== 1 || input.protocols[0] !== PANE_STREAM_WEBSOCKET_SUBPROTOCOL) {
|
|
@@ -616,6 +617,7 @@ export class PaneStreamLiveConnection {
|
|
|
616
617
|
#semanticLayouts = new Map();
|
|
617
618
|
#semanticExpectedPaneIds = null;
|
|
618
619
|
#semanticRuntimeSessionId = null;
|
|
620
|
+
#semanticWindowLinks = null;
|
|
619
621
|
#semanticTopologyEpoch = -1;
|
|
620
622
|
#sendQueue = [];
|
|
621
623
|
#semanticDrainWaiters = new Map();
|
|
@@ -920,7 +922,13 @@ export class PaneStreamLiveConnection {
|
|
|
920
922
|
authority.topologyEpoch < 0
|
|
921
923
|
? null
|
|
922
924
|
: this.#validateInitialLayout(authority.layouts, expectedPaneIds);
|
|
923
|
-
if (!stagedInitialFrames
|
|
925
|
+
if (!stagedInitialFrames ||
|
|
926
|
+
!PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
|
|
927
|
+
type: "layout-snapshot",
|
|
928
|
+
topologyEpoch: authority?.topologyEpoch,
|
|
929
|
+
layouts: stagedInitialFrames,
|
|
930
|
+
windowLinks: authority?.windowLinks,
|
|
931
|
+
}).success) {
|
|
924
932
|
await subscription.close().catch(() => undefined);
|
|
925
933
|
this.#failTopologyChanged();
|
|
926
934
|
return;
|
|
@@ -1006,7 +1014,13 @@ export class PaneStreamLiveConnection {
|
|
|
1006
1014
|
authority.topologyEpoch < 0
|
|
1007
1015
|
? null
|
|
1008
1016
|
: this.#validateInitialLayout(authority.layouts, expectedPaneIds);
|
|
1009
|
-
if (!initialFrames
|
|
1017
|
+
if (!initialFrames ||
|
|
1018
|
+
!PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
|
|
1019
|
+
type: "layout-snapshot",
|
|
1020
|
+
topologyEpoch: authority?.topologyEpoch,
|
|
1021
|
+
layouts: initialFrames,
|
|
1022
|
+
windowLinks: authority?.windowLinks,
|
|
1023
|
+
}).success) {
|
|
1010
1024
|
await Promise.all(opened.map(({ delivery }) => delivery.close().catch(() => undefined)));
|
|
1011
1025
|
this.#failTopologyChanged();
|
|
1012
1026
|
return;
|
|
@@ -1014,6 +1028,7 @@ export class PaneStreamLiveConnection {
|
|
|
1014
1028
|
this.#recordDiagnosticLifecycle("pane-stream-layout-validated");
|
|
1015
1029
|
this.#semanticExpectedPaneIds = expectedPaneIds;
|
|
1016
1030
|
this.#semanticRuntimeSessionId = authority.runtimeSessionId;
|
|
1031
|
+
this.#semanticWindowLinks = authority.windowLinks;
|
|
1017
1032
|
this.#semanticTopologyEpoch = authority.topologyEpoch;
|
|
1018
1033
|
this.#semanticLayouts.clear();
|
|
1019
1034
|
for (const event of authority.layouts)
|
|
@@ -1022,6 +1037,7 @@ export class PaneStreamLiveConnection {
|
|
|
1022
1037
|
type: "layout-snapshot",
|
|
1023
1038
|
topologyEpoch: authority.topologyEpoch,
|
|
1024
1039
|
layouts: initialFrames,
|
|
1040
|
+
windowLinks: authority.windowLinks,
|
|
1025
1041
|
});
|
|
1026
1042
|
layoutActivated = true;
|
|
1027
1043
|
this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
|
|
@@ -1332,10 +1348,27 @@ export class PaneStreamLiveConnection {
|
|
|
1332
1348
|
return;
|
|
1333
1349
|
}
|
|
1334
1350
|
const frames = this.#validateInitialLayout(snapshot.layouts, this.#semanticExpectedPaneIds);
|
|
1335
|
-
if (!frames
|
|
1351
|
+
if (!frames ||
|
|
1352
|
+
!PaneStreamLayoutSnapshotFrameSchemaZ.safeParse({
|
|
1353
|
+
type: "layout-snapshot",
|
|
1354
|
+
topologyEpoch: snapshot.topologyEpoch,
|
|
1355
|
+
layouts: frames,
|
|
1356
|
+
windowLinks: snapshot.windowLinks,
|
|
1357
|
+
}).success) {
|
|
1336
1358
|
this.#failTopologyChanged();
|
|
1337
1359
|
return;
|
|
1338
1360
|
}
|
|
1361
|
+
const previousLinks = this.#semanticWindowLinks;
|
|
1362
|
+
if (!previousLinks ||
|
|
1363
|
+
snapshot.windowLinks.liveSessionId !== previousLinks.liveSessionId ||
|
|
1364
|
+
snapshot.windowLinks.linkRevision < previousLinks.linkRevision ||
|
|
1365
|
+
(snapshot.windowLinks.linkRevision === previousLinks.linkRevision &&
|
|
1366
|
+
JSON.stringify([...snapshot.windowLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId))) !==
|
|
1367
|
+
JSON.stringify([...previousLinks.links].sort((a, b) => a.linkId.localeCompare(b.linkId))))) {
|
|
1368
|
+
this.#failTopologyChanged();
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
this.#semanticWindowLinks = snapshot.windowLinks;
|
|
1339
1372
|
this.#semanticTopologyEpoch = snapshot.topologyEpoch;
|
|
1340
1373
|
this.#semanticLayouts.clear();
|
|
1341
1374
|
for (const event of snapshot.layouts)
|
|
@@ -1344,6 +1377,7 @@ export class PaneStreamLiveConnection {
|
|
|
1344
1377
|
type: "layout-snapshot",
|
|
1345
1378
|
topologyEpoch: snapshot.topologyEpoch,
|
|
1346
1379
|
layouts: frames,
|
|
1380
|
+
windowLinks: snapshot.windowLinks,
|
|
1347
1381
|
});
|
|
1348
1382
|
}
|
|
1349
1383
|
#onLayout(event) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Shared opaque incarnation identity for catalog and terminal authority. */
|
|
3
|
+
export function liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated) {
|
|
4
|
+
const digest = createHash("sha256")
|
|
5
|
+
.update(`${serverPid}\0${sessionId}\0${sessionCreated}`)
|
|
6
|
+
.digest("hex")
|
|
7
|
+
.slice(0, 20);
|
|
8
|
+
return `live-session.${digest}`;
|
|
9
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { decodeNativeGridCapture } from "../mirror/native-grid-capture.js";
|
|
2
2
|
/** One bounded read on entering scrollback. No captures are issued by wheel movement. */
|
|
3
3
|
export async function readNativeBacking(options) {
|
|
4
|
-
const url = new URL(
|
|
4
|
+
const url = new URL(options.resourcePath ??
|
|
5
|
+
`/api/project/${encodeURIComponent(options.workspaceName)}/terminal-native-backing/${encodeURIComponent(options.paneId)}`, options.baseUrl);
|
|
5
6
|
for (const [key, value] of Object.entries(options.expected))
|
|
6
7
|
url.searchParams.set(key, String(value));
|
|
7
8
|
const response = await fetch(url, {
|
|
@@ -7,6 +7,12 @@ export function sessionRuntimeInteractionFacts(intent) {
|
|
|
7
7
|
target: { kind: "pane", semanticPaneId: intent.semanticPaneId },
|
|
8
8
|
summary: { operationKind: intent.verb, direction: intent.direction },
|
|
9
9
|
};
|
|
10
|
+
case "workspace.window.link.select":
|
|
11
|
+
case "workspace.window.link.unlink":
|
|
12
|
+
return {
|
|
13
|
+
target: { kind: "window-link", target: intent.target },
|
|
14
|
+
summary: { operationKind: intent.verb },
|
|
15
|
+
};
|
|
10
16
|
case "workspace.window.kill":
|
|
11
17
|
return {
|
|
12
18
|
target: { kind: "window", target: intent.target },
|
|
@@ -85,6 +91,9 @@ export function sessionRuntimeObservedProof(intent, rawResult) {
|
|
|
85
91
|
if (result.verb !== intent.verb)
|
|
86
92
|
throw new TypeError("Mutation result verb does not match intent");
|
|
87
93
|
switch (result.verb) {
|
|
94
|
+
case "workspace.window.link.select":
|
|
95
|
+
case "workspace.window.link.unlink":
|
|
96
|
+
return { operationKind: result.verb, outcome: result.outcome, target: result.target };
|
|
88
97
|
case "workspace.window.split":
|
|
89
98
|
return {
|
|
90
99
|
operationKind: result.verb,
|
|
@@ -258,6 +258,12 @@ export class SessionRuntimeRegistry {
|
|
|
258
258
|
return ((this.#sessions.get(session)?.trustedInventoryQualified() ?? false) &&
|
|
259
259
|
this.#mirror.hasRetainedSession(session));
|
|
260
260
|
}
|
|
261
|
+
/** Called by the serialized semantic mutation executor, never by a transport directly. */
|
|
262
|
+
executeWindowLinkAction(session, request) {
|
|
263
|
+
if (this.#disposed)
|
|
264
|
+
return Promise.reject(new Error("Session runtime disposed"));
|
|
265
|
+
return this.#mirror.executeWindowLinkAction(session, request);
|
|
266
|
+
}
|
|
261
267
|
/** Retire one no-longer-registered session without disturbing siblings. */
|
|
262
268
|
async retireSession(session) {
|
|
263
269
|
const runtime = this.#sessions.get(session);
|
|
@@ -244,7 +244,7 @@ export class SessionSemanticMutationExecutor {
|
|
|
244
244
|
// Admission can wait behind prior work. Revalidate the opaque principal
|
|
245
245
|
// at the last synchronous boundary before tmux receives any effect.
|
|
246
246
|
authorizeBeforeEffect?.();
|
|
247
|
-
result = this.#options.execute(operationId, intent, timing);
|
|
247
|
+
result = await this.#options.execute(operationId, intent, timing);
|
|
248
248
|
}
|
|
249
249
|
catch (cause) {
|
|
250
250
|
if (needsTmuxObservation)
|
|
@@ -17,6 +17,7 @@ export function semanticMutationResourceChanges(raw) {
|
|
|
17
17
|
];
|
|
18
18
|
if (result.verb === "workspace.window.split" ||
|
|
19
19
|
result.verb === "workspace.window.kill" ||
|
|
20
|
+
result.verb === "workspace.window.link.unlink" ||
|
|
20
21
|
result.verb === "workspace.pane.kill" ||
|
|
21
22
|
result.verb === "workspace.session.kill") {
|
|
22
23
|
changes.push({ ...base, workspaceName: null, resource: "fleet-catalog" });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { prepareScopedOpenTuiConnection } from "./application-shell-server-connection.js";
|
|
1
2
|
import { OpenTuiStartupError, startupFailureFromError } from "./startup-failure.js";
|
|
2
|
-
import { readApplicationDaemonInfo as readCanonicalDaemonInfo, isApplicationDaemonAlive as isCanonicalDaemonAlive, } from "./runtime/application-daemon-authority.js";
|
|
3
|
+
import { applicationDaemonEndpoint, readApplicationDaemonInfo as readCanonicalDaemonInfo, isApplicationDaemonAlive as isCanonicalDaemonAlive, } from "./runtime/application-daemon-authority.js";
|
|
3
4
|
import { APPLICATION_SHELL_RESOURCE_V2_VERSION, } from "@tmux-ide/contracts";
|
|
4
5
|
import { createDirectLoopbackDaemonTransport, } from "@tmux-ide/daemon-client/direct-application-shell-transport";
|
|
5
6
|
import { WebSocket } from "ws";
|
|
@@ -88,7 +89,7 @@ export async function resolveOpenTuiApplicationShellConnection(sessionName, over
|
|
|
88
89
|
applicationShellResourceVersion: APPLICATION_SHELL_RESOURCE_V2_VERSION,
|
|
89
90
|
...(diagnose ? { terminalRuntimeDiagnostic: diagnose } : {}),
|
|
90
91
|
});
|
|
91
|
-
const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName);
|
|
92
|
+
const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint ?? applicationDaemonEndpoint, undefined, liveSessionId ?? undefined);
|
|
92
93
|
let disposed = false;
|
|
93
94
|
let diagnosticClientFetchOrdinal = 0;
|
|
94
95
|
let diagnosticPrewarmOrdinal = 0;
|
|
@@ -263,6 +264,10 @@ export async function resolveOpenTuiApplicationShellConnection(sessionName, over
|
|
|
263
264
|
export async function prepareOpenTuiApplicationShellConnection(sessionName, overrides = {}) {
|
|
264
265
|
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
265
266
|
try {
|
|
267
|
+
if (dependencies.server) {
|
|
268
|
+
const unscoped = { ...dependencies, server: undefined };
|
|
269
|
+
return await prepareScopedOpenTuiConnection(sessionName, dependencies, () => prepareOpenTuiApplicationShellConnection(sessionName, unscoped));
|
|
270
|
+
}
|
|
266
271
|
const result = await dependencies.ensureSessionWorkspace(sessionName);
|
|
267
272
|
if (result === false)
|
|
268
273
|
return null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
2
|
+
import { createScopedTmuxServerTransport, createScopedTmuxServerCatalogPort, createScopedTmuxServerOwnerActions, } from "@tmux-ide/daemon-client/scoped-tmux-server-transport";
|
|
3
|
+
import { canonicalDaemonUrl } from "../../lib/canonical-daemon.js";
|
|
4
|
+
import { createOpenTuiVerifiedRoutingContext } from "./open-tui-verified-routing.js";
|
|
5
|
+
import { OPEN_TUI_HOST_CLIENT_ID } from "./open-tui-workspace-runtime-port.js";
|
|
6
|
+
import { OpenTuiStartupError } from "./startup-failure.js";
|
|
7
|
+
/** A selected registration never falls through to a name-only/default-server route. */
|
|
8
|
+
export async function prepareScopedOpenTuiConnection(sessionName, dependencies, prepareDefault) {
|
|
9
|
+
const scope = dependencies.server;
|
|
10
|
+
const daemon = dependencies.readCanonicalDaemonInfo();
|
|
11
|
+
if (!daemon || !daemon.authToken || !(await dependencies.isCanonicalDaemonAlive(daemon)))
|
|
12
|
+
throw new OpenTuiStartupError({ reason: "daemon-unavailable" });
|
|
13
|
+
const clientOptions = {
|
|
14
|
+
baseUrl: canonicalDaemonUrl("http", daemon.bindHostname, daemon.port),
|
|
15
|
+
ownerToken: daemon.authToken,
|
|
16
|
+
hostClientId: OPEN_TUI_HOST_CLIENT_ID,
|
|
17
|
+
origin: "tmux-ide://opentui",
|
|
18
|
+
};
|
|
19
|
+
const client = createTmuxServerClient(clientOptions, scope);
|
|
20
|
+
try {
|
|
21
|
+
const catalog = await client.sessions();
|
|
22
|
+
const candidates = catalog.sessions.filter((session) => session.sessionName === sessionName &&
|
|
23
|
+
(!dependencies.expectedLiveSessionId ||
|
|
24
|
+
session.liveSessionId === dependencies.expectedLiveSessionId));
|
|
25
|
+
if (candidates.length !== 1)
|
|
26
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
27
|
+
const liveSessionId = candidates[0].liveSessionId;
|
|
28
|
+
// The borrowed default owner has the daemon's original generation. Its existing
|
|
29
|
+
// transport preserves the full app surface while the scoped read proves routing.
|
|
30
|
+
if (scope.generation === daemon.instanceId) {
|
|
31
|
+
const connection = await prepareDefault();
|
|
32
|
+
if (!connection || connection.liveSessionId !== liveSessionId) {
|
|
33
|
+
connection?.dispose();
|
|
34
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const current = await client.sessions();
|
|
38
|
+
if (!current.sessions.some((session) => session.liveSessionId === liveSessionId && session.sessionName === sessionName))
|
|
39
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
40
|
+
const options = {
|
|
41
|
+
clientOptions,
|
|
42
|
+
scope,
|
|
43
|
+
target: connection.target,
|
|
44
|
+
sessionName,
|
|
45
|
+
liveSessionId,
|
|
46
|
+
};
|
|
47
|
+
const routing = createOpenTuiVerifiedRoutingContext(daemon, connection.workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint, scope, liveSessionId);
|
|
48
|
+
connection.routing?.retire();
|
|
49
|
+
return {
|
|
50
|
+
...connection,
|
|
51
|
+
server: scope,
|
|
52
|
+
routing,
|
|
53
|
+
ownerActions: createScopedTmuxServerOwnerActions(options, connection.transport),
|
|
54
|
+
dispose() {
|
|
55
|
+
routing?.retire();
|
|
56
|
+
connection.dispose();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
connection.dispose();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const opened = await client.openSession(liveSessionId);
|
|
66
|
+
if (opened.liveSessionId !== liveSessionId)
|
|
67
|
+
throw new OpenTuiStartupError({ reason: "session-unavailable" });
|
|
68
|
+
const workspaceName = opened.workspaceName;
|
|
69
|
+
const target = {
|
|
70
|
+
daemon: {
|
|
71
|
+
protocolVersion: daemon.protocolVersion,
|
|
72
|
+
productVersion: daemon.productVersion,
|
|
73
|
+
instanceId: scope.generation,
|
|
74
|
+
startedAt: daemon.startedAt,
|
|
75
|
+
...(daemon.environmentId ? { environmentId: daemon.environmentId } : {}),
|
|
76
|
+
},
|
|
77
|
+
workspaceName,
|
|
78
|
+
};
|
|
79
|
+
const options = { clientOptions, scope, target, sessionName, liveSessionId };
|
|
80
|
+
const transport = createScopedTmuxServerTransport(options);
|
|
81
|
+
const routing = createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, undefined, dependencies.readDaemonEndpoint, scope, liveSessionId);
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
let preparation = null;
|
|
84
|
+
return {
|
|
85
|
+
workspaceName,
|
|
86
|
+
liveSessionId,
|
|
87
|
+
target,
|
|
88
|
+
server: scope,
|
|
89
|
+
transport,
|
|
90
|
+
routing,
|
|
91
|
+
catalog: createScopedTmuxServerCatalogPort(options, transport),
|
|
92
|
+
ownerActions: createScopedTmuxServerOwnerActions(options, transport),
|
|
93
|
+
prepareTerminalRuntimeInventory() {
|
|
94
|
+
preparation ??= transport
|
|
95
|
+
.prepareTerminalRuntimeInventory(target, AbortSignal.any([controller.signal, AbortSignal.timeout(5_000)]))
|
|
96
|
+
.catch(() => {
|
|
97
|
+
// Fallback remains inside this owner; never use the legacy default resource.
|
|
98
|
+
transport.selectApplicationShellFallback("preparation-rejected");
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
return preparation;
|
|
102
|
+
},
|
|
103
|
+
dispose() {
|
|
104
|
+
controller.abort(new DOMException("Selected server connection retired", "AbortError"));
|
|
105
|
+
void preparation?.then((prepared) => prepared?.dispose()).catch(() => undefined);
|
|
106
|
+
transport.disposeEventSupervisor();
|
|
107
|
+
routing?.retire();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
client.dispose();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -52,7 +52,7 @@ export const MENU_ITEMS = {
|
|
|
52
52
|
window: [
|
|
53
53
|
{ id: "new", label: "New window" },
|
|
54
54
|
{ id: "rename", label: "Rename window", input: "rename to" },
|
|
55
|
-
{ id: "kill", label: "Kill window", danger: true },
|
|
55
|
+
{ id: "kill", label: "Kill window and panes in all links", danger: true },
|
|
56
56
|
],
|
|
57
57
|
};
|
|
58
58
|
/** PURE — the pane menu's item list for a concrete pane (M22.9). App-mouse
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { openPaneStreamRuntimeClient, } from "@tmux-ide/daemon-client/pane-stream-client";
|
|
1
|
+
import { openPaneStreamRuntimeClient, connectIssuedPaneStreamRuntimeClient, } from "@tmux-ide/daemon-client/pane-stream-client";
|
|
2
|
+
import { createTmuxServerClient } from "@tmux-ide/daemon-client/tmux-server-client";
|
|
2
3
|
import { canonicalDaemonUrl } from "../../lib/canonical-daemon.js";
|
|
3
4
|
import { readNativeBacking, } from "../../terminal/protocol/native-backing-client.js";
|
|
4
5
|
import { applicationDaemonEndpoint, } from "./runtime/application-daemon-authority.js";
|
|
5
|
-
export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, openClient = openPaneStreamRuntimeClient, readEndpoint = applicationDaemonEndpoint) {
|
|
6
|
+
export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessionName, openClient = openPaneStreamRuntimeClient, readEndpoint = applicationDaemonEndpoint, server, liveSessionId) {
|
|
6
7
|
if (!daemon.authToken)
|
|
7
8
|
return null;
|
|
8
9
|
const ownerToken = daemon.authToken;
|
|
@@ -21,7 +22,7 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
|
|
|
21
22
|
const localSocketOrigin = new URL(baseUrl);
|
|
22
23
|
localSocketOrigin.protocol = "ws:";
|
|
23
24
|
const identity = Object.freeze({
|
|
24
|
-
daemonInstanceId: daemon.instanceId,
|
|
25
|
+
daemonInstanceId: server?.generation ?? daemon.instanceId,
|
|
25
26
|
workspaceName,
|
|
26
27
|
sessionName,
|
|
27
28
|
});
|
|
@@ -47,11 +48,17 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
|
|
|
47
48
|
};
|
|
48
49
|
return Object.freeze({
|
|
49
50
|
...identity,
|
|
51
|
+
...(liveSessionId ? { liveSessionId } : {}),
|
|
50
52
|
assertCurrent,
|
|
51
53
|
readNativeBacking: async (paneId, expected, signal) => {
|
|
52
54
|
assertCurrent({ ...identity, daemonInstanceId: expected.generation });
|
|
53
55
|
const result = await readNativeBacking({
|
|
54
56
|
baseUrl,
|
|
57
|
+
...(server
|
|
58
|
+
? {
|
|
59
|
+
resourcePath: `/api/v1/tmux-servers/${server.serverId}/${server.generation}/native-backing/${encodeURIComponent(workspaceName)}/${encodeURIComponent(paneId)}`,
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
55
62
|
ownerToken,
|
|
56
63
|
workspaceName,
|
|
57
64
|
paneId,
|
|
@@ -66,7 +73,30 @@ export function createOpenTuiVerifiedRoutingContext(daemon, workspaceName, sessi
|
|
|
66
73
|
if (options.stream.workspaceName !== identity.workspaceName) {
|
|
67
74
|
throw new Error("Pane-stream request escaped its verified workspace route");
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
const openScoped = async (scopedOptions) => {
|
|
77
|
+
const client = createTmuxServerClient({
|
|
78
|
+
baseUrl,
|
|
79
|
+
ownerToken,
|
|
80
|
+
hostClientId: scopedOptions.hostClientId,
|
|
81
|
+
origin: scopedOptions.origin,
|
|
82
|
+
fetch: scopedOptions.fetch,
|
|
83
|
+
}, server);
|
|
84
|
+
try {
|
|
85
|
+
if (scopedOptions.signal?.aborted)
|
|
86
|
+
throw scopedOptions.signal.reason;
|
|
87
|
+
const issued = await client.issuePaneStream(scopedOptions.requestId, scopedOptions.stream, liveSessionId);
|
|
88
|
+
assertCurrent(expected);
|
|
89
|
+
if (scopedOptions.signal?.aborted)
|
|
90
|
+
throw scopedOptions.signal.reason;
|
|
91
|
+
if (issued.status === "error")
|
|
92
|
+
throw new Error(issued.error.reason);
|
|
93
|
+
return await connectIssuedPaneStreamRuntimeClient(scopedOptions, issued.descriptor);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
client.dispose();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return await (server ? openScoped : openClient)({
|
|
70
100
|
...options,
|
|
71
101
|
baseUrl,
|
|
72
102
|
ownerToken,
|