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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fenceNativeTmuxCommand, } from "../../lib/tmux-server-generation-runner.js";
|
|
1
2
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { execFile } from "node:child_process";
|
|
3
4
|
import { isAbsolute } from "node:path";
|
|
@@ -91,6 +92,7 @@ function canonicalAuthority(input) {
|
|
|
91
92
|
socketArgv = ["-L", input.socketSelector.name];
|
|
92
93
|
}
|
|
93
94
|
return Object.freeze({
|
|
95
|
+
nativeServerIdentity: input.nativeServerIdentity,
|
|
94
96
|
namedSocketFence: socketSelector.kind === "name" ? (input.namedSocketFence ?? null) : null,
|
|
95
97
|
executablePath,
|
|
96
98
|
socketSelector: Object.freeze(socketSelector),
|
|
@@ -144,13 +146,16 @@ function pinnedRunner(authority, execute, startupPolicy) {
|
|
|
144
146
|
if (command.executable !== "tmux")
|
|
145
147
|
return { status: "failed" };
|
|
146
148
|
try {
|
|
147
|
-
const
|
|
149
|
+
const guarded = authority.nativeServerIdentity
|
|
150
|
+
? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
|
|
151
|
+
: null;
|
|
152
|
+
const stdout = execute(authority.executablePath, [...currentSocketArgv(authority), ...(guarded?.argv ?? command.argv)], {
|
|
148
153
|
cwd: authority.trustedCwd,
|
|
149
154
|
env: authority.environment,
|
|
150
155
|
maxBuffer: MAX_TMUX_OUTPUT_BYTES,
|
|
151
156
|
timeoutMs: TERMINAL_ATTACHMENT_TMUX_COMMAND_TIMEOUT_MS,
|
|
152
157
|
});
|
|
153
|
-
const value = String(stdout);
|
|
158
|
+
const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
|
|
154
159
|
if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
|
|
155
160
|
authority.namedSocketFence.resolve();
|
|
156
161
|
if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES) {
|
|
@@ -209,11 +214,14 @@ function pinnedReadRunner(authority, execute) {
|
|
|
209
214
|
if (command.executable !== "tmux" || signal?.aborted)
|
|
210
215
|
return { status: "failed" };
|
|
211
216
|
try {
|
|
217
|
+
const guarded = authority.nativeServerIdentity
|
|
218
|
+
? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
|
|
219
|
+
: null;
|
|
212
220
|
const stdout = await execute(authority.executablePath, [
|
|
213
221
|
...(authority.namedSocketFence
|
|
214
222
|
? await authority.namedSocketFence.resolveAsync(signal)
|
|
215
223
|
: currentSocketArgv(authority)),
|
|
216
|
-
...command.argv,
|
|
224
|
+
...(guarded?.argv ?? command.argv),
|
|
217
225
|
], {
|
|
218
226
|
cwd: authority.trustedCwd,
|
|
219
227
|
env: authority.environment,
|
|
@@ -221,7 +229,7 @@ function pinnedReadRunner(authority, execute) {
|
|
|
221
229
|
timeoutMs: TERMINAL_ATTACHMENT_TMUX_COMMAND_TIMEOUT_MS,
|
|
222
230
|
...(signal ? { signal } : {}),
|
|
223
231
|
});
|
|
224
|
-
const value = String(stdout);
|
|
232
|
+
const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
|
|
225
233
|
if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
|
|
226
234
|
await authority.namedSocketFence.resolveAsync(signal);
|
|
227
235
|
if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES)
|
|
@@ -443,6 +451,7 @@ const PANE_FORMAT = [
|
|
|
443
451
|
// window reports the same value; the catalog requires it before a multi-pane
|
|
444
452
|
// window is attachable.
|
|
445
453
|
"#{@tmux_ide_window_id}",
|
|
454
|
+
"#{window_index}",
|
|
446
455
|
PANE_WIRE_SENTINEL,
|
|
447
456
|
].join(WIRE_SEPARATOR);
|
|
448
457
|
async function requiredTmuxResult(runner, argv, signal) {
|
|
@@ -479,25 +488,24 @@ async function liveSessionIdentities(runner, signal) {
|
|
|
479
488
|
}
|
|
480
489
|
function parsePaneSnapshot(stdout, expected) {
|
|
481
490
|
const panes = [];
|
|
482
|
-
const runtimeIds = new
|
|
491
|
+
const runtimeIds = new Map();
|
|
492
|
+
const links = new Map();
|
|
483
493
|
for (const line of strictLines(stdout, MAX_DISCOVERED_PANES)) {
|
|
484
494
|
const fields = line.split(WIRE_SEPARATOR);
|
|
485
|
-
if (fields.length !==
|
|
495
|
+
if (fields.length !== 20 || fields[19] !== PANE_WIRE_SENTINEL) {
|
|
486
496
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
487
497
|
}
|
|
488
|
-
const [sessionName, sessionId, windowId, runtimePaneId, paneCountValue, windowCountValue, stamp, indexValue, title, currentCommand, windowActive, paneActive, role, name, type, missionStamp, dir, windowStampValue,] = fields;
|
|
498
|
+
const [sessionName, sessionId, windowId, runtimePaneId, paneCountValue, windowCountValue, stamp, indexValue, title, currentCommand, windowActive, paneActive, role, name, type, missionStamp, dir, windowStampValue, windowIndexValue,] = fields;
|
|
489
499
|
if (sessionName !== expected.name ||
|
|
490
500
|
sessionId !== expected.id ||
|
|
491
501
|
!RUNTIME_WINDOW_ID.test(windowId) ||
|
|
492
502
|
!RUNTIME_PANE_ID.test(runtimePaneId) ||
|
|
493
|
-
runtimeIds.has(runtimePaneId) ||
|
|
494
503
|
!["0", "1"].includes(windowActive) ||
|
|
495
504
|
!["0", "1"].includes(paneActive)) {
|
|
496
505
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
497
506
|
}
|
|
498
|
-
runtimeIds.add(runtimePaneId);
|
|
499
507
|
const nullable = (value) => boundedWireValue(value, 256).length === 0 ? null : value;
|
|
500
|
-
|
|
508
|
+
const facts = {
|
|
501
509
|
sessionName,
|
|
502
510
|
sessionId,
|
|
503
511
|
windowId,
|
|
@@ -519,18 +527,51 @@ function parsePaneSnapshot(stdout, expected) {
|
|
|
519
527
|
// registered workspace remains the trusted application-shell root, so
|
|
520
528
|
// keep discovery available and carry the empty presentation value.
|
|
521
529
|
dir: boundedWireValue(dir, 4_096),
|
|
522
|
-
}
|
|
530
|
+
};
|
|
531
|
+
const windowIndex = nonnegativeInteger(windowIndexValue);
|
|
532
|
+
const link = links.get(windowIndex) ?? {
|
|
533
|
+
windowId,
|
|
534
|
+
active: windowActive === "1",
|
|
535
|
+
panes: new Set(),
|
|
536
|
+
};
|
|
537
|
+
if (link.windowId !== windowId ||
|
|
538
|
+
link.active !== (windowActive === "1") ||
|
|
539
|
+
link.panes.has(runtimePaneId)) {
|
|
540
|
+
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
541
|
+
}
|
|
542
|
+
link.panes.add(runtimePaneId);
|
|
543
|
+
links.set(windowIndex, link);
|
|
544
|
+
// Only link index and current-link state may differ for a shared backing.
|
|
545
|
+
const proof = JSON.stringify(fields.filter((_value, index) => index !== 10 && index !== 18));
|
|
546
|
+
const previous = runtimeIds.get(runtimePaneId);
|
|
547
|
+
if (previous) {
|
|
548
|
+
if (previous.proof !== proof)
|
|
549
|
+
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
550
|
+
if (facts.active && !previous.facts.active) {
|
|
551
|
+
const merged = { ...previous.facts, active: true };
|
|
552
|
+
panes[panes.indexOf(previous.facts)] = merged;
|
|
553
|
+
previous.facts = merged;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
runtimeIds.set(runtimePaneId, { facts, proof });
|
|
558
|
+
panes.push(facts);
|
|
559
|
+
}
|
|
523
560
|
}
|
|
524
561
|
const counts = new Map();
|
|
525
562
|
for (const pane of panes)
|
|
526
563
|
counts.set(pane.windowId, (counts.get(pane.windowId) ?? 0) + 1);
|
|
527
|
-
const windows = new Set(panes.map((pane) => pane.windowId));
|
|
528
564
|
if (panes.some((pane) => counts.get(pane.windowId) !== pane.windowPaneCount ||
|
|
529
|
-
|
|
565
|
+
links.size !== pane.sessionWindowCount) ||
|
|
566
|
+
[...links.values()].some((link) => link.panes.size !== counts.get(link.windowId)) ||
|
|
567
|
+
[...links.values()].filter((link) => link.active).length > 1 ||
|
|
530
568
|
panes.filter((pane) => pane.active).length > 1) {
|
|
531
569
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
532
570
|
}
|
|
533
|
-
return
|
|
571
|
+
return {
|
|
572
|
+
panes,
|
|
573
|
+
linksProof: JSON.stringify([...links].map(([index, link]) => [index, link.windowId, [...link.panes]])),
|
|
574
|
+
};
|
|
534
575
|
}
|
|
535
576
|
/**
|
|
536
577
|
* One bounded, old-tmux-safe discovery path for both live attachment and the
|
|
@@ -558,11 +599,13 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
|
|
|
558
599
|
const before = await requiredTmuxResult(runner, argv, signal);
|
|
559
600
|
if (before === null)
|
|
560
601
|
continue;
|
|
561
|
-
const
|
|
602
|
+
const first = parsePaneSnapshot(before, identity);
|
|
603
|
+
const panes = first.panes;
|
|
562
604
|
const after = await requiredTmuxResult(runner, argv, signal);
|
|
563
605
|
if (after === null)
|
|
564
606
|
throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
565
|
-
const
|
|
607
|
+
const second = parsePaneSnapshot(after, identity);
|
|
608
|
+
const latest = second.panes;
|
|
566
609
|
// Foreground commands, titles, focus and paths can change while the same
|
|
567
610
|
// pane remains valid. Fence binding/topology only, then publish one complete
|
|
568
611
|
// latest snapshot rather than mixing presentation values across reads.
|
|
@@ -577,7 +620,8 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
|
|
|
577
620
|
"windowStamp",
|
|
578
621
|
"index",
|
|
579
622
|
];
|
|
580
|
-
if (
|
|
623
|
+
if (first.linksProof !== second.linksProof ||
|
|
624
|
+
panes.length !== latest.length ||
|
|
581
625
|
panes.some((pane, index) => proofKeys.some((key) => pane[key] !== latest[index][key]))) {
|
|
582
626
|
throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
583
627
|
}
|
|
@@ -23,6 +23,10 @@ export class ScriptedChannelDriver {
|
|
|
23
23
|
if (command.startsWith('display-message -p "#{qa:session_name}')) {
|
|
24
24
|
return basic(command) ?? [];
|
|
25
25
|
}
|
|
26
|
+
// This scalar observation is not a deferred cursor probe. Answer it
|
|
27
|
+
// before an automatic inventory reply can consume its pending FIFO slot.
|
|
28
|
+
if (isHistorySizeProbe(command))
|
|
29
|
+
return ["0"];
|
|
26
30
|
if (command.includes("capture-pane") || command.startsWith("display-message"))
|
|
27
31
|
return null;
|
|
28
32
|
return basic(command) ?? [];
|
|
@@ -32,7 +36,8 @@ export class ScriptedChannelDriver {
|
|
|
32
36
|
pump() {
|
|
33
37
|
for (let index = this.#handledWrites; index < this.channel.written.length; index += 1) {
|
|
34
38
|
const command = this.channel.written[index];
|
|
35
|
-
if (command.startsWith('display-message -p "#{qa:session_name}')
|
|
39
|
+
if (command.startsWith('display-message -p "#{qa:session_name}') ||
|
|
40
|
+
isHistorySizeProbe(command))
|
|
36
41
|
continue;
|
|
37
42
|
if (command.includes("capture-pane") || command.startsWith("display-message")) {
|
|
38
43
|
this.deferredCommands.push(command);
|
|
@@ -88,3 +93,6 @@ export function standardScriptedChannel(handlers, options) {
|
|
|
88
93
|
...options,
|
|
89
94
|
});
|
|
90
95
|
}
|
|
96
|
+
function isHistorySizeProbe(command) {
|
|
97
|
+
return /^display-message -p -t %\d+ "#\{history_size\}"$/u.test(command);
|
|
98
|
+
}
|
|
@@ -116,13 +116,22 @@ export const FIXTURE = {
|
|
|
116
116
|
export function fixtureAutoReply(state) {
|
|
117
117
|
return (cmd) => {
|
|
118
118
|
if (cmd.startsWith('display-message -p "#{qa:session_name}'))
|
|
119
|
-
return ["zz-sim\t$1"];
|
|
119
|
+
return ["zz-sim\t$1\t1234\t1700000000"];
|
|
120
120
|
if (cmd.includes("qa:@tmux_ide_pane_id"))
|
|
121
121
|
return state.descriptorRows;
|
|
122
122
|
if (cmd.startsWith("list-panes -s"))
|
|
123
123
|
return state.truthRows;
|
|
124
124
|
if (cmd.startsWith("list-windows"))
|
|
125
|
-
return state.windowRows
|
|
125
|
+
return state.windowRows.map((row, index) => {
|
|
126
|
+
const parts = row.split("\t");
|
|
127
|
+
if (parts.length === 7)
|
|
128
|
+
parts.push(parts[4]);
|
|
129
|
+
if (parts.length === 8)
|
|
130
|
+
parts.push("emacs");
|
|
131
|
+
if (parts.length === 9)
|
|
132
|
+
parts.push(String(index));
|
|
133
|
+
return parts.join("\t");
|
|
134
|
+
});
|
|
126
135
|
// Product-owned seeds are one atomic `set-option ; capture-pane` command
|
|
127
136
|
// list. The capture reply remains manual just like a bare capture probe.
|
|
128
137
|
if (cmd.includes("capture-pane"))
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* chance to detach us — killing the reader first can wedge the server.
|
|
21
21
|
*/
|
|
22
22
|
import { spawn } from "node:child_process";
|
|
23
|
+
import { shellEscape } from "../../lib/shell.js";
|
|
23
24
|
import { parseControlLine } from "../protocol/control.js";
|
|
24
25
|
const ATOMIC_CAPTURE_BYTE_HARD_CAP = 16 * 1024 * 1024;
|
|
25
26
|
// Budget row bookkeeping separately from wire bytes (64 bytes per retained
|
|
@@ -538,6 +539,17 @@ export class ControlChannelCore {
|
|
|
538
539
|
}
|
|
539
540
|
}
|
|
540
541
|
export function mirrorControlAttachArgs(options, pauseAfterSeconds = DEFAULT_PAUSE_AFTER_SECONDS) {
|
|
542
|
+
const attach = [
|
|
543
|
+
"attach",
|
|
544
|
+
"-t",
|
|
545
|
+
options.session,
|
|
546
|
+
"-f",
|
|
547
|
+
`ignore-size,pause-after=${pauseAfterSeconds},active-pane`,
|
|
548
|
+
];
|
|
549
|
+
const expected = options.nativeServerIdentity;
|
|
550
|
+
if (expected &&
|
|
551
|
+
(!/^[1-9][0-9]*$/u.test(expected.pid) || !/^[1-9][0-9]*$/u.test(expected.startTime)))
|
|
552
|
+
throw new Error("Invalid expected native server identity");
|
|
541
553
|
return [
|
|
542
554
|
...(options.socketPath
|
|
543
555
|
? ["-S", options.socketPath]
|
|
@@ -546,11 +558,15 @@ export function mirrorControlAttachArgs(options, pauseAfterSeconds = DEFAULT_PAU
|
|
|
546
558
|
: []),
|
|
547
559
|
...(options.configFile ? ["-f", options.configFile] : []),
|
|
548
560
|
"-C",
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
561
|
+
...(expected
|
|
562
|
+
? [
|
|
563
|
+
"if-shell",
|
|
564
|
+
"-F",
|
|
565
|
+
`#{&&:#{==:#{pid},${expected.pid}},#{==:#{start_time},${expected.startTime}}}`,
|
|
566
|
+
attach.map(shellEscape).join(" "),
|
|
567
|
+
"display-message -p 'tmux-server-generation-refused'",
|
|
568
|
+
]
|
|
569
|
+
: attach),
|
|
554
570
|
];
|
|
555
571
|
}
|
|
556
572
|
/** Spike-verified default: bounds a stalled reader's server-side buffering
|
|
@@ -561,11 +577,25 @@ export class MirrorControlChannel {
|
|
|
561
577
|
core;
|
|
562
578
|
opts;
|
|
563
579
|
exited = false;
|
|
580
|
+
verifiedAttach = false;
|
|
581
|
+
guardedStart = null;
|
|
564
582
|
atomicCollectorTimer = null;
|
|
565
583
|
constructor(opts) {
|
|
566
584
|
this.opts = opts;
|
|
567
585
|
this.core = new ControlChannelCore({
|
|
568
586
|
...opts.handlers,
|
|
587
|
+
onOutput: (...args) => {
|
|
588
|
+
if (!opts.nativeServerIdentity || this.verifiedAttach)
|
|
589
|
+
opts.handlers.onOutput(...args);
|
|
590
|
+
},
|
|
591
|
+
onNotify: (name, rest) => {
|
|
592
|
+
if (opts.nativeServerIdentity && name === "session-changed") {
|
|
593
|
+
this.verifiedAttach = true;
|
|
594
|
+
this.guardedStart?.resolve();
|
|
595
|
+
}
|
|
596
|
+
if (!opts.nativeServerIdentity || this.verifiedAttach)
|
|
597
|
+
opts.handlers.onNotify(name, rest);
|
|
598
|
+
},
|
|
569
599
|
onExit: (reason) => this.noteExit(reason),
|
|
570
600
|
}, opts.nowMicros);
|
|
571
601
|
}
|
|
@@ -604,7 +634,35 @@ export class MirrorControlChannel {
|
|
|
604
634
|
// tmux opens with an unsolicited %begin/%end greeting block; a queued
|
|
605
635
|
// resolver makes start() settle when the protocol is actually live.
|
|
606
636
|
return new Promise((resolve, reject) => {
|
|
607
|
-
|
|
637
|
+
let timer;
|
|
638
|
+
const settle = (error) => {
|
|
639
|
+
if (timer)
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
this.guardedStart = null;
|
|
642
|
+
if (error)
|
|
643
|
+
reject(error);
|
|
644
|
+
else
|
|
645
|
+
resolve();
|
|
646
|
+
};
|
|
647
|
+
if (this.opts.nativeServerIdentity) {
|
|
648
|
+
this.guardedStart = { resolve: () => settle(), reject: (error) => settle(error) };
|
|
649
|
+
timer = setTimeout(() => {
|
|
650
|
+
settle(new Error("Tmux generation-guarded attach timed out"));
|
|
651
|
+
void this.dispose();
|
|
652
|
+
}, 5_000);
|
|
653
|
+
timer.unref?.();
|
|
654
|
+
}
|
|
655
|
+
// The outer if-shell greeting precedes its attach branch. Only native
|
|
656
|
+
// session-changed proves that the guarded attach actually succeeded.
|
|
657
|
+
this.core.push({
|
|
658
|
+
kind: "promise",
|
|
659
|
+
resolve: () => {
|
|
660
|
+
if (!this.opts.nativeServerIdentity)
|
|
661
|
+
settle();
|
|
662
|
+
},
|
|
663
|
+
reject: (error) => settle(error),
|
|
664
|
+
lines: [],
|
|
665
|
+
});
|
|
608
666
|
proc.on("error", (err) => {
|
|
609
667
|
this.core.fail(String(err));
|
|
610
668
|
reject(err);
|
|
@@ -747,6 +805,7 @@ export class MirrorControlChannel {
|
|
|
747
805
|
if (this.exited)
|
|
748
806
|
return;
|
|
749
807
|
this.exited = true;
|
|
808
|
+
this.guardedStart?.reject(new Error(reason ?? "Control channel exited before guarded attach"));
|
|
750
809
|
this.opts.handlers.onExit(reason);
|
|
751
810
|
}
|
|
752
811
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { resolveTmuxExecutable, tmuxClientEnvironment } from "../../lib/tmux-client-execution.js";
|
|
4
|
+
import { captureUnixSocketIdentity, revalidateUnixSocketIdentity, } from "../../lib/unix-socket-authority.js";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
1
6
|
/**
|
|
2
7
|
* MirrorService — the daemon-side shared mirror layer (m43 card 1).
|
|
3
8
|
*
|
|
@@ -144,6 +149,12 @@ export class MirrorService {
|
|
|
144
149
|
},
|
|
145
150
|
};
|
|
146
151
|
}
|
|
152
|
+
async executeWindowLinkAction(session, request) {
|
|
153
|
+
const entry = this.channels.get(session);
|
|
154
|
+
if (!entry || entry.retired)
|
|
155
|
+
throw new Error(`Mirror session ${session} is unavailable`);
|
|
156
|
+
return entry.channel.executeWindowLinkAction(request);
|
|
157
|
+
}
|
|
147
158
|
/** Synchronous hot input on an already-retained SessionRuntime channel. */
|
|
148
159
|
sendText(session, semanticPaneId, text, performanceTraceId, isolated = false) {
|
|
149
160
|
const entry = this.channels.get(session);
|
|
@@ -254,6 +265,21 @@ export class MirrorService {
|
|
|
254
265
|
}), this.owner);
|
|
255
266
|
let channel;
|
|
256
267
|
try {
|
|
268
|
+
const initialSocket = this.opts.resolveSocketPath
|
|
269
|
+
? this.opts.resolveSocketPath()
|
|
270
|
+
: this.opts.socketPath;
|
|
271
|
+
const mutationSocketIdentity = initialSocket
|
|
272
|
+
? captureUnixSocketIdentity(initialSocket)
|
|
273
|
+
: null;
|
|
274
|
+
const executable = mutationSocketIdentity
|
|
275
|
+
? realpathSync(this.opts.executable ?? resolveTmuxExecutable())
|
|
276
|
+
: null;
|
|
277
|
+
if (executable) {
|
|
278
|
+
accessSync(executable, constants.X_OK);
|
|
279
|
+
if (!isAbsolute(executable) || !statSync(executable).isFile())
|
|
280
|
+
throw new Error("Invalid pinned tmux executable");
|
|
281
|
+
}
|
|
282
|
+
const environment = Object.freeze(tmuxClientEnvironment(process.env));
|
|
257
283
|
const channelOptions = {
|
|
258
284
|
session,
|
|
259
285
|
createIo: (handlers) => this.opts.createIo?.(session, handlers) ??
|
|
@@ -264,10 +290,28 @@ export class MirrorService {
|
|
|
264
290
|
socketPath: this.opts.socketPath,
|
|
265
291
|
resolveSocketPath: this.opts.resolveSocketPath,
|
|
266
292
|
executable: this.opts.executable,
|
|
293
|
+
nativeServerIdentity: this.opts.nativeServerIdentity,
|
|
267
294
|
configFile: this.opts.configFile,
|
|
268
295
|
pauseAfterSeconds: this.opts.pauseAfterSeconds,
|
|
269
296
|
nowMicros: this.opts.nowMicros,
|
|
270
297
|
}),
|
|
298
|
+
executeWindowLinkGuard: (args) => {
|
|
299
|
+
if (!mutationSocketIdentity || !executable)
|
|
300
|
+
throw new Error("Window link mutation requires a pinned socket authority");
|
|
301
|
+
if (this.opts.resolveSocketPath)
|
|
302
|
+
this.opts.resolveSocketPath();
|
|
303
|
+
const socket = revalidateUnixSocketIdentity(mutationSocketIdentity);
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
execFile(executable, ["-N", "-u", "-S", socket, ...args], { timeout: 5000, maxBuffer: 65536, encoding: "utf8", env: environment }, (error, stdout) => {
|
|
306
|
+
const status = !error
|
|
307
|
+
? 0
|
|
308
|
+
: typeof error.code === "number" && !error.killed
|
|
309
|
+
? error.code
|
|
310
|
+
: null;
|
|
311
|
+
resolve({ status, stdout });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
},
|
|
271
315
|
historyLines: this.opts.historyLines,
|
|
272
316
|
internalReadHookEmission: this.opts.internalReadHookEmission,
|
|
273
317
|
generatePaneId: this.opts.generatePaneId,
|