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
|
@@ -30,7 +30,7 @@ import { decodeNativeGridCapture, isNativeBootstrapCapture, } from "./native-gri
|
|
|
30
30
|
*/
|
|
31
31
|
import { createHash, randomBytes } from "node:crypto";
|
|
32
32
|
import { hostname } from "node:os";
|
|
33
|
-
import { WORKSPACE_SEMANTIC_PANE_OPTION, WORKSPACE_SEMANTIC_WINDOW_OPTION, WorkspaceIdSchemaZ, } from "@tmux-ide/contracts";
|
|
33
|
+
import { WINDOW_LINK_MAX_LINKS, WORKSPACE_SEMANTIC_PANE_OPTION, WORKSPACE_SEMANTIC_WINDOW_OPTION, WorkspaceIdSchemaZ, } from "@tmux-ide/contracts";
|
|
34
34
|
import { textToHexKeys } from "../protocol/control.js";
|
|
35
35
|
import { InputCoalescer } from "../protocol/input-coalescer.js";
|
|
36
36
|
import { NativeGridCaptureReader } from "./native-grid-reader.js";
|
|
@@ -38,6 +38,9 @@ import { parseLayout, parseLayoutChange, parseSessionWindowChanged, parseWindowP
|
|
|
38
38
|
import { SESSION_PANE_DESCRIPTOR_FORMAT, SessionDescriptorDiscovery, decodeControlReplyUtf8, decodeTmuxArgument, parseSessionPaneDescriptorReply, } from "../protocol/session-descriptor-discovery.js";
|
|
39
39
|
import { resolvePaneDisplayName } from "../protocol/pane-display-name.js";
|
|
40
40
|
import { finalizeWorkspaceTmuxReconciliation, planWorkspaceTmuxReconciliation, } from "../protocol/workspace-tmux-adapter.js";
|
|
41
|
+
import { liveSessionIdForNativeIdentity } from "../protocol/live-session-identity.js";
|
|
42
|
+
import { WindowLinkAuthority, WindowLinkResolutionError } from "./window-link-authority.js";
|
|
43
|
+
import { buildNativeWindowLinkGuard, buildNativeWindowLinkPaneSelectGuard, classifyNativeWindowLinkGuardResult, } from "../../lib/tmux-window-link-guard.js";
|
|
41
44
|
import { FlowLedger } from "./flow-ledger.js";
|
|
42
45
|
import { PaneFeed } from "./pane-feed.js";
|
|
43
46
|
import { INTERNAL_READ_OPERATION_OPTION, registerInternalReadOperation, retireInternalReadOperation, } from "../../lib/tmux-interaction-options.js";
|
|
@@ -165,6 +168,9 @@ export class SessionChannel {
|
|
|
165
168
|
truthActive = new Map();
|
|
166
169
|
truthWindow = new Map();
|
|
167
170
|
currentWindow = "";
|
|
171
|
+
windowLinkAuthority = null;
|
|
172
|
+
latestWindowStage = null;
|
|
173
|
+
attachedServerGeneration = null;
|
|
168
174
|
diagnostics = [];
|
|
169
175
|
degraded = false;
|
|
170
176
|
ageByRuntime = new Map();
|
|
@@ -325,7 +331,13 @@ export class SessionChannel {
|
|
|
325
331
|
? this.trustedInventoryFlight
|
|
326
332
|
: Promise.reject(new Error(`trusted inventory identity changed for ${this.opts.session}`));
|
|
327
333
|
}
|
|
328
|
-
const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
|
|
334
|
+
const flight = this.refreshTrustedInventory(expectedRuntimeSessionId)
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
this.windowLinkAuthority?.invalidate();
|
|
337
|
+
this.latestWindowStage = null;
|
|
338
|
+
throw error;
|
|
339
|
+
})
|
|
340
|
+
.finally(() => {
|
|
329
341
|
if (this.trustedInventoryFlight === flight) {
|
|
330
342
|
this.trustedInventoryFlight = null;
|
|
331
343
|
this.trustedInventoryFlightSessionId = null;
|
|
@@ -344,21 +356,92 @@ export class SessionChannel {
|
|
|
344
356
|
return this.attachedIdentity;
|
|
345
357
|
}
|
|
346
358
|
async captureAttachedSessionIdentity() {
|
|
347
|
-
const lines = await this.io.request(`display-message -p "#{qa:session_name}\t#{session_id}"`);
|
|
359
|
+
const lines = await this.io.request(`display-message -p "#{qa:session_name}\t#{session_id}\t#{pid}\t#{session_created}"`);
|
|
348
360
|
if (lines.length !== 1)
|
|
349
361
|
throw new Error(`mirror session ${this.opts.session} identity is absent`);
|
|
350
362
|
const decodedLine = decodeControlReplyUtf8(lines[0]);
|
|
351
363
|
if (decodedLine === null)
|
|
352
364
|
throw new Error(`mirror session ${this.opts.session} identity is malformed`);
|
|
353
|
-
const [encodedName = "", runtimeSessionId = ""] = decodedLine.split("\t");
|
|
365
|
+
const [encodedName = "", runtimeSessionId = "", serverPid = "", sessionCreated = ""] = decodedLine.split("\t");
|
|
354
366
|
const sessionName = decodeTmuxArgument(encodedName);
|
|
355
367
|
if (sessionName.length === 0 ||
|
|
356
368
|
sessionName.length > 160 ||
|
|
357
369
|
!/^\$(?:0|[1-9][0-9]*)$/u.test(runtimeSessionId) ||
|
|
358
|
-
runtimeSessionId.length > 32
|
|
370
|
+
runtimeSessionId.length > 32 ||
|
|
371
|
+
!/^[1-9][0-9]*$/u.test(serverPid) ||
|
|
372
|
+
!/^[0-9]+$/u.test(sessionCreated)) {
|
|
359
373
|
throw new Error(`mirror session ${this.opts.session} identity is malformed`);
|
|
360
374
|
}
|
|
361
375
|
this.attachedIdentity = Object.freeze({ sessionName, runtimeSessionId });
|
|
376
|
+
this.attachedServerGeneration = { serverPid, sessionCreated };
|
|
377
|
+
if (this.disposed)
|
|
378
|
+
return;
|
|
379
|
+
this.bindWindowLinkAuthority(liveSessionIdForNativeIdentity(serverPid, runtimeSessionId, sessionCreated));
|
|
380
|
+
}
|
|
381
|
+
bindWindowLinkAuthority(liveSessionId) {
|
|
382
|
+
const identity = this.attachedIdentity;
|
|
383
|
+
if (!identity || this.disposed)
|
|
384
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
385
|
+
if (this.windowLinkAuthority?.liveSessionId === liveSessionId)
|
|
386
|
+
return;
|
|
387
|
+
this.windowLinkAuthority?.dispose();
|
|
388
|
+
this.windowLinkAuthority = new WindowLinkAuthority(liveSessionId, identity.runtimeSessionId);
|
|
389
|
+
if (this.latestWindowStage)
|
|
390
|
+
this.windowLinkAuthority.reconcile(this.latestWindowStage.links);
|
|
391
|
+
}
|
|
392
|
+
async executeWindowLinkAction(request) {
|
|
393
|
+
const authority = this.windowLinkAuthority;
|
|
394
|
+
if (!authority || this.disposed)
|
|
395
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
396
|
+
const pane = request.paneId ? this.panesBySemantic.get(request.paneId) : undefined;
|
|
397
|
+
if (request.paneId && !pane)
|
|
398
|
+
throw new WindowLinkResolutionError("window_link_backing_mismatch");
|
|
399
|
+
const semanticWindow = pane?.windowRuntimeId
|
|
400
|
+
? this.windowsByRuntime.get(pane.windowRuntimeId)?.semanticId
|
|
401
|
+
: null;
|
|
402
|
+
const target = request.target ?? (semanticWindow ? authority.uniqueTargetForBacking(semanticWindow) : null);
|
|
403
|
+
if (!target)
|
|
404
|
+
throw new WindowLinkResolutionError("window_link_stale");
|
|
405
|
+
const resolved = authority.resolve(target);
|
|
406
|
+
if (pane && (pane.windowRuntimeId !== resolved.runtimeWindowId || request.action !== "select"))
|
|
407
|
+
throw new WindowLinkResolutionError("window_link_backing_mismatch");
|
|
408
|
+
const address = {
|
|
409
|
+
sessionId: resolved.runtimeSessionId,
|
|
410
|
+
windowIndex: resolved.index,
|
|
411
|
+
expectedWindowId: resolved.runtimeWindowId,
|
|
412
|
+
expectedServerPid: this.attachedServerGeneration.serverPid,
|
|
413
|
+
expectedSessionCreated: this.attachedServerGeneration.sessionCreated,
|
|
414
|
+
};
|
|
415
|
+
const args = pane
|
|
416
|
+
? buildNativeWindowLinkPaneSelectGuard(address, pane.runtimeId)
|
|
417
|
+
: buildNativeWindowLinkGuard(address, request.action);
|
|
418
|
+
let outcome;
|
|
419
|
+
try {
|
|
420
|
+
if (!this.opts.executeWindowLinkGuard)
|
|
421
|
+
throw new Error("Window link executor unavailable");
|
|
422
|
+
const result = await this.opts.executeWindowLinkGuard(args);
|
|
423
|
+
outcome = classifyNativeWindowLinkGuardResult(result.status, result.stdout);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// A lost command boundary can follow a mutation; never retry automatically.
|
|
427
|
+
outcome = "indeterminate";
|
|
428
|
+
}
|
|
429
|
+
let timer;
|
|
430
|
+
const reconciled = await Promise.race([
|
|
431
|
+
this.syncNow().then(() => true, () => false),
|
|
432
|
+
new Promise((resolve) => {
|
|
433
|
+
timer = setTimeout(() => resolve(false), 2000);
|
|
434
|
+
}),
|
|
435
|
+
]).finally(() => {
|
|
436
|
+
if (timer !== undefined)
|
|
437
|
+
clearTimeout(timer);
|
|
438
|
+
});
|
|
439
|
+
if (!reconciled) {
|
|
440
|
+
authority.invalidate();
|
|
441
|
+
this.latestWindowStage = null;
|
|
442
|
+
return { outcome, windowLinks: null };
|
|
443
|
+
}
|
|
444
|
+
return { outcome, windowLinks: authority.snapshot() };
|
|
362
445
|
}
|
|
363
446
|
/** Optional native backing, addressed through the verified semantic binding. */
|
|
364
447
|
captureNativeBacking(semanticPaneId, ownsSubscription = () => true) {
|
|
@@ -494,7 +577,7 @@ export class SessionChannel {
|
|
|
494
577
|
throw error;
|
|
495
578
|
}
|
|
496
579
|
}
|
|
497
|
-
if (
|
|
580
|
+
if (this.latestWindowStage?.links.length !== inventory.panes[0].sessionWindowCount ||
|
|
498
581
|
expectedByWindow.size !== this.windowsByRuntime.size) {
|
|
499
582
|
throw new Error(`authoritative layout for ${this.opts.session} has incomplete windows`);
|
|
500
583
|
}
|
|
@@ -631,6 +714,7 @@ export class SessionChannel {
|
|
|
631
714
|
if (this.disposed)
|
|
632
715
|
return;
|
|
633
716
|
this.disposed = true;
|
|
717
|
+
this.windowLinkAuthority?.dispose();
|
|
634
718
|
this.nativeGrid.dispose();
|
|
635
719
|
this.settleFirstJoin();
|
|
636
720
|
this.cancelSync?.();
|
|
@@ -701,12 +785,13 @@ export class SessionChannel {
|
|
|
701
785
|
this.noteRecoveryOutput(pane, outputOrdinal);
|
|
702
786
|
}
|
|
703
787
|
// ── Seed / reseed (the atomic recipe) ────────────────────────────────────
|
|
704
|
-
reseed(
|
|
788
|
+
reseed(lease, onSettled, deferPublish = false, deadlineAt = this.recoveryNowMs() + RECOVERY_ABSOLUTE_DEADLINE_MS) {
|
|
789
|
+
const { sub } = lease;
|
|
705
790
|
if (sub.closed || sub.frozen || this.disposed) {
|
|
706
791
|
onSettled?.(FAILED_RESEED_RESULT);
|
|
707
792
|
return;
|
|
708
793
|
}
|
|
709
|
-
|
|
794
|
+
lease.cancelRecipe?.();
|
|
710
795
|
const runtime = sub.pane.runtimeId;
|
|
711
796
|
const epoch = sub.feed.beginReseed();
|
|
712
797
|
let settled = false;
|
|
@@ -719,7 +804,7 @@ export class SessionChannel {
|
|
|
719
804
|
return;
|
|
720
805
|
settled = true;
|
|
721
806
|
cancelDeadline?.();
|
|
722
|
-
|
|
807
|
+
lease.cancelRecipe = null;
|
|
723
808
|
onSettled?.(result);
|
|
724
809
|
};
|
|
725
810
|
// Keystroke ordering: pending coalesced input leaves before the probes.
|
|
@@ -732,12 +817,12 @@ export class SessionChannel {
|
|
|
732
817
|
markerRetired = true;
|
|
733
818
|
this.retireInternalReadMarker(runtime, internalReadMarker);
|
|
734
819
|
};
|
|
735
|
-
|
|
820
|
+
lease.cancelRecipe = () => {
|
|
736
821
|
if (settled)
|
|
737
822
|
return;
|
|
738
823
|
settled = true;
|
|
739
824
|
cancelDeadline?.();
|
|
740
|
-
|
|
825
|
+
lease.cancelRecipe = null;
|
|
741
826
|
sub.feed.abort(epoch);
|
|
742
827
|
if (!captureSucceeded)
|
|
743
828
|
retireMarker();
|
|
@@ -770,9 +855,9 @@ export class SessionChannel {
|
|
|
770
855
|
settled = true;
|
|
771
856
|
sub.feed.abort(epoch);
|
|
772
857
|
cancelDeadline?.();
|
|
773
|
-
|
|
858
|
+
lease.cancelRecipe = null;
|
|
774
859
|
retireMarker();
|
|
775
|
-
this.reseed(
|
|
860
|
+
this.reseed(lease, onSettled, deferPublish, deadlineAt);
|
|
776
861
|
return;
|
|
777
862
|
}
|
|
778
863
|
sub.feed.abort(epoch);
|
|
@@ -787,10 +872,10 @@ export class SessionChannel {
|
|
|
787
872
|
retireMarker();
|
|
788
873
|
sub.feed.abort(epoch);
|
|
789
874
|
cancelDeadline?.();
|
|
790
|
-
|
|
875
|
+
lease.cancelRecipe = null;
|
|
791
876
|
sub.nativeBootstrap = false;
|
|
792
877
|
this.nativeBootstrapUnavailable = true;
|
|
793
|
-
this.reseed(
|
|
878
|
+
this.reseed(lease, onSettled, deferPublish, deadlineAt);
|
|
794
879
|
return;
|
|
795
880
|
}
|
|
796
881
|
if (!reply.ok) {
|
|
@@ -864,14 +949,14 @@ export class SessionChannel {
|
|
|
864
949
|
reseedPlain(sub, resumeReason = null) {
|
|
865
950
|
if (sub.closed || sub.frozen || this.disposed)
|
|
866
951
|
return;
|
|
867
|
-
if (this.plainReseedActive === sub)
|
|
952
|
+
if (this.plainReseedActive?.sub === sub)
|
|
868
953
|
sub.cancelCapture?.();
|
|
869
954
|
if (!this.plainReseedQueue.has(sub) && this.plainReseedQueue.size >= MAX_QUEUED_PLAIN_RESEEDS) {
|
|
870
955
|
this.failPlainReseed(sub);
|
|
871
956
|
return;
|
|
872
957
|
}
|
|
873
958
|
this.plainReseedQueue.set(sub, resumeReason);
|
|
874
|
-
if (this.plainReseedActive !== sub) {
|
|
959
|
+
if (this.plainReseedActive?.sub !== sub) {
|
|
875
960
|
// Output before this queued recipe starts is already in its future
|
|
876
961
|
// capture. Do not publish it using the prior geometry in the meantime.
|
|
877
962
|
sub.feed.beginReseed();
|
|
@@ -904,13 +989,26 @@ export class SessionChannel {
|
|
|
904
989
|
this.drainPlainReseeds();
|
|
905
990
|
return;
|
|
906
991
|
}
|
|
907
|
-
|
|
992
|
+
sub.cancelCapture?.();
|
|
993
|
+
const lease = { sub, cancelRecipe: null };
|
|
994
|
+
this.plainReseedActive = lease;
|
|
995
|
+
// The queue lease outlives individual native capability probes. Retrying
|
|
996
|
+
// or falling back replaces only cancelRecipe, never this queue owner.
|
|
997
|
+
sub.cancelCapture = () => {
|
|
998
|
+
if (this.plainReseedActive !== lease)
|
|
999
|
+
return;
|
|
1000
|
+
lease.cancelRecipe?.();
|
|
1001
|
+
sub.cancelCapture = null;
|
|
1002
|
+
this.plainReseedActive = null;
|
|
1003
|
+
this.drainPlainReseeds();
|
|
1004
|
+
};
|
|
908
1005
|
// Only the recipe actually submitted to the control FIFO owns a five
|
|
909
1006
|
// second deadline. A large sibling projection cannot consume the waiting
|
|
910
1007
|
// panes' entire capture budgets before their commands have been written.
|
|
911
|
-
this.reseed(
|
|
912
|
-
if (this.plainReseedActive !==
|
|
1008
|
+
this.reseed(lease, (result) => {
|
|
1009
|
+
if (this.plainReseedActive !== lease)
|
|
913
1010
|
return;
|
|
1011
|
+
sub.cancelCapture = null;
|
|
914
1012
|
this.plainReseedActive = null;
|
|
915
1013
|
if (!result.ok) {
|
|
916
1014
|
// A stalled FIFO cannot make progress for queued recipes either. Retire
|
|
@@ -930,16 +1028,6 @@ export class SessionChannel {
|
|
|
930
1028
|
if (resumeReason && !sub.closed && !sub.frozen)
|
|
931
1029
|
sub.onEvent({ type: "flow", state: "resumed", reason: resumeReason });
|
|
932
1030
|
}, true);
|
|
933
|
-
if (this.plainReseedActive === sub) {
|
|
934
|
-
const cancel = sub.cancelCapture;
|
|
935
|
-
sub.cancelCapture = () => {
|
|
936
|
-
cancel?.();
|
|
937
|
-
this.plainReseedQueue.delete(sub);
|
|
938
|
-
if (this.plainReseedActive === sub)
|
|
939
|
-
this.plainReseedActive = null;
|
|
940
|
-
this.drainPlainReseeds();
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
1031
|
}
|
|
944
1032
|
retireInternalReadMarker(runtime, marker) {
|
|
945
1033
|
if (!/^%(?:0|[1-9][0-9]*)$/u.test(runtime))
|
|
@@ -2014,6 +2102,11 @@ export class SessionChannel {
|
|
|
2014
2102
|
return;
|
|
2015
2103
|
}
|
|
2016
2104
|
if (name === "session-window-changed") {
|
|
2105
|
+
// The notification carries backing identity only; duplicate links require an index read.
|
|
2106
|
+
if (this.windowLinkAuthority) {
|
|
2107
|
+
this.scheduleSync();
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2017
2110
|
const change = parseSessionWindowChanged(rest);
|
|
2018
2111
|
if (!change)
|
|
2019
2112
|
return;
|
|
@@ -2097,6 +2190,9 @@ export class SessionChannel {
|
|
|
2097
2190
|
this.emitLayoutAuthorityTo(subscriber, runtimeSessionId);
|
|
2098
2191
|
}
|
|
2099
2192
|
emitLayoutAuthorityTo(subscriber, runtimeSessionId) {
|
|
2193
|
+
const windowLinks = this.windowLinkAuthority?.snapshot();
|
|
2194
|
+
if (!windowLinks)
|
|
2195
|
+
return;
|
|
2100
2196
|
const layouts = [...this.layoutByWindow.keys()]
|
|
2101
2197
|
.map((runtimeId) => this.layoutEventFor(runtimeId))
|
|
2102
2198
|
.filter((event) => event !== null);
|
|
@@ -2104,6 +2200,7 @@ export class SessionChannel {
|
|
|
2104
2200
|
session: this.opts.session,
|
|
2105
2201
|
runtimeSessionId,
|
|
2106
2202
|
topologyEpoch: this.layoutTopologyEpoch,
|
|
2203
|
+
windowLinks,
|
|
2107
2204
|
layouts,
|
|
2108
2205
|
});
|
|
2109
2206
|
}
|
|
@@ -2207,8 +2304,9 @@ export class SessionChannel {
|
|
|
2207
2304
|
windowActive: windowActive === "1",
|
|
2208
2305
|
});
|
|
2209
2306
|
}
|
|
2307
|
+
const previousCurrentWindow = this.currentWindow;
|
|
2210
2308
|
const { listed, movedWindowRuntimeIds } = this.applyPaneTruth(truth);
|
|
2211
|
-
await this.syncWindows(this.opts.session, movedWindowRuntimeIds);
|
|
2309
|
+
await this.syncWindows(this.opts.session, movedWindowRuntimeIds, previousCurrentWindow);
|
|
2212
2310
|
this.discovery.discover(listed);
|
|
2213
2311
|
}
|
|
2214
2312
|
applyPaneTruth(truth) {
|
|
@@ -2266,7 +2364,20 @@ export class SessionChannel {
|
|
|
2266
2364
|
parsed.descriptors.length !== beforeLines.length) {
|
|
2267
2365
|
throw new Error(`trusted inventory for ${this.opts.session} is malformed`);
|
|
2268
2366
|
}
|
|
2269
|
-
const
|
|
2367
|
+
const descriptorByPane = new Map();
|
|
2368
|
+
for (const row of parsed.descriptors) {
|
|
2369
|
+
const previous = descriptorByPane.get(row.runtimePaneId);
|
|
2370
|
+
if (previous) {
|
|
2371
|
+
const comparable = (value) => JSON.stringify({ ...value, windowActive: false, windowIndex: null });
|
|
2372
|
+
if (comparable(previous) !== comparable(row))
|
|
2373
|
+
throw new Error("Inconsistent linked pane observation");
|
|
2374
|
+
if (row.windowActive)
|
|
2375
|
+
descriptorByPane.set(row.runtimePaneId, row);
|
|
2376
|
+
}
|
|
2377
|
+
else
|
|
2378
|
+
descriptorByPane.set(row.runtimePaneId, row);
|
|
2379
|
+
}
|
|
2380
|
+
const descriptors = [...descriptorByPane.values()];
|
|
2270
2381
|
const runtimePaneIds = new Set(descriptors.map((pane) => pane.runtimePaneId));
|
|
2271
2382
|
const runtimeSessionIds = new Set(descriptors.map((pane) => pane.runtimeSessionId));
|
|
2272
2383
|
const activeWindowIds = new Set(descriptors.filter((pane) => pane.windowActive).map((pane) => pane.windowId));
|
|
@@ -2286,10 +2397,26 @@ export class SessionChannel {
|
|
|
2286
2397
|
computedWindowCounts.set(runtimeWindowId, (computedWindowCounts.get(runtimeWindowId) ?? 0) + 1);
|
|
2287
2398
|
}
|
|
2288
2399
|
if (descriptors.some((pane) => pane.windowPaneCount !== computedWindowCounts.get(pane.windowId) ||
|
|
2289
|
-
pane.sessionWindowCount !==
|
|
2400
|
+
pane.sessionWindowCount !== descriptors[0].sessionWindowCount)) {
|
|
2290
2401
|
throw new Error(`trusted inventory for ${this.opts.session} has incomplete counts`);
|
|
2291
2402
|
}
|
|
2292
2403
|
const windowStage = await this.stageWindows(expectedRuntimeSessionId);
|
|
2404
|
+
const descriptorKeys = new Set();
|
|
2405
|
+
for (const row of parsed.descriptors) {
|
|
2406
|
+
const link = windowStage.links.find((link) => link.index === row.windowIndex);
|
|
2407
|
+
const key = `${row.windowIndex}:${row.runtimePaneId}`;
|
|
2408
|
+
if (!link ||
|
|
2409
|
+
link.runtimeWindowId !== row.windowId ||
|
|
2410
|
+
link.active !== row.windowActive ||
|
|
2411
|
+
descriptorKeys.has(key))
|
|
2412
|
+
throw new Error(`trusted inventory for ${this.opts.session} lacks verified identity: inconsistent linked pane membership`);
|
|
2413
|
+
descriptorKeys.add(key);
|
|
2414
|
+
}
|
|
2415
|
+
for (const link of windowStage.links) {
|
|
2416
|
+
const count = computedWindowCounts.get(link.runtimeWindowId);
|
|
2417
|
+
if (parsed.descriptors.filter((row) => row.windowIndex === link.index).length !== count)
|
|
2418
|
+
throw new Error("Incomplete linked pane membership");
|
|
2419
|
+
}
|
|
2293
2420
|
const repairedPanes = await this.repairTrustedPaneIdentity(descriptors, windowStage.layouts);
|
|
2294
2421
|
const afterLines = await this.io.request(`list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`);
|
|
2295
2422
|
const confirmedWindowStage = await this.stageWindows(expectedRuntimeSessionId);
|
|
@@ -2330,6 +2457,7 @@ export class SessionChannel {
|
|
|
2330
2457
|
break;
|
|
2331
2458
|
}
|
|
2332
2459
|
if (confirmedWindowStage.currentWindow !== activeWindowId ||
|
|
2460
|
+
confirmedWindowStage.links.length !== descriptors[0].sessionWindowCount ||
|
|
2333
2461
|
confirmedWindowStage.windows.size !== computedWindowCounts.size ||
|
|
2334
2462
|
!stagedPaneMembershipExact ||
|
|
2335
2463
|
stagedPaneCount !== descriptors.length ||
|
|
@@ -2367,7 +2495,7 @@ export class SessionChannel {
|
|
|
2367
2495
|
throw new Error(`trusted inventory for ${this.opts.session} is degraded`);
|
|
2368
2496
|
}
|
|
2369
2497
|
const windowCounts = computedWindowCounts;
|
|
2370
|
-
const sessionWindowCount =
|
|
2498
|
+
const sessionWindowCount = confirmedWindowStage.links.length;
|
|
2371
2499
|
const panes = descriptors.map((descriptor) => {
|
|
2372
2500
|
const record = this.panesByRuntime.get(descriptor.runtimePaneId);
|
|
2373
2501
|
const runtimeWindowId = descriptor.windowId;
|
|
@@ -2406,12 +2534,18 @@ export class SessionChannel {
|
|
|
2406
2534
|
panes: Object.freeze(panes),
|
|
2407
2535
|
});
|
|
2408
2536
|
}
|
|
2409
|
-
async syncWindows(target = this.opts.session, requiredLayoutEmits = new Set()) {
|
|
2410
|
-
const stage = await this.stageWindows(target)
|
|
2411
|
-
|
|
2537
|
+
async syncWindows(target = this.opts.session, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow) {
|
|
2538
|
+
const stage = await this.stageWindows(target).catch((error) => {
|
|
2539
|
+
this.windowLinkAuthority?.invalidate();
|
|
2540
|
+
this.latestWindowStage = null;
|
|
2541
|
+
throw error;
|
|
2542
|
+
});
|
|
2543
|
+
this.commitWindowStage(stage, requiredLayoutEmits, previousCurrentWindow);
|
|
2412
2544
|
return stage.repairedIdentity;
|
|
2413
2545
|
}
|
|
2414
2546
|
commitWindowStage(stage, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow) {
|
|
2547
|
+
this.windowLinkAuthority?.reconcile(stage.links);
|
|
2548
|
+
this.latestWindowStage = stage;
|
|
2415
2549
|
const changedWindows = new Set();
|
|
2416
2550
|
for (const [runtimeId, record] of stage.windows) {
|
|
2417
2551
|
const previous = this.windowsByRuntime.get(runtimeId);
|
|
@@ -2468,7 +2602,8 @@ export class SessionChannel {
|
|
|
2468
2602
|
this.emitLayoutAuthority();
|
|
2469
2603
|
}
|
|
2470
2604
|
windowStagesEqual(left, right) {
|
|
2471
|
-
if (left.
|
|
2605
|
+
if (JSON.stringify(left.links) !== JSON.stringify(right.links) ||
|
|
2606
|
+
left.currentWindow !== right.currentWindow ||
|
|
2472
2607
|
left.windows.size !== right.windows.size ||
|
|
2473
2608
|
left.layouts.size !== right.layouts.size) {
|
|
2474
2609
|
return false;
|
|
@@ -2504,29 +2639,35 @@ export class SessionChannel {
|
|
|
2504
2639
|
return true;
|
|
2505
2640
|
}
|
|
2506
2641
|
async stageWindows(target = this.opts.session) {
|
|
2507
|
-
const lines = await this.io.request(`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}"`);
|
|
2642
|
+
const lines = await this.io.request(`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}"`);
|
|
2643
|
+
if (lines.length > WINDOW_LINK_MAX_LINKS)
|
|
2644
|
+
throw new Error("Window link observation exceeds bound");
|
|
2508
2645
|
const rows = [];
|
|
2509
|
-
const
|
|
2646
|
+
const seenIndexes = new Set();
|
|
2510
2647
|
for (const raw of lines) {
|
|
2511
2648
|
// Replies are latin1 byte strings; recover UTF-8 window names first.
|
|
2512
2649
|
const line = Buffer.from(raw, "latin1").toString("utf8");
|
|
2513
2650
|
const parts = line.split("\t");
|
|
2514
|
-
if (parts.length !==
|
|
2651
|
+
if (parts.length !== 10) {
|
|
2515
2652
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
2516
2653
|
}
|
|
2517
|
-
const [runtimeId = "", stampRaw = "", nameRaw = "", active = "", visible = "", zoomed = "", borderStatus = "off", full = visible, modeKeys,] = parts;
|
|
2654
|
+
const [runtimeId = "", stampRaw = "", nameRaw = "", active = "", visible = "", zoomed = "", borderStatus = "off", full = visible, modeKeys, indexRaw = "",] = parts;
|
|
2655
|
+
const index = Number(indexRaw);
|
|
2518
2656
|
if (!/^@[0-9]+$/u.test(runtimeId) ||
|
|
2519
|
-
|
|
2657
|
+
!/^(?:0|[1-9][0-9]*)$/u.test(indexRaw) ||
|
|
2658
|
+
!Number.isSafeInteger(index) ||
|
|
2659
|
+
seenIndexes.has(index) ||
|
|
2520
2660
|
(active !== "0" && active !== "1") ||
|
|
2521
2661
|
(zoomed !== "0" && zoomed !== "1") ||
|
|
2522
2662
|
(borderStatus !== "top" && borderStatus !== "bottom" && borderStatus !== "off") ||
|
|
2523
2663
|
(modeKeys !== undefined && modeKeys !== "emacs" && modeKeys !== "vi")) {
|
|
2524
2664
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
2525
2665
|
}
|
|
2526
|
-
|
|
2666
|
+
seenIndexes.add(index);
|
|
2527
2667
|
const stamp = decodeTmuxArgument(stampRaw);
|
|
2528
2668
|
const name = decodeTmuxArgument(nameRaw);
|
|
2529
2669
|
rows.push({
|
|
2670
|
+
index,
|
|
2530
2671
|
runtimeId,
|
|
2531
2672
|
stamp: stamp.length > 0 ? stamp : null,
|
|
2532
2673
|
name: name.length > 0 ? name : null,
|
|
@@ -2545,8 +2686,22 @@ export class SessionChannel {
|
|
|
2545
2686
|
if (activeRows.length !== 1) {
|
|
2546
2687
|
throw new Error(`window layout truth for ${this.opts.session} has inconsistent active window`);
|
|
2547
2688
|
}
|
|
2548
|
-
const
|
|
2689
|
+
const backingRows = new Map();
|
|
2549
2690
|
for (const row of rows) {
|
|
2691
|
+
const previous = backingRows.get(row.runtimeId);
|
|
2692
|
+
if (previous &&
|
|
2693
|
+
(previous.stamp !== row.stamp ||
|
|
2694
|
+
previous.name !== row.name ||
|
|
2695
|
+
previous.visible !== row.visible ||
|
|
2696
|
+
previous.full !== row.full ||
|
|
2697
|
+
previous.zoomed !== row.zoomed ||
|
|
2698
|
+
previous.paneBorderStatus !== row.paneBorderStatus ||
|
|
2699
|
+
previous.modeKeys !== row.modeKeys))
|
|
2700
|
+
throw new Error("Inconsistent linked backing observation");
|
|
2701
|
+
backingRows.set(row.runtimeId, row);
|
|
2702
|
+
}
|
|
2703
|
+
const nextLayoutByWindow = new Map();
|
|
2704
|
+
for (const row of backingRows.values()) {
|
|
2550
2705
|
const parsed = parseLayout(row.visible);
|
|
2551
2706
|
if (!parsed) {
|
|
2552
2707
|
throw new Error(`window layout truth for ${this.opts.session} is malformed`);
|
|
@@ -2559,7 +2714,7 @@ export class SessionChannel {
|
|
|
2559
2714
|
// Valid unique stamps are identity; missing/invalid/duplicated stamps are
|
|
2560
2715
|
// ALL regenerated and stamped back (the pane policy, applied to windows).
|
|
2561
2716
|
const stampCounts = new Map();
|
|
2562
|
-
for (const row of
|
|
2717
|
+
for (const row of backingRows.values()) {
|
|
2563
2718
|
if (row.stamp && WorkspaceIdSchemaZ.safeParse(row.stamp).success) {
|
|
2564
2719
|
stampCounts.set(row.stamp, (stampCounts.get(row.stamp) ?? 0) + 1);
|
|
2565
2720
|
}
|
|
@@ -2568,10 +2723,8 @@ export class SessionChannel {
|
|
|
2568
2723
|
const generateWindowId = this.opts.generateWindowId ?? defaultMirrorWindowId;
|
|
2569
2724
|
let repairedIdentity = false;
|
|
2570
2725
|
const next = new Map();
|
|
2571
|
-
|
|
2572
|
-
for (const row of
|
|
2573
|
-
if (row.active)
|
|
2574
|
-
nextCurrentWindow = row.runtimeId;
|
|
2726
|
+
const nextCurrentWindow = activeRows[0].runtimeId;
|
|
2727
|
+
for (const row of backingRows.values()) {
|
|
2575
2728
|
let semanticId = null;
|
|
2576
2729
|
if (row.stamp && stampCounts.get(row.stamp) === 1) {
|
|
2577
2730
|
semanticId = row.stamp;
|
|
@@ -2614,6 +2767,12 @@ export class SessionChannel {
|
|
|
2614
2767
|
layouts: nextLayoutByWindow,
|
|
2615
2768
|
currentWindow: nextCurrentWindow,
|
|
2616
2769
|
repairedIdentity,
|
|
2770
|
+
links: rows.map((row) => ({
|
|
2771
|
+
index: row.index,
|
|
2772
|
+
runtimeWindowId: row.runtimeId,
|
|
2773
|
+
semanticWindowId: next.get(row.runtimeId).semanticId ?? "",
|
|
2774
|
+
active: row.active,
|
|
2775
|
+
})),
|
|
2617
2776
|
};
|
|
2618
2777
|
}
|
|
2619
2778
|
async reconcileIdentity(descriptors, listed) {
|
|
@@ -2804,6 +2963,7 @@ export class SessionChannel {
|
|
|
2804
2963
|
onChannelExit() {
|
|
2805
2964
|
if (this.disposed)
|
|
2806
2965
|
return;
|
|
2966
|
+
this.windowLinkAuthority?.dispose();
|
|
2807
2967
|
this.nativeGrid.dispose();
|
|
2808
2968
|
this.settleFirstJoin();
|
|
2809
2969
|
for (const runtime of [...this.recoveries.keys()])
|