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
|
@@ -35,10 +35,13 @@ import {
|
|
|
35
35
|
import { createHash, randomBytes } from "node:crypto";
|
|
36
36
|
import { hostname } from "node:os";
|
|
37
37
|
import {
|
|
38
|
+
WINDOW_LINK_MAX_LINKS,
|
|
38
39
|
WORKSPACE_SEMANTIC_PANE_OPTION,
|
|
39
40
|
WORKSPACE_SEMANTIC_WINDOW_OPTION,
|
|
40
41
|
WorkspaceIdSchemaZ,
|
|
41
42
|
type WorkspacePaneRect,
|
|
43
|
+
type WindowLinkTarget,
|
|
44
|
+
type WindowLinkTopology,
|
|
42
45
|
} from "@tmux-ide/contracts";
|
|
43
46
|
import { textToHexKeys } from "../protocol/control.ts";
|
|
44
47
|
import { InputCoalescer } from "../protocol/input-coalescer.ts";
|
|
@@ -81,6 +84,13 @@ import type {
|
|
|
81
84
|
MirrorPaneEvent,
|
|
82
85
|
MirrorSessionDescription,
|
|
83
86
|
} from "./events.ts";
|
|
87
|
+
import { liveSessionIdForNativeIdentity } from "../protocol/live-session-identity.ts";
|
|
88
|
+
import { WindowLinkAuthority, WindowLinkResolutionError } from "./window-link-authority.ts";
|
|
89
|
+
import {
|
|
90
|
+
buildNativeWindowLinkGuard,
|
|
91
|
+
buildNativeWindowLinkPaneSelectGuard,
|
|
92
|
+
classifyNativeWindowLinkGuardResult,
|
|
93
|
+
} from "../../lib/tmux-window-link-guard.ts";
|
|
84
94
|
import { FlowLedger } from "./flow-ledger.ts";
|
|
85
95
|
import { PaneFeed } from "./pane-feed.ts";
|
|
86
96
|
import type {
|
|
@@ -217,6 +227,7 @@ export interface MirrorFlowRecoveryObservation {
|
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
export interface SessionChannelOptions {
|
|
230
|
+
executeWindowLinkGuard?: (args: string[]) => Promise<{ status: number | null; stdout: string }>;
|
|
220
231
|
session: string;
|
|
221
232
|
createIo: (handlers: MirrorChannelHandlers) => MirrorChannelIo;
|
|
222
233
|
/** Explicit capture tail override. Omitted captures all retained native history. */
|
|
@@ -279,6 +290,11 @@ interface SubRecord {
|
|
|
279
290
|
closed: boolean;
|
|
280
291
|
}
|
|
281
292
|
|
|
293
|
+
interface PlainReseedLease {
|
|
294
|
+
readonly sub: SubRecord;
|
|
295
|
+
cancelRecipe: (() => void) | null;
|
|
296
|
+
}
|
|
297
|
+
|
|
282
298
|
interface PaneRecord {
|
|
283
299
|
historySize?: number;
|
|
284
300
|
scrollOnClear?: boolean;
|
|
@@ -395,6 +411,12 @@ interface WindowSyncStage {
|
|
|
395
411
|
readonly layouts: Map<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>;
|
|
396
412
|
readonly currentWindow: string;
|
|
397
413
|
readonly repairedIdentity: boolean;
|
|
414
|
+
readonly links: readonly {
|
|
415
|
+
index: number;
|
|
416
|
+
runtimeWindowId: string;
|
|
417
|
+
semanticWindowId: string;
|
|
418
|
+
active: boolean;
|
|
419
|
+
}[];
|
|
398
420
|
}
|
|
399
421
|
|
|
400
422
|
export function defaultMirrorPaneId(): string {
|
|
@@ -429,6 +451,9 @@ export class SessionChannel {
|
|
|
429
451
|
private readonly truthActive = new Map<string, boolean>();
|
|
430
452
|
private readonly truthWindow = new Map<string, string>();
|
|
431
453
|
private currentWindow = "";
|
|
454
|
+
private windowLinkAuthority: WindowLinkAuthority | null = null;
|
|
455
|
+
private latestWindowStage: WindowSyncStage | null = null;
|
|
456
|
+
private attachedServerGeneration: { serverPid: string; sessionCreated: string } | null = null;
|
|
432
457
|
private diagnostics: MirrorDiagnostic[] = [];
|
|
433
458
|
private degraded = false;
|
|
434
459
|
private readonly ageByRuntime = new Map<string, number>();
|
|
@@ -443,7 +468,7 @@ export class SessionChannel {
|
|
|
443
468
|
private windowAuthorityOrdinal = 0;
|
|
444
469
|
private paneIncarnation = 0;
|
|
445
470
|
private recoveryOrdinal = 0;
|
|
446
|
-
private plainReseedActive:
|
|
471
|
+
private plainReseedActive: PlainReseedLease | null = null;
|
|
447
472
|
private readonly plainReseedQueue = new Map<SubRecord, "requested" | null>();
|
|
448
473
|
private readonly outputOrdinals = new Map<string, number>();
|
|
449
474
|
private readonly pendingLayoutOutput = new Map<
|
|
@@ -629,12 +654,18 @@ export class SessionChannel {
|
|
|
629
654
|
? this.trustedInventoryFlight
|
|
630
655
|
: Promise.reject(new Error(`trusted inventory identity changed for ${this.opts.session}`));
|
|
631
656
|
}
|
|
632
|
-
const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
|
|
633
|
-
|
|
634
|
-
this.
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
657
|
+
const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
|
|
658
|
+
.catch((error) => {
|
|
659
|
+
this.windowLinkAuthority?.invalidate();
|
|
660
|
+
this.latestWindowStage = null;
|
|
661
|
+
throw error;
|
|
662
|
+
})
|
|
663
|
+
.finally(() => {
|
|
664
|
+
if (this.trustedInventoryFlight === flight) {
|
|
665
|
+
this.trustedInventoryFlight = null;
|
|
666
|
+
this.trustedInventoryFlightSessionId = null;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
638
669
|
this.trustedInventoryFlight = flight;
|
|
639
670
|
this.trustedInventoryFlightSessionId = expectedRuntimeSessionId;
|
|
640
671
|
return flight;
|
|
@@ -649,23 +680,103 @@ export class SessionChannel {
|
|
|
649
680
|
}
|
|
650
681
|
|
|
651
682
|
private async captureAttachedSessionIdentity(): Promise<void> {
|
|
652
|
-
const lines = await this.io.request(
|
|
683
|
+
const lines = await this.io.request(
|
|
684
|
+
`display-message -p "#{qa:session_name}\t#{session_id}\t#{pid}\t#{session_created}"`,
|
|
685
|
+
);
|
|
653
686
|
if (lines.length !== 1)
|
|
654
687
|
throw new Error(`mirror session ${this.opts.session} identity is absent`);
|
|
655
688
|
const decodedLine = decodeControlReplyUtf8(lines[0]!);
|
|
656
689
|
if (decodedLine === null)
|
|
657
690
|
throw new Error(`mirror session ${this.opts.session} identity is malformed`);
|
|
658
|
-
const [encodedName = "", runtimeSessionId = ""
|
|
691
|
+
const [encodedName = "", runtimeSessionId = "", serverPid = "", sessionCreated = ""] =
|
|
692
|
+
decodedLine.split("\t");
|
|
659
693
|
const sessionName = decodeTmuxArgument(encodedName);
|
|
660
694
|
if (
|
|
661
695
|
sessionName.length === 0 ||
|
|
662
696
|
sessionName.length > 160 ||
|
|
663
697
|
!/^\$(?:0|[1-9][0-9]*)$/u.test(runtimeSessionId) ||
|
|
664
|
-
runtimeSessionId.length > 32
|
|
698
|
+
runtimeSessionId.length > 32 ||
|
|
699
|
+
!/^[1-9][0-9]*$/u.test(serverPid) ||
|
|
700
|
+
!/^[0-9]+$/u.test(sessionCreated)
|
|
665
701
|
) {
|
|
666
702
|
throw new Error(`mirror session ${this.opts.session} identity is malformed`);
|
|
667
703
|
}
|
|
668
704
|
this.attachedIdentity = Object.freeze({ sessionName, runtimeSessionId });
|
|
705
|
+
this.attachedServerGeneration = { serverPid, sessionCreated };
|
|
706
|
+
if (this.disposed) return;
|
|
707
|
+
this.bindWindowLinkAuthority(
|
|
708
|
+
liveSessionIdForNativeIdentity(serverPid, runtimeSessionId, sessionCreated),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private bindWindowLinkAuthority(liveSessionId: string): void {
|
|
713
|
+
const identity = this.attachedIdentity;
|
|
714
|
+
if (!identity || this.disposed) throw new WindowLinkResolutionError("window_link_stale");
|
|
715
|
+
if (this.windowLinkAuthority?.liveSessionId === liveSessionId) return;
|
|
716
|
+
this.windowLinkAuthority?.dispose();
|
|
717
|
+
this.windowLinkAuthority = new WindowLinkAuthority(liveSessionId, identity.runtimeSessionId);
|
|
718
|
+
if (this.latestWindowStage) this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async executeWindowLinkAction(request: {
|
|
722
|
+
action: "select" | "unlink";
|
|
723
|
+
target?: WindowLinkTarget;
|
|
724
|
+
paneId?: string;
|
|
725
|
+
}): Promise<{
|
|
726
|
+
outcome: "applied" | "stale" | "native-refused" | "indeterminate";
|
|
727
|
+
windowLinks: WindowLinkTopology | null;
|
|
728
|
+
}> {
|
|
729
|
+
const authority = this.windowLinkAuthority;
|
|
730
|
+
if (!authority || this.disposed) throw new WindowLinkResolutionError("window_link_stale");
|
|
731
|
+
const pane = request.paneId ? this.panesBySemantic.get(request.paneId) : undefined;
|
|
732
|
+
if (request.paneId && !pane)
|
|
733
|
+
throw new WindowLinkResolutionError("window_link_backing_mismatch");
|
|
734
|
+
const semanticWindow = pane?.windowRuntimeId
|
|
735
|
+
? this.windowsByRuntime.get(pane.windowRuntimeId)?.semanticId
|
|
736
|
+
: null;
|
|
737
|
+
const target =
|
|
738
|
+
request.target ?? (semanticWindow ? authority.uniqueTargetForBacking(semanticWindow) : null);
|
|
739
|
+
if (!target) throw new WindowLinkResolutionError("window_link_stale");
|
|
740
|
+
const resolved = authority.resolve(target);
|
|
741
|
+
if (pane && (pane.windowRuntimeId !== resolved.runtimeWindowId || request.action !== "select"))
|
|
742
|
+
throw new WindowLinkResolutionError("window_link_backing_mismatch");
|
|
743
|
+
const address = {
|
|
744
|
+
sessionId: resolved.runtimeSessionId,
|
|
745
|
+
windowIndex: resolved.index,
|
|
746
|
+
expectedWindowId: resolved.runtimeWindowId,
|
|
747
|
+
expectedServerPid: this.attachedServerGeneration!.serverPid,
|
|
748
|
+
expectedSessionCreated: this.attachedServerGeneration!.sessionCreated,
|
|
749
|
+
};
|
|
750
|
+
const args = pane
|
|
751
|
+
? buildNativeWindowLinkPaneSelectGuard(address, pane.runtimeId)
|
|
752
|
+
: buildNativeWindowLinkGuard(address, request.action);
|
|
753
|
+
let outcome: "applied" | "stale" | "native-refused" | "indeterminate";
|
|
754
|
+
try {
|
|
755
|
+
if (!this.opts.executeWindowLinkGuard) throw new Error("Window link executor unavailable");
|
|
756
|
+
const result = await this.opts.executeWindowLinkGuard(args);
|
|
757
|
+
outcome = classifyNativeWindowLinkGuardResult(result.status, result.stdout);
|
|
758
|
+
} catch {
|
|
759
|
+
// A lost command boundary can follow a mutation; never retry automatically.
|
|
760
|
+
outcome = "indeterminate";
|
|
761
|
+
}
|
|
762
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
763
|
+
const reconciled = await Promise.race([
|
|
764
|
+
this.syncNow().then(
|
|
765
|
+
() => true,
|
|
766
|
+
() => false,
|
|
767
|
+
),
|
|
768
|
+
new Promise<false>((resolve) => {
|
|
769
|
+
timer = setTimeout(() => resolve(false), 2000);
|
|
770
|
+
}),
|
|
771
|
+
]).finally(() => {
|
|
772
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
773
|
+
});
|
|
774
|
+
if (!reconciled) {
|
|
775
|
+
authority.invalidate();
|
|
776
|
+
this.latestWindowStage = null;
|
|
777
|
+
return { outcome, windowLinks: null };
|
|
778
|
+
}
|
|
779
|
+
return { outcome, windowLinks: authority.snapshot() };
|
|
669
780
|
}
|
|
670
781
|
|
|
671
782
|
/** Optional native backing, addressed through the verified semantic binding. */
|
|
@@ -829,7 +940,7 @@ export class SessionChannel {
|
|
|
829
940
|
}
|
|
830
941
|
}
|
|
831
942
|
if (
|
|
832
|
-
|
|
943
|
+
this.latestWindowStage?.links.length !== inventory.panes[0]!.sessionWindowCount ||
|
|
833
944
|
expectedByWindow.size !== this.windowsByRuntime.size
|
|
834
945
|
) {
|
|
835
946
|
throw new Error(`authoritative layout for ${this.opts.session} has incomplete windows`);
|
|
@@ -981,6 +1092,7 @@ export class SessionChannel {
|
|
|
981
1092
|
async dispose(): Promise<void> {
|
|
982
1093
|
if (this.disposed) return;
|
|
983
1094
|
this.disposed = true;
|
|
1095
|
+
this.windowLinkAuthority?.dispose();
|
|
984
1096
|
this.nativeGrid.dispose();
|
|
985
1097
|
this.settleFirstJoin();
|
|
986
1098
|
this.cancelSync?.();
|
|
@@ -1052,16 +1164,17 @@ export class SessionChannel {
|
|
|
1052
1164
|
// ── Seed / reseed (the atomic recipe) ────────────────────────────────────
|
|
1053
1165
|
|
|
1054
1166
|
private reseed(
|
|
1055
|
-
|
|
1167
|
+
lease: PlainReseedLease,
|
|
1056
1168
|
onSettled?: (result: ReseedResult) => void,
|
|
1057
1169
|
deferPublish = false,
|
|
1058
1170
|
deadlineAt = this.recoveryNowMs() + RECOVERY_ABSOLUTE_DEADLINE_MS,
|
|
1059
1171
|
): void {
|
|
1172
|
+
const { sub } = lease;
|
|
1060
1173
|
if (sub.closed || sub.frozen || this.disposed) {
|
|
1061
1174
|
onSettled?.(FAILED_RESEED_RESULT);
|
|
1062
1175
|
return;
|
|
1063
1176
|
}
|
|
1064
|
-
|
|
1177
|
+
lease.cancelRecipe?.();
|
|
1065
1178
|
const runtime = sub.pane.runtimeId;
|
|
1066
1179
|
const epoch = sub.feed.beginReseed();
|
|
1067
1180
|
let settled = false;
|
|
@@ -1073,7 +1186,7 @@ export class SessionChannel {
|
|
|
1073
1186
|
if (settled) return;
|
|
1074
1187
|
settled = true;
|
|
1075
1188
|
cancelDeadline?.();
|
|
1076
|
-
|
|
1189
|
+
lease.cancelRecipe = null;
|
|
1077
1190
|
onSettled?.(result);
|
|
1078
1191
|
};
|
|
1079
1192
|
// Keystroke ordering: pending coalesced input leaves before the probes.
|
|
@@ -1085,11 +1198,11 @@ export class SessionChannel {
|
|
|
1085
1198
|
markerRetired = true;
|
|
1086
1199
|
this.retireInternalReadMarker(runtime, internalReadMarker);
|
|
1087
1200
|
};
|
|
1088
|
-
|
|
1201
|
+
lease.cancelRecipe = () => {
|
|
1089
1202
|
if (settled) return;
|
|
1090
1203
|
settled = true;
|
|
1091
1204
|
cancelDeadline?.();
|
|
1092
|
-
|
|
1205
|
+
lease.cancelRecipe = null;
|
|
1093
1206
|
sub.feed.abort(epoch);
|
|
1094
1207
|
if (!captureSucceeded) retireMarker();
|
|
1095
1208
|
};
|
|
@@ -1130,9 +1243,9 @@ export class SessionChannel {
|
|
|
1130
1243
|
settled = true;
|
|
1131
1244
|
sub.feed.abort(epoch);
|
|
1132
1245
|
cancelDeadline?.();
|
|
1133
|
-
|
|
1246
|
+
lease.cancelRecipe = null;
|
|
1134
1247
|
retireMarker();
|
|
1135
|
-
this.reseed(
|
|
1248
|
+
this.reseed(lease, onSettled, deferPublish, deadlineAt);
|
|
1136
1249
|
return;
|
|
1137
1250
|
}
|
|
1138
1251
|
sub.feed.abort(epoch);
|
|
@@ -1147,10 +1260,10 @@ export class SessionChannel {
|
|
|
1147
1260
|
retireMarker();
|
|
1148
1261
|
sub.feed.abort(epoch);
|
|
1149
1262
|
cancelDeadline?.();
|
|
1150
|
-
|
|
1263
|
+
lease.cancelRecipe = null;
|
|
1151
1264
|
sub.nativeBootstrap = false;
|
|
1152
1265
|
this.nativeBootstrapUnavailable = true;
|
|
1153
|
-
this.reseed(
|
|
1266
|
+
this.reseed(lease, onSettled, deferPublish, deadlineAt);
|
|
1154
1267
|
return;
|
|
1155
1268
|
}
|
|
1156
1269
|
if (!reply.ok) {
|
|
@@ -1219,13 +1332,13 @@ export class SessionChannel {
|
|
|
1219
1332
|
|
|
1220
1333
|
private reseedPlain(sub: SubRecord, resumeReason: "requested" | null = null): void {
|
|
1221
1334
|
if (sub.closed || sub.frozen || this.disposed) return;
|
|
1222
|
-
if (this.plainReseedActive === sub) sub.cancelCapture?.();
|
|
1335
|
+
if (this.plainReseedActive?.sub === sub) sub.cancelCapture?.();
|
|
1223
1336
|
if (!this.plainReseedQueue.has(sub) && this.plainReseedQueue.size >= MAX_QUEUED_PLAIN_RESEEDS) {
|
|
1224
1337
|
this.failPlainReseed(sub);
|
|
1225
1338
|
return;
|
|
1226
1339
|
}
|
|
1227
1340
|
this.plainReseedQueue.set(sub, resumeReason);
|
|
1228
|
-
if (this.plainReseedActive !== sub) {
|
|
1341
|
+
if (this.plainReseedActive?.sub !== sub) {
|
|
1229
1342
|
// Output before this queued recipe starts is already in its future
|
|
1230
1343
|
// capture. Do not publish it using the prior geometry in the meantime.
|
|
1231
1344
|
sub.feed.beginReseed();
|
|
@@ -1260,14 +1373,26 @@ export class SessionChannel {
|
|
|
1260
1373
|
this.drainPlainReseeds();
|
|
1261
1374
|
return;
|
|
1262
1375
|
}
|
|
1263
|
-
|
|
1376
|
+
sub.cancelCapture?.();
|
|
1377
|
+
const lease: PlainReseedLease = { sub, cancelRecipe: null };
|
|
1378
|
+
this.plainReseedActive = lease;
|
|
1379
|
+
// The queue lease outlives individual native capability probes. Retrying
|
|
1380
|
+
// or falling back replaces only cancelRecipe, never this queue owner.
|
|
1381
|
+
sub.cancelCapture = () => {
|
|
1382
|
+
if (this.plainReseedActive !== lease) return;
|
|
1383
|
+
lease.cancelRecipe?.();
|
|
1384
|
+
sub.cancelCapture = null;
|
|
1385
|
+
this.plainReseedActive = null;
|
|
1386
|
+
this.drainPlainReseeds();
|
|
1387
|
+
};
|
|
1264
1388
|
// Only the recipe actually submitted to the control FIFO owns a five
|
|
1265
1389
|
// second deadline. A large sibling projection cannot consume the waiting
|
|
1266
1390
|
// panes' entire capture budgets before their commands have been written.
|
|
1267
1391
|
this.reseed(
|
|
1268
|
-
|
|
1392
|
+
lease,
|
|
1269
1393
|
(result) => {
|
|
1270
|
-
if (this.plainReseedActive !==
|
|
1394
|
+
if (this.plainReseedActive !== lease) return;
|
|
1395
|
+
sub.cancelCapture = null;
|
|
1271
1396
|
this.plainReseedActive = null;
|
|
1272
1397
|
if (!result.ok) {
|
|
1273
1398
|
// A stalled FIFO cannot make progress for queued recipes either. Retire
|
|
@@ -1287,15 +1412,6 @@ export class SessionChannel {
|
|
|
1287
1412
|
},
|
|
1288
1413
|
true,
|
|
1289
1414
|
);
|
|
1290
|
-
if (this.plainReseedActive === sub) {
|
|
1291
|
-
const cancel = sub.cancelCapture;
|
|
1292
|
-
sub.cancelCapture = () => {
|
|
1293
|
-
cancel?.();
|
|
1294
|
-
this.plainReseedQueue.delete(sub);
|
|
1295
|
-
if (this.plainReseedActive === sub) this.plainReseedActive = null;
|
|
1296
|
-
this.drainPlainReseeds();
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
1415
|
}
|
|
1300
1416
|
|
|
1301
1417
|
private retireInternalReadMarker(runtime: string, marker: string): void {
|
|
@@ -2492,6 +2608,11 @@ export class SessionChannel {
|
|
|
2492
2608
|
return;
|
|
2493
2609
|
}
|
|
2494
2610
|
if (name === "session-window-changed") {
|
|
2611
|
+
// The notification carries backing identity only; duplicate links require an index read.
|
|
2612
|
+
if (this.windowLinkAuthority) {
|
|
2613
|
+
this.scheduleSync();
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2495
2616
|
const change = parseSessionWindowChanged(rest);
|
|
2496
2617
|
if (!change) return;
|
|
2497
2618
|
const previous = this.currentWindow;
|
|
@@ -2574,6 +2695,8 @@ export class SessionChannel {
|
|
|
2574
2695
|
subscriber: (snapshot: MirrorLayoutAuthoritySnapshot) => void,
|
|
2575
2696
|
runtimeSessionId: string,
|
|
2576
2697
|
): void {
|
|
2698
|
+
const windowLinks = this.windowLinkAuthority?.snapshot();
|
|
2699
|
+
if (!windowLinks) return;
|
|
2577
2700
|
const layouts = [...this.layoutByWindow.keys()]
|
|
2578
2701
|
.map((runtimeId) => this.layoutEventFor(runtimeId))
|
|
2579
2702
|
.filter((event): event is MirrorLayoutEvent => event !== null);
|
|
@@ -2581,6 +2704,7 @@ export class SessionChannel {
|
|
|
2581
2704
|
session: this.opts.session,
|
|
2582
2705
|
runtimeSessionId,
|
|
2583
2706
|
topologyEpoch: this.layoutTopologyEpoch,
|
|
2707
|
+
windowLinks,
|
|
2584
2708
|
layouts,
|
|
2585
2709
|
});
|
|
2586
2710
|
}
|
|
@@ -2694,8 +2818,9 @@ export class SessionChannel {
|
|
|
2694
2818
|
windowActive: windowActive === "1",
|
|
2695
2819
|
});
|
|
2696
2820
|
}
|
|
2821
|
+
const previousCurrentWindow = this.currentWindow;
|
|
2697
2822
|
const { listed, movedWindowRuntimeIds } = this.applyPaneTruth(truth);
|
|
2698
|
-
await this.syncWindows(this.opts.session, movedWindowRuntimeIds);
|
|
2823
|
+
await this.syncWindows(this.opts.session, movedWindowRuntimeIds, previousCurrentWindow);
|
|
2699
2824
|
this.discovery.discover(listed);
|
|
2700
2825
|
}
|
|
2701
2826
|
|
|
@@ -2766,7 +2891,18 @@ export class SessionChannel {
|
|
|
2766
2891
|
) {
|
|
2767
2892
|
throw new Error(`trusted inventory for ${this.opts.session} is malformed`);
|
|
2768
2893
|
}
|
|
2769
|
-
const
|
|
2894
|
+
const descriptorByPane = new Map<string, SessionPaneDescriptor>();
|
|
2895
|
+
for (const row of parsed.descriptors) {
|
|
2896
|
+
const previous = descriptorByPane.get(row.runtimePaneId);
|
|
2897
|
+
if (previous) {
|
|
2898
|
+
const comparable = (value: SessionPaneDescriptor) =>
|
|
2899
|
+
JSON.stringify({ ...value, windowActive: false, windowIndex: null });
|
|
2900
|
+
if (comparable(previous) !== comparable(row))
|
|
2901
|
+
throw new Error("Inconsistent linked pane observation");
|
|
2902
|
+
if (row.windowActive) descriptorByPane.set(row.runtimePaneId, row);
|
|
2903
|
+
} else descriptorByPane.set(row.runtimePaneId, row);
|
|
2904
|
+
}
|
|
2905
|
+
const descriptors = [...descriptorByPane.values()];
|
|
2770
2906
|
const runtimePaneIds = new Set(descriptors.map((pane) => pane.runtimePaneId));
|
|
2771
2907
|
const runtimeSessionIds = new Set(descriptors.map((pane) => pane.runtimeSessionId));
|
|
2772
2908
|
const activeWindowIds = new Set(
|
|
@@ -2796,12 +2932,32 @@ export class SessionChannel {
|
|
|
2796
2932
|
descriptors.some(
|
|
2797
2933
|
(pane) =>
|
|
2798
2934
|
pane.windowPaneCount !== computedWindowCounts.get(pane.windowId!) ||
|
|
2799
|
-
pane.sessionWindowCount !==
|
|
2935
|
+
pane.sessionWindowCount !== descriptors[0]!.sessionWindowCount,
|
|
2800
2936
|
)
|
|
2801
2937
|
) {
|
|
2802
2938
|
throw new Error(`trusted inventory for ${this.opts.session} has incomplete counts`);
|
|
2803
2939
|
}
|
|
2804
2940
|
const windowStage = await this.stageWindows(expectedRuntimeSessionId);
|
|
2941
|
+
const descriptorKeys = new Set<string>();
|
|
2942
|
+
for (const row of parsed.descriptors) {
|
|
2943
|
+
const link = windowStage.links.find((link) => link.index === row.windowIndex);
|
|
2944
|
+
const key = `${row.windowIndex}:${row.runtimePaneId}`;
|
|
2945
|
+
if (
|
|
2946
|
+
!link ||
|
|
2947
|
+
link.runtimeWindowId !== row.windowId ||
|
|
2948
|
+
link.active !== row.windowActive ||
|
|
2949
|
+
descriptorKeys.has(key)
|
|
2950
|
+
)
|
|
2951
|
+
throw new Error(
|
|
2952
|
+
`trusted inventory for ${this.opts.session} lacks verified identity: inconsistent linked pane membership`,
|
|
2953
|
+
);
|
|
2954
|
+
descriptorKeys.add(key);
|
|
2955
|
+
}
|
|
2956
|
+
for (const link of windowStage.links) {
|
|
2957
|
+
const count = computedWindowCounts.get(link.runtimeWindowId)!;
|
|
2958
|
+
if (parsed.descriptors.filter((row) => row.windowIndex === link.index).length !== count)
|
|
2959
|
+
throw new Error("Incomplete linked pane membership");
|
|
2960
|
+
}
|
|
2805
2961
|
const repairedPanes = await this.repairTrustedPaneIdentity(descriptors, windowStage.layouts);
|
|
2806
2962
|
const afterLines = await this.io.request(
|
|
2807
2963
|
`list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`,
|
|
@@ -2848,6 +3004,7 @@ export class SessionChannel {
|
|
|
2848
3004
|
}
|
|
2849
3005
|
if (
|
|
2850
3006
|
confirmedWindowStage.currentWindow !== activeWindowId ||
|
|
3007
|
+
confirmedWindowStage.links.length !== descriptors[0]!.sessionWindowCount ||
|
|
2851
3008
|
confirmedWindowStage.windows.size !== computedWindowCounts.size ||
|
|
2852
3009
|
!stagedPaneMembershipExact ||
|
|
2853
3010
|
stagedPaneCount !== descriptors.length ||
|
|
@@ -2896,7 +3053,7 @@ export class SessionChannel {
|
|
|
2896
3053
|
throw new Error(`trusted inventory for ${this.opts.session} is degraded`);
|
|
2897
3054
|
}
|
|
2898
3055
|
const windowCounts = computedWindowCounts;
|
|
2899
|
-
const sessionWindowCount =
|
|
3056
|
+
const sessionWindowCount = confirmedWindowStage.links.length;
|
|
2900
3057
|
const panes: TrustedMirrorPaneInventory[] = descriptors.map((descriptor) => {
|
|
2901
3058
|
const record = this.panesByRuntime.get(descriptor.runtimePaneId);
|
|
2902
3059
|
const runtimeWindowId = descriptor.windowId!;
|
|
@@ -2941,9 +3098,14 @@ export class SessionChannel {
|
|
|
2941
3098
|
private async syncWindows(
|
|
2942
3099
|
target = this.opts.session,
|
|
2943
3100
|
requiredLayoutEmits: ReadonlySet<string> = new Set(),
|
|
3101
|
+
previousCurrentWindow = this.currentWindow,
|
|
2944
3102
|
): Promise<boolean> {
|
|
2945
|
-
const stage = await this.stageWindows(target)
|
|
2946
|
-
|
|
3103
|
+
const stage = await this.stageWindows(target).catch((error) => {
|
|
3104
|
+
this.windowLinkAuthority?.invalidate();
|
|
3105
|
+
this.latestWindowStage = null;
|
|
3106
|
+
throw error;
|
|
3107
|
+
});
|
|
3108
|
+
this.commitWindowStage(stage, requiredLayoutEmits, previousCurrentWindow);
|
|
2947
3109
|
return stage.repairedIdentity;
|
|
2948
3110
|
}
|
|
2949
3111
|
|
|
@@ -2952,6 +3114,8 @@ export class SessionChannel {
|
|
|
2952
3114
|
requiredLayoutEmits: ReadonlySet<string> = new Set(),
|
|
2953
3115
|
previousCurrentWindow = this.currentWindow,
|
|
2954
3116
|
): void {
|
|
3117
|
+
this.windowLinkAuthority?.reconcile(stage.links);
|
|
3118
|
+
this.latestWindowStage = stage;
|
|
2955
3119
|
const changedWindows = new Set<string>();
|
|
2956
3120
|
for (const [runtimeId, record] of stage.windows) {
|
|
2957
3121
|
const previous = this.windowsByRuntime.get(runtimeId);
|
|
@@ -3011,6 +3175,7 @@ export class SessionChannel {
|
|
|
3011
3175
|
|
|
3012
3176
|
private windowStagesEqual(left: WindowSyncStage, right: WindowSyncStage): boolean {
|
|
3013
3177
|
if (
|
|
3178
|
+
JSON.stringify(left.links) !== JSON.stringify(right.links) ||
|
|
3014
3179
|
left.currentWindow !== right.currentWindow ||
|
|
3015
3180
|
left.windows.size !== right.windows.size ||
|
|
3016
3181
|
left.layouts.size !== right.layouts.size
|
|
@@ -3054,9 +3219,12 @@ export class SessionChannel {
|
|
|
3054
3219
|
|
|
3055
3220
|
private async stageWindows(target = this.opts.session): Promise<WindowSyncStage> {
|
|
3056
3221
|
const lines = await this.io.request(
|
|
3057
|
-
`list-windows -t "${target}" -F "#{window_id}\t#{qa:@tmux_ide_window_id}\t#{qa:window_name}\t#{window_active}\t#{window_visible_layout}\t#{?window_zoomed_flag,1,0}\t#{pane-border-status}\t#{window_layout}\t#{mode-keys}"`,
|
|
3222
|
+
`list-windows -t "${target}" -F "#{window_id}\t#{qa:@tmux_ide_window_id}\t#{qa:window_name}\t#{window_active}\t#{window_visible_layout}\t#{?window_zoomed_flag,1,0}\t#{pane-border-status}\t#{window_layout}\t#{mode-keys}\t#{window_index}"`,
|
|
3058
3223
|
);
|
|
3224
|
+
if (lines.length > WINDOW_LINK_MAX_LINKS)
|
|
3225
|
+
throw new Error("Window link observation exceeds bound");
|
|
3059
3226
|
interface Row {
|
|
3227
|
+
index: number;
|
|
3060
3228
|
runtimeId: string;
|
|
3061
3229
|
stamp: string | null;
|
|
3062
3230
|
name: string | null;
|
|
@@ -3068,12 +3236,12 @@ export class SessionChannel {
|
|
|
3068
3236
|
modeKeys?: "emacs" | "vi";
|
|
3069
3237
|
}
|
|
3070
3238
|
const rows: Row[] = [];
|
|
3071
|
-
const
|
|
3239
|
+
const seenIndexes = new Set<number>();
|
|
3072
3240
|
for (const raw of lines) {
|
|
3073
3241
|
// Replies are latin1 byte strings; recover UTF-8 window names first.
|
|
3074
3242
|
const line = Buffer.from(raw, "latin1").toString("utf8");
|
|
3075
3243
|
const parts = line.split("\t");
|
|
3076
|
-
if (parts.length !==
|
|
3244
|
+
if (parts.length !== 10) {
|
|
3077
3245
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
3078
3246
|
}
|
|
3079
3247
|
const [
|
|
@@ -3086,10 +3254,14 @@ export class SessionChannel {
|
|
|
3086
3254
|
borderStatus = "off",
|
|
3087
3255
|
full = visible,
|
|
3088
3256
|
modeKeys,
|
|
3257
|
+
indexRaw = "",
|
|
3089
3258
|
] = parts;
|
|
3259
|
+
const index = Number(indexRaw);
|
|
3090
3260
|
if (
|
|
3091
3261
|
!/^@[0-9]+$/u.test(runtimeId) ||
|
|
3092
|
-
|
|
3262
|
+
!/^(?:0|[1-9][0-9]*)$/u.test(indexRaw) ||
|
|
3263
|
+
!Number.isSafeInteger(index) ||
|
|
3264
|
+
seenIndexes.has(index) ||
|
|
3093
3265
|
(active !== "0" && active !== "1") ||
|
|
3094
3266
|
(zoomed !== "0" && zoomed !== "1") ||
|
|
3095
3267
|
(borderStatus !== "top" && borderStatus !== "bottom" && borderStatus !== "off") ||
|
|
@@ -3097,10 +3269,11 @@ export class SessionChannel {
|
|
|
3097
3269
|
) {
|
|
3098
3270
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
3099
3271
|
}
|
|
3100
|
-
|
|
3272
|
+
seenIndexes.add(index);
|
|
3101
3273
|
const stamp = decodeTmuxArgument(stampRaw);
|
|
3102
3274
|
const name = decodeTmuxArgument(nameRaw);
|
|
3103
3275
|
rows.push({
|
|
3276
|
+
index,
|
|
3104
3277
|
runtimeId,
|
|
3105
3278
|
stamp: stamp.length > 0 ? stamp : null,
|
|
3106
3279
|
name: name.length > 0 ? name : null,
|
|
@@ -3121,11 +3294,27 @@ export class SessionChannel {
|
|
|
3121
3294
|
`window layout truth for ${this.opts.session} has inconsistent active window`,
|
|
3122
3295
|
);
|
|
3123
3296
|
}
|
|
3297
|
+
const backingRows = new Map<string, Row>();
|
|
3298
|
+
for (const row of rows) {
|
|
3299
|
+
const previous = backingRows.get(row.runtimeId);
|
|
3300
|
+
if (
|
|
3301
|
+
previous &&
|
|
3302
|
+
(previous.stamp !== row.stamp ||
|
|
3303
|
+
previous.name !== row.name ||
|
|
3304
|
+
previous.visible !== row.visible ||
|
|
3305
|
+
previous.full !== row.full ||
|
|
3306
|
+
previous.zoomed !== row.zoomed ||
|
|
3307
|
+
previous.paneBorderStatus !== row.paneBorderStatus ||
|
|
3308
|
+
previous.modeKeys !== row.modeKeys)
|
|
3309
|
+
)
|
|
3310
|
+
throw new Error("Inconsistent linked backing observation");
|
|
3311
|
+
backingRows.set(row.runtimeId, row);
|
|
3312
|
+
}
|
|
3124
3313
|
const nextLayoutByWindow = new Map<
|
|
3125
3314
|
string,
|
|
3126
3315
|
ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }
|
|
3127
3316
|
>();
|
|
3128
|
-
for (const row of
|
|
3317
|
+
for (const row of backingRows.values()) {
|
|
3129
3318
|
const parsed = parseLayout(row.visible);
|
|
3130
3319
|
if (!parsed) {
|
|
3131
3320
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
@@ -3137,7 +3326,7 @@ export class SessionChannel {
|
|
|
3137
3326
|
// Valid unique stamps are identity; missing/invalid/duplicated stamps are
|
|
3138
3327
|
// ALL regenerated and stamped back (the pane policy, applied to windows).
|
|
3139
3328
|
const stampCounts = new Map<string, number>();
|
|
3140
|
-
for (const row of
|
|
3329
|
+
for (const row of backingRows.values()) {
|
|
3141
3330
|
if (row.stamp && WorkspaceIdSchemaZ.safeParse(row.stamp).success) {
|
|
3142
3331
|
stampCounts.set(row.stamp, (stampCounts.get(row.stamp) ?? 0) + 1);
|
|
3143
3332
|
}
|
|
@@ -3146,9 +3335,8 @@ export class SessionChannel {
|
|
|
3146
3335
|
const generateWindowId = this.opts.generateWindowId ?? defaultMirrorWindowId;
|
|
3147
3336
|
let repairedIdentity = false;
|
|
3148
3337
|
const next = new Map<string, WindowRecord>();
|
|
3149
|
-
|
|
3150
|
-
for (const row of
|
|
3151
|
-
if (row.active) nextCurrentWindow = row.runtimeId;
|
|
3338
|
+
const nextCurrentWindow = activeRows[0]!.runtimeId;
|
|
3339
|
+
for (const row of backingRows.values()) {
|
|
3152
3340
|
let semanticId: string | null = null;
|
|
3153
3341
|
if (row.stamp && stampCounts.get(row.stamp) === 1) {
|
|
3154
3342
|
semanticId = row.stamp;
|
|
@@ -3194,6 +3382,12 @@ export class SessionChannel {
|
|
|
3194
3382
|
layouts: nextLayoutByWindow,
|
|
3195
3383
|
currentWindow: nextCurrentWindow,
|
|
3196
3384
|
repairedIdentity,
|
|
3385
|
+
links: rows.map((row) => ({
|
|
3386
|
+
index: row.index,
|
|
3387
|
+
runtimeWindowId: row.runtimeId,
|
|
3388
|
+
semanticWindowId: next.get(row.runtimeId)!.semanticId ?? "",
|
|
3389
|
+
active: row.active,
|
|
3390
|
+
})),
|
|
3197
3391
|
};
|
|
3198
3392
|
}
|
|
3199
3393
|
|
|
@@ -3410,6 +3604,7 @@ export class SessionChannel {
|
|
|
3410
3604
|
|
|
3411
3605
|
private onChannelExit(): void {
|
|
3412
3606
|
if (this.disposed) return;
|
|
3607
|
+
this.windowLinkAuthority?.dispose();
|
|
3413
3608
|
this.nativeGrid.dispose();
|
|
3414
3609
|
this.settleFirstJoin();
|
|
3415
3610
|
for (const runtime of [...this.recoveries.keys()]) this.cancelRecovery(runtime);
|