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
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fenceNativeTmuxCommand,
|
|
3
|
+
type NativeTmuxServerIdentity,
|
|
4
|
+
} from "../../lib/tmux-server-generation-runner.ts";
|
|
1
5
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
6
|
import { execFile } from "node:child_process";
|
|
3
7
|
import type { createNamedSocketFence } from "../../lib/tmux-named-socket-fence.ts";
|
|
@@ -119,6 +123,7 @@ export function getNativeTerminalAttachmentRuntimeConstructionCount(): number {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export interface NativeTerminalAttachmentTmuxAuthority {
|
|
126
|
+
readonly nativeServerIdentity?: NativeTmuxServerIdentity;
|
|
122
127
|
/** Shared with the canonical daemon's named command runners. */
|
|
123
128
|
readonly namedSocketFence?: ReturnType<typeof createNamedSocketFence>;
|
|
124
129
|
readonly executablePath: string;
|
|
@@ -163,6 +168,7 @@ export interface NativeTerminalInventoryReadRunner {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
interface CanonicalTmuxAuthority {
|
|
171
|
+
readonly nativeServerIdentity?: NativeTmuxServerIdentity;
|
|
166
172
|
readonly namedSocketFence: ReturnType<typeof createNamedSocketFence> | null;
|
|
167
173
|
readonly executablePath: string;
|
|
168
174
|
readonly socketSelector: DaemonTmuxSocketSelector;
|
|
@@ -208,6 +214,7 @@ function canonicalAuthority(input: NativeTerminalAttachmentTmuxAuthority): Canon
|
|
|
208
214
|
socketArgv = ["-L", input.socketSelector.name];
|
|
209
215
|
}
|
|
210
216
|
return Object.freeze({
|
|
217
|
+
nativeServerIdentity: input.nativeServerIdentity,
|
|
211
218
|
namedSocketFence: socketSelector.kind === "name" ? (input.namedSocketFence ?? null) : null,
|
|
212
219
|
executablePath,
|
|
213
220
|
socketSelector: Object.freeze(socketSelector),
|
|
@@ -288,9 +295,12 @@ function pinnedRunner(
|
|
|
288
295
|
run(command: TmuxArgvPlan): TmuxAttachmentCommandResult {
|
|
289
296
|
if (command.executable !== "tmux") return { status: "failed" };
|
|
290
297
|
try {
|
|
298
|
+
const guarded = authority.nativeServerIdentity
|
|
299
|
+
? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
|
|
300
|
+
: null;
|
|
291
301
|
const stdout = execute(
|
|
292
302
|
authority.executablePath,
|
|
293
|
-
[...currentSocketArgv(authority), ...command.argv],
|
|
303
|
+
[...currentSocketArgv(authority), ...(guarded?.argv ?? command.argv)],
|
|
294
304
|
{
|
|
295
305
|
cwd: authority.trustedCwd,
|
|
296
306
|
env: authority.environment,
|
|
@@ -298,7 +308,7 @@ function pinnedRunner(
|
|
|
298
308
|
timeoutMs: TERMINAL_ATTACHMENT_TMUX_COMMAND_TIMEOUT_MS,
|
|
299
309
|
},
|
|
300
310
|
);
|
|
301
|
-
const value = String(stdout);
|
|
311
|
+
const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
|
|
302
312
|
if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
|
|
303
313
|
authority.namedSocketFence.resolve();
|
|
304
314
|
if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES) {
|
|
@@ -368,13 +378,16 @@ function pinnedReadRunner(
|
|
|
368
378
|
async run(command: TmuxArgvPlan, signal?: AbortSignal): Promise<TmuxAttachmentCommandResult> {
|
|
369
379
|
if (command.executable !== "tmux" || signal?.aborted) return { status: "failed" };
|
|
370
380
|
try {
|
|
381
|
+
const guarded = authority.nativeServerIdentity
|
|
382
|
+
? fenceNativeTmuxCommand(command.argv, authority.nativeServerIdentity)
|
|
383
|
+
: null;
|
|
371
384
|
const stdout = await execute(
|
|
372
385
|
authority.executablePath,
|
|
373
386
|
[
|
|
374
387
|
...(authority.namedSocketFence
|
|
375
388
|
? await authority.namedSocketFence.resolveAsync(signal)
|
|
376
389
|
: currentSocketArgv(authority)),
|
|
377
|
-
...command.argv,
|
|
390
|
+
...(guarded?.argv ?? command.argv),
|
|
378
391
|
],
|
|
379
392
|
{
|
|
380
393
|
cwd: authority.trustedCwd,
|
|
@@ -384,7 +397,7 @@ function pinnedReadRunner(
|
|
|
384
397
|
...(signal ? { signal } : {}),
|
|
385
398
|
},
|
|
386
399
|
);
|
|
387
|
-
const value = String(stdout);
|
|
400
|
+
const value = guarded ? guarded.verify(String(stdout)) : String(stdout);
|
|
388
401
|
if (authority.namedSocketFence && !authority.namedSocketFence.isPinned())
|
|
389
402
|
await authority.namedSocketFence.resolveAsync(signal);
|
|
390
403
|
if (value.includes("\0") || Buffer.byteLength(value, "utf8") > MAX_TMUX_OUTPUT_BYTES)
|
|
@@ -690,6 +703,7 @@ const PANE_FORMAT = [
|
|
|
690
703
|
// window reports the same value; the catalog requires it before a multi-pane
|
|
691
704
|
// window is attachable.
|
|
692
705
|
"#{@tmux_ide_window_id}",
|
|
706
|
+
"#{window_index}",
|
|
693
707
|
PANE_WIRE_SENTINEL,
|
|
694
708
|
].join(WIRE_SEPARATOR);
|
|
695
709
|
|
|
@@ -742,12 +756,13 @@ type LivePaneFacts = Omit<NativeTerminalInventoryPaneSnapshot, "workspaceName">;
|
|
|
742
756
|
function parsePaneSnapshot(
|
|
743
757
|
stdout: string,
|
|
744
758
|
expected: LiveSessionIdentity,
|
|
745
|
-
): readonly LivePaneFacts[] {
|
|
759
|
+
): { panes: readonly LivePaneFacts[]; linksProof: string } {
|
|
746
760
|
const panes: LivePaneFacts[] = [];
|
|
747
|
-
const runtimeIds = new
|
|
761
|
+
const runtimeIds = new Map<string, { facts: LivePaneFacts; proof: string }>();
|
|
762
|
+
const links = new Map<number, { windowId: string; active: boolean; panes: Set<string> }>();
|
|
748
763
|
for (const line of strictLines(stdout, MAX_DISCOVERED_PANES)) {
|
|
749
764
|
const fields = line.split(WIRE_SEPARATOR);
|
|
750
|
-
if (fields.length !==
|
|
765
|
+
if (fields.length !== 20 || fields[19] !== PANE_WIRE_SENTINEL) {
|
|
751
766
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
752
767
|
}
|
|
753
768
|
const [
|
|
@@ -769,6 +784,7 @@ function parsePaneSnapshot(
|
|
|
769
784
|
missionStamp,
|
|
770
785
|
dir,
|
|
771
786
|
windowStampValue,
|
|
787
|
+
windowIndexValue,
|
|
772
788
|
] = fields as [
|
|
773
789
|
string,
|
|
774
790
|
string,
|
|
@@ -789,22 +805,21 @@ function parsePaneSnapshot(
|
|
|
789
805
|
string,
|
|
790
806
|
string,
|
|
791
807
|
string,
|
|
808
|
+
string,
|
|
792
809
|
];
|
|
793
810
|
if (
|
|
794
811
|
sessionName !== expected.name ||
|
|
795
812
|
sessionId !== expected.id ||
|
|
796
813
|
!RUNTIME_WINDOW_ID.test(windowId) ||
|
|
797
814
|
!RUNTIME_PANE_ID.test(runtimePaneId) ||
|
|
798
|
-
runtimeIds.has(runtimePaneId) ||
|
|
799
815
|
!["0", "1"].includes(windowActive) ||
|
|
800
816
|
!["0", "1"].includes(paneActive)
|
|
801
817
|
) {
|
|
802
818
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
803
819
|
}
|
|
804
|
-
runtimeIds.add(runtimePaneId);
|
|
805
820
|
const nullable = (value: string): string | null =>
|
|
806
821
|
boundedWireValue(value, 256).length === 0 ? null : value;
|
|
807
|
-
|
|
822
|
+
const facts: LivePaneFacts = {
|
|
808
823
|
sessionName,
|
|
809
824
|
sessionId,
|
|
810
825
|
windowId,
|
|
@@ -826,22 +841,58 @@ function parsePaneSnapshot(
|
|
|
826
841
|
// registered workspace remains the trusted application-shell root, so
|
|
827
842
|
// keep discovery available and carry the empty presentation value.
|
|
828
843
|
dir: boundedWireValue(dir, 4_096),
|
|
829
|
-
}
|
|
844
|
+
};
|
|
845
|
+
const windowIndex = nonnegativeInteger(windowIndexValue);
|
|
846
|
+
const link = links.get(windowIndex) ?? {
|
|
847
|
+
windowId,
|
|
848
|
+
active: windowActive === "1",
|
|
849
|
+
panes: new Set<string>(),
|
|
850
|
+
};
|
|
851
|
+
if (
|
|
852
|
+
link.windowId !== windowId ||
|
|
853
|
+
link.active !== (windowActive === "1") ||
|
|
854
|
+
link.panes.has(runtimePaneId)
|
|
855
|
+
) {
|
|
856
|
+
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
857
|
+
}
|
|
858
|
+
link.panes.add(runtimePaneId);
|
|
859
|
+
links.set(windowIndex, link);
|
|
860
|
+
// Only link index and current-link state may differ for a shared backing.
|
|
861
|
+
const proof = JSON.stringify(fields.filter((_value, index) => index !== 10 && index !== 18));
|
|
862
|
+
const previous = runtimeIds.get(runtimePaneId);
|
|
863
|
+
if (previous) {
|
|
864
|
+
if (previous.proof !== proof)
|
|
865
|
+
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
866
|
+
if (facts.active && !previous.facts.active) {
|
|
867
|
+
const merged = { ...previous.facts, active: true };
|
|
868
|
+
panes[panes.indexOf(previous.facts)] = merged;
|
|
869
|
+
previous.facts = merged;
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
runtimeIds.set(runtimePaneId, { facts, proof });
|
|
873
|
+
panes.push(facts);
|
|
874
|
+
}
|
|
830
875
|
}
|
|
831
876
|
const counts = new Map<string, number>();
|
|
832
877
|
for (const pane of panes) counts.set(pane.windowId, (counts.get(pane.windowId) ?? 0) + 1);
|
|
833
|
-
const windows = new Set(panes.map((pane) => pane.windowId));
|
|
834
878
|
if (
|
|
835
879
|
panes.some(
|
|
836
880
|
(pane) =>
|
|
837
881
|
counts.get(pane.windowId) !== pane.windowPaneCount ||
|
|
838
|
-
|
|
882
|
+
links.size !== pane.sessionWindowCount,
|
|
839
883
|
) ||
|
|
884
|
+
[...links.values()].some((link) => link.panes.size !== counts.get(link.windowId)) ||
|
|
885
|
+
[...links.values()].filter((link) => link.active).length > 1 ||
|
|
840
886
|
panes.filter((pane) => pane.active).length > 1
|
|
841
887
|
) {
|
|
842
888
|
throw new NativeTerminalAttachmentRuntimeError("invalid-tmux-output");
|
|
843
889
|
}
|
|
844
|
-
return
|
|
890
|
+
return {
|
|
891
|
+
panes,
|
|
892
|
+
linksProof: JSON.stringify(
|
|
893
|
+
[...links].map(([index, link]) => [index, link.windowId, [...link.panes]]),
|
|
894
|
+
),
|
|
895
|
+
};
|
|
845
896
|
}
|
|
846
897
|
|
|
847
898
|
/**
|
|
@@ -872,10 +923,12 @@ export async function discoverWorkspaceRegistryTerminalInventory(
|
|
|
872
923
|
const argv = ["list-panes", "-s", "-t", identity.id, "-F", PANE_FORMAT] as const;
|
|
873
924
|
const before = await requiredTmuxResult(runner, argv, signal);
|
|
874
925
|
if (before === null) continue;
|
|
875
|
-
const
|
|
926
|
+
const first = parsePaneSnapshot(before, identity);
|
|
927
|
+
const panes = first.panes;
|
|
876
928
|
const after = await requiredTmuxResult(runner, argv, signal);
|
|
877
929
|
if (after === null) throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
878
|
-
const
|
|
930
|
+
const second = parsePaneSnapshot(after, identity);
|
|
931
|
+
const latest = second.panes;
|
|
879
932
|
// Foreground commands, titles, focus and paths can change while the same
|
|
880
933
|
// pane remains valid. Fence binding/topology only, then publish one complete
|
|
881
934
|
// latest snapshot rather than mixing presentation values across reads.
|
|
@@ -891,6 +944,7 @@ export async function discoverWorkspaceRegistryTerminalInventory(
|
|
|
891
944
|
"index",
|
|
892
945
|
] as const;
|
|
893
946
|
if (
|
|
947
|
+
first.linksProof !== second.linksProof ||
|
|
894
948
|
panes.length !== latest.length ||
|
|
895
949
|
panes.some((pane, index) => proofKeys.some((key) => pane[key] !== latest[index]![key]))
|
|
896
950
|
) {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* chance to detach us — killing the reader first can wedge the server.
|
|
21
21
|
*/
|
|
22
22
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
23
|
+
import { shellEscape } from "../../lib/shell.ts";
|
|
24
|
+
import type { NativeTmuxServerIdentity } from "../../lib/tmux-server-generation-runner.ts";
|
|
23
25
|
import { parseControlLine } from "../protocol/control.ts";
|
|
24
26
|
|
|
25
27
|
export interface ControlReply {
|
|
@@ -752,6 +754,7 @@ export class ControlChannelCore {
|
|
|
752
754
|
export interface MirrorControlChannelOptions {
|
|
753
755
|
/** Resolve through the owning daemon's generation fence before each spawn. */
|
|
754
756
|
resolveSocketPath?: () => string;
|
|
757
|
+
nativeServerIdentity?: NativeTmuxServerIdentity;
|
|
755
758
|
session: string;
|
|
756
759
|
handlers: MirrorChannelHandlers;
|
|
757
760
|
/** `tmux -L <name>` — isolated servers in tests; omit for the default. */
|
|
@@ -772,10 +775,23 @@ export interface MirrorControlChannelOptions {
|
|
|
772
775
|
export function mirrorControlAttachArgs(
|
|
773
776
|
options: Pick<
|
|
774
777
|
MirrorControlChannelOptions,
|
|
775
|
-
"session" | "socketName" | "socketPath" | "configFile"
|
|
778
|
+
"session" | "socketName" | "socketPath" | "configFile" | "nativeServerIdentity"
|
|
776
779
|
>,
|
|
777
780
|
pauseAfterSeconds = DEFAULT_PAUSE_AFTER_SECONDS,
|
|
778
781
|
): string[] {
|
|
782
|
+
const attach = [
|
|
783
|
+
"attach",
|
|
784
|
+
"-t",
|
|
785
|
+
options.session,
|
|
786
|
+
"-f",
|
|
787
|
+
`ignore-size,pause-after=${pauseAfterSeconds},active-pane`,
|
|
788
|
+
];
|
|
789
|
+
const expected = options.nativeServerIdentity;
|
|
790
|
+
if (
|
|
791
|
+
expected &&
|
|
792
|
+
(!/^[1-9][0-9]*$/u.test(expected.pid) || !/^[1-9][0-9]*$/u.test(expected.startTime))
|
|
793
|
+
)
|
|
794
|
+
throw new Error("Invalid expected native server identity");
|
|
779
795
|
return [
|
|
780
796
|
...(options.socketPath
|
|
781
797
|
? ["-S", options.socketPath]
|
|
@@ -784,11 +800,15 @@ export function mirrorControlAttachArgs(
|
|
|
784
800
|
: []),
|
|
785
801
|
...(options.configFile ? ["-f", options.configFile] : []),
|
|
786
802
|
"-C",
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
803
|
+
...(expected
|
|
804
|
+
? [
|
|
805
|
+
"if-shell",
|
|
806
|
+
"-F",
|
|
807
|
+
`#{&&:#{==:#{pid},${expected.pid}},#{==:#{start_time},${expected.startTime}}}`,
|
|
808
|
+
attach.map(shellEscape).join(" "),
|
|
809
|
+
"display-message -p 'tmux-server-generation-refused'",
|
|
810
|
+
]
|
|
811
|
+
: attach),
|
|
792
812
|
];
|
|
793
813
|
}
|
|
794
814
|
|
|
@@ -801,6 +821,8 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
801
821
|
private readonly core: ControlChannelCore;
|
|
802
822
|
private readonly opts: MirrorControlChannelOptions;
|
|
803
823
|
private exited = false;
|
|
824
|
+
private verifiedAttach = false;
|
|
825
|
+
private guardedStart: { resolve: () => void; reject: (error: Error) => void } | null = null;
|
|
804
826
|
private atomicCollectorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
805
827
|
|
|
806
828
|
constructor(opts: MirrorControlChannelOptions) {
|
|
@@ -808,6 +830,16 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
808
830
|
this.core = new ControlChannelCore(
|
|
809
831
|
{
|
|
810
832
|
...opts.handlers,
|
|
833
|
+
onOutput: (...args) => {
|
|
834
|
+
if (!opts.nativeServerIdentity || this.verifiedAttach) opts.handlers.onOutput(...args);
|
|
835
|
+
},
|
|
836
|
+
onNotify: (name, rest) => {
|
|
837
|
+
if (opts.nativeServerIdentity && name === "session-changed") {
|
|
838
|
+
this.verifiedAttach = true;
|
|
839
|
+
this.guardedStart?.resolve();
|
|
840
|
+
}
|
|
841
|
+
if (!opts.nativeServerIdentity || this.verifiedAttach) opts.handlers.onNotify(name, rest);
|
|
842
|
+
},
|
|
811
843
|
onExit: (reason) => this.noteExit(reason),
|
|
812
844
|
},
|
|
813
845
|
opts.nowMicros,
|
|
@@ -851,7 +883,31 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
851
883
|
// tmux opens with an unsolicited %begin/%end greeting block; a queued
|
|
852
884
|
// resolver makes start() settle when the protocol is actually live.
|
|
853
885
|
return new Promise((resolve, reject) => {
|
|
854
|
-
|
|
886
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
887
|
+
const settle = (error?: Error) => {
|
|
888
|
+
if (timer) clearTimeout(timer);
|
|
889
|
+
this.guardedStart = null;
|
|
890
|
+
if (error) reject(error);
|
|
891
|
+
else resolve();
|
|
892
|
+
};
|
|
893
|
+
if (this.opts.nativeServerIdentity) {
|
|
894
|
+
this.guardedStart = { resolve: () => settle(), reject: (error) => settle(error) };
|
|
895
|
+
timer = setTimeout(() => {
|
|
896
|
+
settle(new Error("Tmux generation-guarded attach timed out"));
|
|
897
|
+
void this.dispose();
|
|
898
|
+
}, 5_000);
|
|
899
|
+
timer.unref?.();
|
|
900
|
+
}
|
|
901
|
+
// The outer if-shell greeting precedes its attach branch. Only native
|
|
902
|
+
// session-changed proves that the guarded attach actually succeeded.
|
|
903
|
+
this.core.push({
|
|
904
|
+
kind: "promise",
|
|
905
|
+
resolve: () => {
|
|
906
|
+
if (!this.opts.nativeServerIdentity) settle();
|
|
907
|
+
},
|
|
908
|
+
reject: (error) => settle(error),
|
|
909
|
+
lines: [],
|
|
910
|
+
});
|
|
855
911
|
proc.on("error", (err) => {
|
|
856
912
|
this.core.fail(String(err));
|
|
857
913
|
reject(err);
|
|
@@ -1010,6 +1066,7 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
1010
1066
|
private noteExit(reason: string | null): void {
|
|
1011
1067
|
if (this.exited) return;
|
|
1012
1068
|
this.exited = true;
|
|
1069
|
+
this.guardedStart?.reject(new Error(reason ?? "Control channel exited before guarded attach"));
|
|
1013
1070
|
this.opts.handlers.onExit(reason);
|
|
1014
1071
|
}
|
|
1015
1072
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WindowLinkTopology } from "@tmux-ide/contracts";
|
|
1
2
|
import type { NativeGridCapture } from "./native-grid-capture.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Public event contract of the daemon MirrorService (m43 card 1).
|
|
@@ -131,6 +132,7 @@ export interface MirrorLayoutAuthoritySnapshot {
|
|
|
131
132
|
readonly session: string;
|
|
132
133
|
readonly runtimeSessionId: string;
|
|
133
134
|
readonly topologyEpoch: number;
|
|
135
|
+
readonly windowLinks: WindowLinkTopology;
|
|
134
136
|
readonly layouts: readonly MirrorLayoutEvent[];
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { resolveTmuxExecutable, tmuxClientEnvironment } from "../../lib/tmux-client-execution.ts";
|
|
4
|
+
import {
|
|
5
|
+
captureUnixSocketIdentity,
|
|
6
|
+
revalidateUnixSocketIdentity,
|
|
7
|
+
} from "../../lib/unix-socket-authority.ts";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import type { WindowLinkTarget } from "@tmux-ide/contracts";
|
|
1
10
|
/**
|
|
2
11
|
* MirrorService — the daemon-side shared mirror layer (m43 card 1).
|
|
3
12
|
*
|
|
@@ -37,6 +46,7 @@ import {
|
|
|
37
46
|
} from "./control-mode-ownership.ts";
|
|
38
47
|
|
|
39
48
|
export interface MirrorServiceOptions {
|
|
49
|
+
nativeServerIdentity?: import("../../lib/tmux-server-generation-runner.ts").NativeTmuxServerIdentity;
|
|
40
50
|
resolveSocketPath?: () => string;
|
|
41
51
|
/** `tmux -L <name>` for every channel — isolated servers in tests. */
|
|
42
52
|
socketName?: string;
|
|
@@ -284,6 +294,15 @@ export class MirrorService {
|
|
|
284
294
|
};
|
|
285
295
|
}
|
|
286
296
|
|
|
297
|
+
async executeWindowLinkAction(
|
|
298
|
+
session: string,
|
|
299
|
+
request: { action: "select" | "unlink"; target?: WindowLinkTarget; paneId?: string },
|
|
300
|
+
) {
|
|
301
|
+
const entry = this.channels.get(session);
|
|
302
|
+
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
303
|
+
return entry.channel.executeWindowLinkAction(request);
|
|
304
|
+
}
|
|
305
|
+
|
|
287
306
|
/** Synchronous hot input on an already-retained SessionRuntime channel. */
|
|
288
307
|
sendText(
|
|
289
308
|
session: string,
|
|
@@ -413,6 +432,21 @@ export class MirrorService {
|
|
|
413
432
|
);
|
|
414
433
|
let channel: SessionChannel;
|
|
415
434
|
try {
|
|
435
|
+
const initialSocket = this.opts.resolveSocketPath
|
|
436
|
+
? this.opts.resolveSocketPath()
|
|
437
|
+
: this.opts.socketPath;
|
|
438
|
+
const mutationSocketIdentity = initialSocket
|
|
439
|
+
? captureUnixSocketIdentity(initialSocket)
|
|
440
|
+
: null;
|
|
441
|
+
const executable = mutationSocketIdentity
|
|
442
|
+
? realpathSync(this.opts.executable ?? resolveTmuxExecutable())
|
|
443
|
+
: null;
|
|
444
|
+
if (executable) {
|
|
445
|
+
accessSync(executable, constants.X_OK);
|
|
446
|
+
if (!isAbsolute(executable) || !statSync(executable).isFile())
|
|
447
|
+
throw new Error("Invalid pinned tmux executable");
|
|
448
|
+
}
|
|
449
|
+
const environment = Object.freeze(tmuxClientEnvironment(process.env));
|
|
416
450
|
const channelOptions: SessionChannelOptions = {
|
|
417
451
|
session,
|
|
418
452
|
createIo: (handlers) =>
|
|
@@ -424,10 +458,32 @@ export class MirrorService {
|
|
|
424
458
|
socketPath: this.opts.socketPath,
|
|
425
459
|
resolveSocketPath: this.opts.resolveSocketPath,
|
|
426
460
|
executable: this.opts.executable,
|
|
461
|
+
nativeServerIdentity: this.opts.nativeServerIdentity,
|
|
427
462
|
configFile: this.opts.configFile,
|
|
428
463
|
pauseAfterSeconds: this.opts.pauseAfterSeconds,
|
|
429
464
|
nowMicros: this.opts.nowMicros,
|
|
430
465
|
}),
|
|
466
|
+
executeWindowLinkGuard: (args) => {
|
|
467
|
+
if (!mutationSocketIdentity || !executable)
|
|
468
|
+
throw new Error("Window link mutation requires a pinned socket authority");
|
|
469
|
+
if (this.opts.resolveSocketPath) this.opts.resolveSocketPath();
|
|
470
|
+
const socket = revalidateUnixSocketIdentity(mutationSocketIdentity);
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
execFile(
|
|
473
|
+
executable,
|
|
474
|
+
["-N", "-u", "-S", socket, ...args],
|
|
475
|
+
{ timeout: 5000, maxBuffer: 65536, encoding: "utf8", env: environment },
|
|
476
|
+
(error, stdout) => {
|
|
477
|
+
const status = !error
|
|
478
|
+
? 0
|
|
479
|
+
: typeof error.code === "number" && !error.killed
|
|
480
|
+
? error.code
|
|
481
|
+
: null;
|
|
482
|
+
resolve({ status, stdout });
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
},
|
|
431
487
|
historyLines: this.opts.historyLines,
|
|
432
488
|
internalReadHookEmission: this.opts.internalReadHookEmission,
|
|
433
489
|
generatePaneId: this.opts.generatePaneId,
|