tmux-ide 2.9.2 → 2.9.4
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/LICENSE +1 -1
- package/README.md +121 -140
- package/bin/cli.js +2457 -727
- package/package.json +23 -8
- package/packages/contracts/src/desktop-host.ts +3 -0
- package/packages/contracts/src/saved-machines.ts +19 -0
- package/packages/contracts/src/terminal-replica.ts +1 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/terminal-delivery.ts +51 -3
- package/packages/core/src/terminal-fnv64-wasm.ts +4 -3
- package/packages/core/src/terminal-replica-hash-cache.ts +129 -31
- package/packages/core/src/terminal-replica-owned-row.ts +27 -0
- package/packages/core/src/terminal-replica.ts +13 -28
- package/packages/daemon/dist/command-center/daemon-fleet-facts-observer.js +96 -10
- package/packages/daemon/dist/command-center/resources/saved-machine-route.js +40 -1
- package/packages/daemon/dist/command-center/ws-events.js +5 -24
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +8 -0
- package/packages/daemon/dist/lib/canonical-daemon.js +28 -2
- package/packages/daemon/dist/lib/daemon-embed.js +1 -1
- package/packages/daemon/dist/lib/daemon-service-manager.js +122 -0
- package/packages/daemon/dist/lib/daemon-service-plan.js +127 -0
- package/packages/daemon/dist/lib/daemon-service.js +259 -0
- package/packages/daemon/dist/lib/install-origin.js +2 -0
- package/packages/daemon/dist/lib/local-fleet-request.js +9 -3
- package/packages/daemon/dist/lib/log.js +6 -10
- package/packages/daemon/dist/lib/managed-installer-update.js +67 -0
- package/packages/daemon/dist/lib/{tui-download-lock.js → private-operation-lock.js} +11 -11
- package/packages/daemon/dist/lib/remote-daemon-info.js +3 -2
- package/packages/daemon/dist/lib/remote-daemon-lifecycle.js +4 -4
- package/packages/daemon/dist/lib/remote-tmux-command.js +10 -0
- package/packages/daemon/dist/lib/runtime-namespace.js +28 -16
- package/packages/daemon/dist/lib/saved-machines.js +40 -2
- package/packages/daemon/dist/lib/ssh-daemon-transport.js +24 -9
- package/packages/daemon/dist/lib/tmux-server-owner.js +1 -1
- package/packages/daemon/dist/lib/tmux-server-registration.js +21 -3
- package/packages/daemon/dist/lib/tui-binary.js +2 -2
- package/packages/daemon/dist/lib/update.js +18 -0
- package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +24 -1
- package/packages/daemon/dist/machines.js +40 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/terminal/attachments/direct-websocket.js +4 -1
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +22 -13
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +101 -3
- package/packages/daemon/dist/terminal/mirror/registered-window-guard.js +62 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +75 -4
- package/packages/daemon/dist/terminal/mirror/shared-window-index.js +45 -0
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +11 -1
- package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +1 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +56 -7
- package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
- package/packages/daemon/dist/tui/integrations/claude-agent-teams.js +264 -0
- package/packages/daemon/dist/tui/mirror/dialog-model.js +2 -35
- package/packages/daemon/dist/tui/mirror/runtime/application-daemon-authority-owner.js +8 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +20 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-authority.js +59 -9
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +27 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +23 -18
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-startup.js +31 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +7 -3
- package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +146 -2
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +201 -50
- package/packages/daemon/dist/tui/mirror/runtime/bounded-performance-record-writer.js +50 -10
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +6 -5
- package/packages/daemon/dist/tui/mirror/runtime/terminal-pane-input-router.js +3 -0
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +8 -2
- package/packages/daemon/dist/tui/mirror/terminal-text.js +35 -0
- package/packages/daemon/dist/tui/mirror/workspace/connection-feedback.js +5 -3
- package/packages/daemon/src/command-center/daemon-fleet-facts-observer.ts +100 -10
- package/packages/daemon/src/command-center/resources/saved-machine-route.ts +48 -1
- package/packages/daemon/src/command-center/ws-events.ts +5 -25
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +12 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +44 -2
- package/packages/daemon/src/lib/daemon-embed.ts +1 -0
- package/packages/daemon/src/lib/daemon-service-manager.ts +152 -0
- package/packages/daemon/src/lib/daemon-service-plan.ts +153 -0
- package/packages/daemon/src/lib/daemon-service.ts +301 -0
- package/packages/daemon/src/lib/install-origin.ts +5 -0
- package/packages/daemon/src/lib/local-fleet-request.ts +19 -3
- package/packages/daemon/src/lib/log.ts +13 -9
- package/packages/daemon/src/lib/managed-installer-update.ts +79 -0
- package/packages/daemon/src/lib/{tui-download-lock.ts → private-operation-lock.ts} +15 -11
- package/packages/daemon/src/lib/remote-daemon-info.ts +3 -3
- package/packages/daemon/src/lib/remote-daemon-lifecycle.ts +4 -4
- package/packages/daemon/src/lib/remote-tmux-command.ts +12 -0
- package/packages/daemon/src/lib/runtime-namespace.ts +27 -18
- package/packages/daemon/src/lib/saved-machines.ts +40 -1
- package/packages/daemon/src/lib/ssh-daemon-transport.ts +25 -7
- package/packages/daemon/src/lib/tmux-server-owner.ts +1 -0
- package/packages/daemon/src/lib/tmux-server-registration.ts +21 -3
- package/packages/daemon/src/lib/tui-binary.ts +2 -2
- package/packages/daemon/src/lib/update.ts +24 -0
- package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +22 -0
- package/packages/daemon/src/machines.ts +53 -5
- package/packages/daemon/src/terminal/attachments/direct-websocket.ts +8 -2
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +105 -3
- package/packages/daemon/src/terminal/mirror/registered-window-guard.ts +66 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +92 -4
- package/packages/daemon/src/terminal/mirror/shared-window-index.ts +42 -0
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +12 -1
- package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +2 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +70 -8
- package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +3 -1
- package/packages/daemon/src/tui/integrations/claude-agent-teams.ts +334 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +2 -32
- package/packages/daemon/src/tui/mirror/runtime/application-daemon-authority-owner.ts +8 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +19 -2
- package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-machine-authority.ts +60 -11
- package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +39 -33
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +31 -23
- package/packages/daemon/src/tui/mirror/runtime/application-machine-startup.ts +38 -2
- package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +7 -4
- package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +3 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -1
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +47 -46
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +165 -2
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +207 -53
- package/packages/daemon/src/tui/mirror/runtime/bounded-performance-record-writer.ts +49 -9
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +6 -5
- package/packages/daemon/src/tui/mirror/runtime/terminal-pane-input-router.ts +4 -0
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +8 -3
- package/packages/daemon/src/tui/mirror/terminal-text.ts +32 -0
- package/packages/daemon/src/tui/mirror/workspace/connection-feedback.ts +6 -3
- package/packages/daemon-client/src/fleet-connection-status.ts +10 -1
- package/packages/daemon-client/src/workspace-client.ts +4 -1
- package/skill/SKILL.md +30 -30
- package/packages/contracts/src/__tests__/actions-contract.test.ts +0 -119
- package/packages/contracts/src/__tests__/agent-graph-overlay.test.ts +0 -428
- package/packages/contracts/src/__tests__/app-window-mutation.test.ts +0 -51
- package/packages/contracts/src/__tests__/app-window-state.test.ts +0 -34
- package/packages/contracts/src/__tests__/application-shell.test.ts +0 -645
- package/packages/contracts/src/__tests__/automation-operations.test.ts +0 -78
- package/packages/contracts/src/__tests__/causal-cell.test.ts +0 -47
- package/packages/contracts/src/__tests__/client-view-state.test.ts +0 -115
- package/packages/contracts/src/__tests__/cohesion-fixture.test.ts +0 -106
- package/packages/contracts/src/__tests__/commands.test.ts +0 -86
- package/packages/contracts/src/__tests__/control.test.ts +0 -154
- package/packages/contracts/src/__tests__/daemon-events.test.ts +0 -405
- package/packages/contracts/src/__tests__/daemon-resources.test.ts +0 -143
- package/packages/contracts/src/__tests__/daemon-wire.test.ts +0 -149
- package/packages/contracts/src/__tests__/desktop-host.test.ts +0 -269
- package/packages/contracts/src/__tests__/desktop-missions.test.ts +0 -215
- package/packages/contracts/src/__tests__/experience-conformance.test.ts +0 -153
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +0 -113
- package/packages/contracts/src/__tests__/experience-shell.test.ts +0 -65
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v2.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v3.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/daemon-wire-v4.json +0 -35
- package/packages/contracts/src/__tests__/fixtures/scoped-receipt.ts +0 -62
- package/packages/contracts/src/__tests__/fleet-agent-graph.test.ts +0 -231
- package/packages/contracts/src/__tests__/fleet-catalog.test.ts +0 -270
- package/packages/contracts/src/__tests__/fleet-lifecycle.test.ts +0 -62
- package/packages/contracts/src/__tests__/focus-overlay.test.ts +0 -110
- package/packages/contracts/src/__tests__/ide-config.test.ts +0 -64
- package/packages/contracts/src/__tests__/interaction-evidence.test.ts +0 -288
- package/packages/contracts/src/__tests__/interaction-receipt-v2.test.ts +0 -72
- package/packages/contracts/src/__tests__/issue-error.test.ts +0 -78
- package/packages/contracts/src/__tests__/mission-projections.test.ts +0 -186
- package/packages/contracts/src/__tests__/missions.test.ts +0 -272
- package/packages/contracts/src/__tests__/multiplexer-verbs.test.ts +0 -242
- package/packages/contracts/src/__tests__/pane-appearance.test.ts +0 -146
- package/packages/contracts/src/__tests__/pane-read-result.test.ts +0 -55
- package/packages/contracts/src/__tests__/pane-stream.test.ts +0 -536
- package/packages/contracts/src/__tests__/pane-widget-marker.test.ts +0 -327
- package/packages/contracts/src/__tests__/performance-metrics.test.ts +0 -73
- package/packages/contracts/src/__tests__/performance-qualification.test.ts +0 -137
- package/packages/contracts/src/__tests__/rich-card-widget.test.ts +0 -29
- package/packages/contracts/src/__tests__/session-runtime.test.ts +0 -512
- package/packages/contracts/src/__tests__/startup-readiness.test.ts +0 -409
- package/packages/contracts/src/__tests__/terminal-attachment-stream.test.ts +0 -72
- package/packages/contracts/src/__tests__/terminal-attachments.test.ts +0 -329
- package/packages/contracts/src/__tests__/terminal-delivery.test.ts +0 -88
- package/packages/contracts/src/__tests__/terminal-runtime-inventory.test.ts +0 -68
- package/packages/contracts/src/__tests__/terminals.test.ts +0 -87
- package/packages/contracts/src/__tests__/tmux-server-scope.test.ts +0 -166
- package/packages/contracts/src/__tests__/transport-contract-loading.test.ts +0 -91
- package/packages/contracts/src/__tests__/visual-tokens.test.ts +0 -224
- package/packages/contracts/src/__tests__/window-links.test.ts +0 -243
- package/packages/contracts/src/__tests__/workspace-changes-resource.test.ts +0 -317
- package/packages/contracts/src/__tests__/workspace-changes-view.test.ts +0 -122
- package/packages/contracts/src/__tests__/workspace-config.test.ts +0 -294
- package/packages/contracts/src/__tests__/workspace-files-resource.test.ts +0 -261
- package/packages/contracts/src/__tests__/workspace-files-tree.test.ts +0 -147
- package/packages/contracts/src/__tests__/workspace-pane-creation.test.ts +0 -249
- package/packages/contracts/src/__tests__/workspace-promotion.test.ts +0 -94
- package/packages/contracts/src/__tests__/workspace-resource-identity.test.ts +0 -90
- package/packages/contracts/src/daemon-resource-request.test.ts +0 -192
- package/packages/contracts/src/native-operation-identity.test.ts +0 -23
- package/packages/contracts/src/workspace-admission.test.ts +0 -21
- package/packages/contracts/src/workspace-catalog-resource.test.ts +0 -146
- package/packages/core/src/agent-provisioning.test.ts +0 -42
- package/packages/core/src/application-shell-session.test.ts +0 -105
- package/packages/core/src/interaction-evidence.test.ts +0 -134
- package/packages/core/src/interaction-flow.test.ts +0 -66
- package/packages/core/src/interaction-receipts.test.ts +0 -671
- package/packages/core/src/navigator.test.ts +0 -34
- package/packages/core/src/optimistic-projection-conformance.test.ts +0 -75
- package/packages/core/src/optimistic-projection.test.ts +0 -118
- package/packages/core/src/performance-metrics.test.ts +0 -121
- package/packages/core/src/performance-qualification.test.ts +0 -283
- package/packages/core/src/saved-machines.test.ts +0 -80
- package/packages/core/src/terminal-compact-cold.test.ts +0 -87
- package/packages/core/src/terminal-conformance.test.ts +0 -48
- package/packages/core/src/terminal-delivery-cache-lifetime.test.ts +0 -167
- package/packages/core/src/terminal-delivery-encode-cooperative.test.ts +0 -140
- package/packages/core/src/terminal-delivery-representation-hash.test.ts +0 -77
- package/packages/core/src/terminal-delivery.test.ts +0 -1471
- package/packages/core/src/terminal-fnv64-wasm.test.ts +0 -61
- package/packages/core/src/terminal-replica-cold.test.ts +0 -31
- package/packages/core/src/terminal-replica-cooperative.test.ts +0 -170
- package/packages/core/src/terminal-replica-hash-cache.test.ts +0 -283
- package/packages/core/src/terminal-replica.test.ts +0 -347
- package/packages/core/src/workspace-selection.test.ts +0 -63
- package/packages/daemon/dist/__tests__/support.js +0 -33
- package/packages/daemon/dist/lib/__tests__/fixtures/workspace-state-writer.js +0 -58
- package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +0 -400
- package/packages/daemon/dist/terminal/__tests__/MockPtyAdapter.js +0 -182
- package/packages/daemon/dist/terminal/mirror/__tests__/scripted-channel.js +0 -98
- package/packages/daemon/dist/terminal/mirror/__tests__/simulated-channel.js +0 -156
- package/packages/daemon/src/tui/mirror/__snapshots__/activity-surface-renderer.test.tsx.snap +0 -42
- package/packages/daemon/src/tui/mirror/__snapshots__/changes-terminal-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/home-files-surface-renderer.test.tsx.snap +0 -267
- package/packages/daemon/src/tui/mirror/__snapshots__/missions-surface-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/__snapshots__/recipes-gallery-renderer.test.tsx.snap +0 -141
- package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +0 -87
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/agent-terminal-canvas-renderer.test.tsx.snap +0 -348
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +0 -255
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/pane-frame-renderer.test.tsx.snap +0 -525
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +0 -31
- package/packages/daemon/src/tui/mirror/workspace/__snapshots__/workbench-shell-renderer.test.tsx.snap +0 -134
- package/packages/daemon-client/src/application-shell-session.test.ts +0 -196
- package/packages/daemon-client/src/automation-client.test.ts +0 -214
- package/packages/daemon-client/src/bootstrap-coordinator.test.ts +0 -145
- package/packages/daemon-client/src/connection-supervisor.test.ts +0 -109
- package/packages/daemon-client/src/direct-application-shell-transport.test.ts +0 -941
- package/packages/daemon-client/src/first-latest-coordinator.test.ts +0 -175
- package/packages/daemon-client/src/fleet-dial-scheduler.test.ts +0 -114
- package/packages/daemon-client/src/fleet-environments.test.ts +0 -40
- package/packages/daemon-client/src/generation-bound-store-timers.test.ts +0 -74
- package/packages/daemon-client/src/owner-action-client.test.ts +0 -245
- package/packages/daemon-client/src/pane-stream-client.test.ts +0 -2841
- package/packages/daemon-client/src/pane-stream-clock-calibration.test.ts +0 -207
- package/packages/daemon-client/src/pane-stream-loopback-benchmark.test.ts +0 -51
- package/packages/daemon-client/src/push-resource-session.test.ts +0 -509
- package/packages/daemon-client/src/resource-replica.test.ts +0 -186
- package/packages/daemon-client/src/runtime-resource-ledger.test.ts +0 -18
- package/packages/daemon-client/src/scoped-tmux-server-transport.test.ts +0 -114
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +0 -835
- package/packages/daemon-client/src/tmux-server-client.test.ts +0 -279
- package/packages/daemon-client/src/tmux-server-interaction-events.test.ts +0 -230
- package/packages/daemon-client/src/workspace-catalog-v2.test.ts +0 -109
- package/packages/daemon-client/src/workspace-client-boundaries.test.ts +0 -59
- package/packages/daemon-client/src/workspace-client-operations.test.ts +0 -96
- package/packages/daemon-client/src/workspace-client.test.ts +0 -2912
- package/packages/daemon-client/src/workspace-event-supervisor.test.ts +0 -1086
- package/packages/daemon-client/src/workspace-pane-client.test.ts +0 -80
- package/packages/presentation/src/presentation-contract.test.ts +0 -63
- package/packages/tmux-bridge/src/runner.test.ts +0 -614
- package/packages/tmux-bridge/src/targeting.test.ts +0 -107
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SharedWindowIndex } from "./shared-window-index.js";
|
|
2
|
+
import { RegisteredWindowGuard } from "./registered-window-guard.js";
|
|
1
3
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
4
|
import { isAbsolute } from "node:path";
|
|
3
5
|
import { resolveTmuxExecutable, tmuxClientEnvironment } from "../../lib/tmux-client-execution.js";
|
|
@@ -21,6 +23,45 @@ import { controlModeAuthorityKey, processControlModeOwnershipRegistry, } from ".
|
|
|
21
23
|
export class MirrorService {
|
|
22
24
|
opts;
|
|
23
25
|
channels = new Map();
|
|
26
|
+
sharedWindows = new SharedWindowIndex();
|
|
27
|
+
channelSessions = new Map();
|
|
28
|
+
registeredWindows = null;
|
|
29
|
+
registeredConflicts = new Set();
|
|
30
|
+
setRegisteredWindowReader(reader) {
|
|
31
|
+
this.registeredWindows?.dispose();
|
|
32
|
+
const guard = new RegisteredWindowGuard(reader);
|
|
33
|
+
this.registeredWindows = guard;
|
|
34
|
+
this.invalidateRegisteredWindows();
|
|
35
|
+
return {
|
|
36
|
+
invalidate: () => {
|
|
37
|
+
if (this.registeredWindows === guard)
|
|
38
|
+
this.invalidateRegisteredWindows();
|
|
39
|
+
},
|
|
40
|
+
dispose: () => guard.dispose(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
invalidateRegisteredWindows() {
|
|
44
|
+
this.registeredWindows?.invalidate();
|
|
45
|
+
}
|
|
46
|
+
async verifyRegisteredWindows() {
|
|
47
|
+
const guard = this.registeredWindows;
|
|
48
|
+
if (!guard)
|
|
49
|
+
return;
|
|
50
|
+
await guard.verify();
|
|
51
|
+
if (guard !== this.registeredWindows || this.disposed)
|
|
52
|
+
throw new Error("Window ownership changed during discovery");
|
|
53
|
+
for (const [channel, session] of this.channelSessions) {
|
|
54
|
+
if (guard.blocked(session)) {
|
|
55
|
+
channel.setGeometryParticipation(false);
|
|
56
|
+
if (!this.registeredConflicts.has(session)) {
|
|
57
|
+
this.registeredConflicts.add(session);
|
|
58
|
+
this.opts.onSharedWindowConflict?.(session, true);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
this.registeredConflicts.delete(session);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
24
65
|
pendingDisposals = new Set();
|
|
25
66
|
drainingChannels = new Map();
|
|
26
67
|
sessionExitListeners = new Set();
|
|
@@ -101,8 +142,14 @@ export class MirrorService {
|
|
|
101
142
|
reseed: () => handle.reseed(),
|
|
102
143
|
readHistorySize: () => handle.readHistorySize(),
|
|
103
144
|
captureNativeBacking: () => handle.captureNativeBacking(),
|
|
104
|
-
sendText: (text) =>
|
|
105
|
-
|
|
145
|
+
sendText: (text) => {
|
|
146
|
+
this.assertWindowOwnership(request.session);
|
|
147
|
+
handle.sendText(text);
|
|
148
|
+
},
|
|
149
|
+
sendKey: (key) => {
|
|
150
|
+
this.assertWindowOwnership(request.session);
|
|
151
|
+
handle.sendKey(key);
|
|
152
|
+
},
|
|
106
153
|
close: async () => {
|
|
107
154
|
if (closed)
|
|
108
155
|
return;
|
|
@@ -157,6 +204,7 @@ export class MirrorService {
|
|
|
157
204
|
return (args) => {
|
|
158
205
|
if (entry.retired || this.channels.get(session) !== entry)
|
|
159
206
|
return Promise.reject(new Error("Resize mirror session retired"));
|
|
207
|
+
this.assertWindowOwnership(session);
|
|
160
208
|
return run(args);
|
|
161
209
|
};
|
|
162
210
|
}
|
|
@@ -164,6 +212,7 @@ export class MirrorService {
|
|
|
164
212
|
const entry = this.channels.get(session);
|
|
165
213
|
if (!entry || entry.retired)
|
|
166
214
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
215
|
+
this.assertWindowOwnership(session);
|
|
167
216
|
return entry.channel.executeWindowLinkAction(request);
|
|
168
217
|
}
|
|
169
218
|
/** Synchronous hot input on an already-retained SessionRuntime channel. */
|
|
@@ -171,30 +220,66 @@ export class MirrorService {
|
|
|
171
220
|
const entry = this.channels.get(session);
|
|
172
221
|
if (!entry || entry.retired)
|
|
173
222
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
223
|
+
this.assertWindowOwnership(session);
|
|
174
224
|
entry.channel.sendText(semanticPaneId, text, performanceTraceId, isolated);
|
|
175
225
|
}
|
|
176
226
|
sendBytes(session, semanticPaneId, data, performanceTraceId) {
|
|
177
227
|
const entry = this.channels.get(session);
|
|
178
228
|
if (!entry || entry.retired)
|
|
179
229
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
230
|
+
this.assertWindowOwnership(session);
|
|
180
231
|
entry.channel.sendBytes(semanticPaneId, data, performanceTraceId);
|
|
181
232
|
}
|
|
182
233
|
sendKey(session, semanticPaneId, key, performanceTraceId) {
|
|
183
234
|
const entry = this.channels.get(session);
|
|
184
235
|
if (!entry || entry.retired)
|
|
185
236
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
237
|
+
this.assertWindowOwnership(session);
|
|
186
238
|
entry.channel.sendKey(semanticPaneId, key, performanceTraceId);
|
|
187
239
|
}
|
|
240
|
+
assertWindowOwnership(session) {
|
|
241
|
+
const message = this.windowOwnershipMessage(session);
|
|
242
|
+
if (message)
|
|
243
|
+
throw new Error(message);
|
|
244
|
+
}
|
|
245
|
+
windowOwnershipMessage(session) {
|
|
246
|
+
if (this.registeredWindows?.pending())
|
|
247
|
+
return "Window ownership is being verified. Retry after session discovery completes.";
|
|
248
|
+
if (this.hasSharedWindowConflict(session))
|
|
249
|
+
return "Linked windows across registered sessions are unsupported. Unlink the shared window before controlling this session.";
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
hasSharedWindowConflict(session) {
|
|
253
|
+
const entry = this.channels.get(session);
|
|
254
|
+
return ((this.registeredWindows?.blocked(session) ?? false) ||
|
|
255
|
+
(!!entry && this.sharedWindows.conflicted(entry.channel)));
|
|
256
|
+
}
|
|
257
|
+
updateWindowMembership(channel, windows) {
|
|
258
|
+
// A topology read may finish after retirement. It must not resurrect the
|
|
259
|
+
// old channel's membership or conflict with its replacement.
|
|
260
|
+
if (!this.channelSessions.has(channel))
|
|
261
|
+
return;
|
|
262
|
+
for (const [peer, conflict] of this.sharedWindows.update(channel, windows)) {
|
|
263
|
+
const session = this.channelSessions.get(peer);
|
|
264
|
+
if (session === undefined)
|
|
265
|
+
continue;
|
|
266
|
+
if (conflict)
|
|
267
|
+
peer.setGeometryParticipation(false);
|
|
268
|
+
this.opts.onSharedWindowConflict?.(session, conflict);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
188
271
|
fitViewport(session, cols, rows) {
|
|
189
272
|
const entry = this.channels.get(session);
|
|
190
273
|
if (!entry || entry.retired)
|
|
191
274
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
275
|
+
this.assertWindowOwnership(session);
|
|
192
276
|
entry.channel.fitViewport(cols, rows);
|
|
193
277
|
}
|
|
194
278
|
fitWindowViewport(session, semanticWindowId, cols, rows) {
|
|
195
279
|
const entry = this.channels.get(session);
|
|
196
280
|
if (!entry || entry.retired)
|
|
197
281
|
throw new Error(`Mirror session ${session} is unavailable`);
|
|
282
|
+
this.assertWindowOwnership(session);
|
|
198
283
|
entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
|
|
199
284
|
}
|
|
200
285
|
clearWindowViewports(session) {
|
|
@@ -207,7 +292,7 @@ export class MirrorService {
|
|
|
207
292
|
const entry = this.channels.get(session);
|
|
208
293
|
if (!entry || entry.retired)
|
|
209
294
|
return;
|
|
210
|
-
entry.channel.setGeometryParticipation(active);
|
|
295
|
+
entry.channel.setGeometryParticipation(active && !this.hasSharedWindowConflict(session));
|
|
211
296
|
}
|
|
212
297
|
/**
|
|
213
298
|
* Keep one session channel alive independently of renderer subscriptions.
|
|
@@ -248,11 +333,14 @@ export class MirrorService {
|
|
|
248
333
|
}
|
|
249
334
|
async dispose() {
|
|
250
335
|
this.disposed = true;
|
|
336
|
+
this.registeredWindows?.dispose();
|
|
251
337
|
this.sessionExitListeners.clear();
|
|
252
338
|
const entries = [...this.channels.values()];
|
|
253
339
|
this.channels.clear();
|
|
254
340
|
await Promise.allSettled(entries.map(async (entry) => {
|
|
255
341
|
entry.retired = true;
|
|
342
|
+
this.updateWindowMembership(entry.channel, []);
|
|
343
|
+
this.channelSessions.delete(entry.channel);
|
|
256
344
|
try {
|
|
257
345
|
await entry.channel.dispose();
|
|
258
346
|
}
|
|
@@ -297,6 +385,13 @@ export class MirrorService {
|
|
|
297
385
|
const environment = Object.freeze(tmuxClientEnvironment(process.env, executable ?? this.opts.executable));
|
|
298
386
|
const channelOptions = {
|
|
299
387
|
session,
|
|
388
|
+
onWindowMembership: (windows) => this.updateWindowMembership(channel, windows),
|
|
389
|
+
hasSharedWindowConflict: () => this.hasSharedWindowConflict(session),
|
|
390
|
+
beforeIdentityRepair: () => this.verifyRegisteredWindows(),
|
|
391
|
+
onWindowTopologyChanged: () => {
|
|
392
|
+
if (this.channelSessions.has(channel))
|
|
393
|
+
this.invalidateRegisteredWindows();
|
|
394
|
+
},
|
|
300
395
|
ownedViewer,
|
|
301
396
|
createIo: (handlers) => this.opts.createIo?.(session, handlers) ??
|
|
302
397
|
new MirrorControlChannel({
|
|
@@ -363,6 +458,7 @@ export class MirrorService {
|
|
|
363
458
|
releaseAuthority();
|
|
364
459
|
throw cause;
|
|
365
460
|
}
|
|
461
|
+
this.channelSessions.set(channel, session);
|
|
366
462
|
entry = {
|
|
367
463
|
channel,
|
|
368
464
|
started: channel.start(),
|
|
@@ -400,6 +496,8 @@ export class MirrorService {
|
|
|
400
496
|
if (entry.retired)
|
|
401
497
|
return;
|
|
402
498
|
entry.retired = true;
|
|
499
|
+
this.updateWindowMembership(entry.channel, []);
|
|
500
|
+
this.channelSessions.delete(entry.channel);
|
|
403
501
|
if (this.channels.get(session) === entry)
|
|
404
502
|
this.channels.delete(session);
|
|
405
503
|
const disposal = entry.channel
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** A bounded registration/topology proof, shared by all retained channels. */
|
|
2
|
+
export class RegisteredWindowGuard {
|
|
3
|
+
reader;
|
|
4
|
+
#epoch = 0;
|
|
5
|
+
#current = false;
|
|
6
|
+
#disposed = false;
|
|
7
|
+
#conflicts = new Set();
|
|
8
|
+
#read = null;
|
|
9
|
+
constructor(reader) {
|
|
10
|
+
this.reader = reader;
|
|
11
|
+
}
|
|
12
|
+
blocked(session) {
|
|
13
|
+
return !this.#current || this.#conflicts.has(session);
|
|
14
|
+
}
|
|
15
|
+
pending() {
|
|
16
|
+
return !this.#current;
|
|
17
|
+
}
|
|
18
|
+
invalidate() {
|
|
19
|
+
this.#epoch++;
|
|
20
|
+
this.#current = false;
|
|
21
|
+
this.#read?.abort.abort();
|
|
22
|
+
this.#read = null;
|
|
23
|
+
}
|
|
24
|
+
dispose() {
|
|
25
|
+
this.#disposed = true;
|
|
26
|
+
this.invalidate();
|
|
27
|
+
}
|
|
28
|
+
async verify() {
|
|
29
|
+
if (this.#disposed)
|
|
30
|
+
throw new Error("Registered window inventory is disposed");
|
|
31
|
+
if (this.#current)
|
|
32
|
+
return;
|
|
33
|
+
if (this.#read)
|
|
34
|
+
return this.#read.promise;
|
|
35
|
+
const epoch = this.#epoch;
|
|
36
|
+
const abort = new AbortController();
|
|
37
|
+
const promise = Promise.resolve()
|
|
38
|
+
.then(async () => {
|
|
39
|
+
abort.signal.throwIfAborted();
|
|
40
|
+
const rows = await this.reader(abort.signal);
|
|
41
|
+
abort.signal.throwIfAborted();
|
|
42
|
+
if (epoch !== this.#epoch)
|
|
43
|
+
throw new Error("Window ownership changed during discovery");
|
|
44
|
+
const owners = new Map();
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
const sessions = owners.get(row.window) ?? new Set();
|
|
47
|
+
sessions.add(row.session);
|
|
48
|
+
owners.set(row.window, sessions);
|
|
49
|
+
}
|
|
50
|
+
this.#conflicts = new Set([...owners.values()]
|
|
51
|
+
.filter((sessions) => sessions.size > 1)
|
|
52
|
+
.flatMap((sessions) => [...sessions]));
|
|
53
|
+
this.#current = true;
|
|
54
|
+
})
|
|
55
|
+
.finally(() => {
|
|
56
|
+
if (this.#read?.abort === abort)
|
|
57
|
+
this.#read = null;
|
|
58
|
+
});
|
|
59
|
+
this.#read = { abort, promise };
|
|
60
|
+
return promise;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -53,7 +53,10 @@ const STRUCTURAL_NOTIFICATIONS = new Set([
|
|
|
53
53
|
"window-add",
|
|
54
54
|
"window-close",
|
|
55
55
|
"window-renamed",
|
|
56
|
+
"unlinked-window-add",
|
|
56
57
|
"unlinked-window-close",
|
|
58
|
+
"session-renamed",
|
|
59
|
+
"sessions-changed",
|
|
57
60
|
]);
|
|
58
61
|
const NATIVE_CLIENT_NOTIFICATIONS = new Set([
|
|
59
62
|
"client-attached",
|
|
@@ -193,6 +196,7 @@ export class SessionChannel {
|
|
|
193
196
|
nativeClientProbePending = false;
|
|
194
197
|
// Native captures fence every geometry event; inventory fences identity only.
|
|
195
198
|
windowAuthorityOrdinal = 0;
|
|
199
|
+
layoutNotificationOrdinals = new Map();
|
|
196
200
|
windowIdentityOrdinal = 0;
|
|
197
201
|
paneIncarnation = 0;
|
|
198
202
|
recoveryOrdinal = 0;
|
|
@@ -764,8 +768,25 @@ export class SessionChannel {
|
|
|
764
768
|
}
|
|
765
769
|
this.input.flush();
|
|
766
770
|
this.clearWindowViewports();
|
|
771
|
+
this.restoreWindowSizing(this.attachedIdentity?.runtimeSessionId ?? `=${this.opts.session}`);
|
|
767
772
|
this.io.send(`refresh-client -C ${cols}x${rows}`);
|
|
768
773
|
}
|
|
774
|
+
restoreWindowSizing(target) {
|
|
775
|
+
if (!this.geometryParticipating)
|
|
776
|
+
return;
|
|
777
|
+
// resize-window pins a window (including through inherited session options).
|
|
778
|
+
// Evaluate inside the existing control connection: no shell, polling, or extra
|
|
779
|
+
// request/response round trip. The shell also pre-fits hidden windows;
|
|
780
|
+
// only the active window may have its manual policy repaired.
|
|
781
|
+
const quoted = tmuxSingleQuote(target);
|
|
782
|
+
// if-shell emits its own reply and the selected branch emits another.
|
|
783
|
+
// Keep both branches at one command so either policy consumes exactly two
|
|
784
|
+
// FIFO slots; a discard-only send would misroute the next capture replies.
|
|
785
|
+
this.io.commandListInline(`if-shell -F -t ${quoted} '#{&&:#{window_active},#{==:#{window-size},manual}}' ` +
|
|
786
|
+
tmuxSingleQuote(`set-option -w -t ${quoted} window-size latest`) +
|
|
787
|
+
" " +
|
|
788
|
+
tmuxSingleQuote(`display-message -p -t ${quoted} ''`), 2, 1, () => { });
|
|
789
|
+
}
|
|
769
790
|
/**
|
|
770
791
|
* Experimental: window-specific overrides stay private to this control client.
|
|
771
792
|
* Native tmux still falls back to the client size for unscoped neighbours;
|
|
@@ -788,6 +809,7 @@ export class SessionChannel {
|
|
|
788
809
|
if (previous?.cols === cols && previous.rows === rows)
|
|
789
810
|
return;
|
|
790
811
|
this.input.flush();
|
|
812
|
+
this.restoreWindowSizing(window.runtimeId);
|
|
791
813
|
this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}`);
|
|
792
814
|
this.fittedWindows.set(window.runtimeId, { cols, rows });
|
|
793
815
|
}
|
|
@@ -863,6 +885,7 @@ export class SessionChannel {
|
|
|
863
885
|
pane.subs.clear();
|
|
864
886
|
}
|
|
865
887
|
this.pendingLayoutOutput.clear();
|
|
888
|
+
this.layoutNotificationOrdinals.clear();
|
|
866
889
|
this.layoutSubscribers.clear();
|
|
867
890
|
this.layoutAuthoritySubscribers.clear();
|
|
868
891
|
await this.io.dispose();
|
|
@@ -2303,6 +2326,13 @@ export class SessionChannel {
|
|
|
2303
2326
|
pane.scrollOnClear = value === "0" || value === "1" ? value === "1" : undefined;
|
|
2304
2327
|
}
|
|
2305
2328
|
onNotify(name, rest) {
|
|
2329
|
+
if (name === "window-add" ||
|
|
2330
|
+
name === "window-close" ||
|
|
2331
|
+
name === "unlinked-window-add" ||
|
|
2332
|
+
name === "unlinked-window-close" ||
|
|
2333
|
+
name === "session-renamed" ||
|
|
2334
|
+
name === "sessions-changed")
|
|
2335
|
+
this.opts.onWindowTopologyChanged?.();
|
|
2306
2336
|
if (name === "subscription-changed") {
|
|
2307
2337
|
const policy = /^tmux-ide-scroll-on-clear\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+(%[0-9]+)\s+:\s+([01])\s*$/u.exec(rest);
|
|
2308
2338
|
if (policy) {
|
|
@@ -2420,6 +2450,7 @@ export class SessionChannel {
|
|
|
2420
2450
|
this.scheduleSync(); // never guess from a failed parse
|
|
2421
2451
|
return;
|
|
2422
2452
|
}
|
|
2453
|
+
this.layoutNotificationOrdinals.set(change.windowId, this.windowAuthorityOrdinal);
|
|
2423
2454
|
const pendingLayout = {
|
|
2424
2455
|
...parsed,
|
|
2425
2456
|
zoomed: change.zoomed,
|
|
@@ -2715,9 +2746,12 @@ export class SessionChannel {
|
|
|
2715
2746
|
const previousCurrentWindow = this.currentWindow;
|
|
2716
2747
|
const { listed, movedWindowRuntimeIds } = this.applyPaneTruth(truth);
|
|
2717
2748
|
await this.syncWindows(this.opts.session, movedWindowRuntimeIds, previousCurrentWindow, syncOrdinal);
|
|
2718
|
-
for (const pane of this.panesByRuntime.values())
|
|
2749
|
+
for (const pane of this.panesByRuntime.values()) {
|
|
2750
|
+
if (pane.windowRuntimeId && this.pendingLayoutOutput.has(pane.windowRuntimeId))
|
|
2751
|
+
continue;
|
|
2719
2752
|
for (const sub of pane.subs)
|
|
2720
2753
|
sub.resumeLayoutCapture?.(syncOrdinal);
|
|
2754
|
+
}
|
|
2721
2755
|
this.discovery.discover(listed);
|
|
2722
2756
|
}
|
|
2723
2757
|
applyPaneTruth(truth) {
|
|
@@ -2957,6 +2991,27 @@ export class SessionChannel {
|
|
|
2957
2991
|
return stage.repairedIdentity;
|
|
2958
2992
|
}
|
|
2959
2993
|
commitWindowStage(stage, requiredLayoutEmits = new Set(), previousCurrentWindow = this.currentWindow, syncOrdinal) {
|
|
2994
|
+
// Control notifications in the same read chunk run before a promise-based
|
|
2995
|
+
// inventory read resumes. Preserve newer geometry (and its pending border
|
|
2996
|
+
// query) rather than letting that older inventory erase the notification.
|
|
2997
|
+
const newerLayouts = new Set([...this.layoutNotificationOrdinals]
|
|
2998
|
+
.filter(([, ordinal]) => ordinal > stage.observedAuthorityOrdinal)
|
|
2999
|
+
.map(([runtimeId]) => runtimeId));
|
|
3000
|
+
if (newerLayouts.size > 0) {
|
|
3001
|
+
stage = { ...stage, windows: new Map(stage.windows), layouts: new Map(stage.layouts) };
|
|
3002
|
+
for (const runtimeId of newerLayouts) {
|
|
3003
|
+
const currentLayout = this.layoutByWindow.get(runtimeId);
|
|
3004
|
+
const currentWindow = this.windowsByRuntime.get(runtimeId);
|
|
3005
|
+
const stagedWindow = stage.windows.get(runtimeId);
|
|
3006
|
+
if (currentLayout && currentWindow && stagedWindow) {
|
|
3007
|
+
stage.layouts.set(runtimeId, currentLayout);
|
|
3008
|
+
stage.windows.set(runtimeId, {
|
|
3009
|
+
...stagedWindow,
|
|
3010
|
+
paneBorderStatus: currentWindow.paneBorderStatus,
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
2960
3015
|
this.windowLinkAuthority?.reconcile(stage.links);
|
|
2961
3016
|
this.latestWindowStage = stage;
|
|
2962
3017
|
const changedWindows = new Set();
|
|
@@ -3008,10 +3063,14 @@ export class SessionChannel {
|
|
|
3008
3063
|
? new Set(stage.windows.keys())
|
|
3009
3064
|
: new Set([...changedWindows, ...requiredLayoutEmits, ...this.pendingLayoutOutput.keys()].filter((runtimeId) => stage.windows.has(runtimeId)));
|
|
3010
3065
|
for (const runtimeId of this.pendingLayoutOutput.keys())
|
|
3011
|
-
if (!stage.windows.has(runtimeId))
|
|
3066
|
+
if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
|
|
3012
3067
|
this.pendingLayoutOutput.delete(runtimeId);
|
|
3068
|
+
for (const runtimeId of this.layoutNotificationOrdinals.keys())
|
|
3069
|
+
if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
|
|
3070
|
+
this.layoutNotificationOrdinals.delete(runtimeId);
|
|
3013
3071
|
for (const runtimeId of layoutEmits)
|
|
3014
|
-
this.
|
|
3072
|
+
if (!newerLayouts.has(runtimeId) || !this.pendingLayoutOutput.has(runtimeId))
|
|
3073
|
+
this.releasePendingLayout(runtimeId, syncOrdinal);
|
|
3015
3074
|
this.emitLayoutAuthority();
|
|
3016
3075
|
}
|
|
3017
3076
|
// Inventory establishes pane/window identity, not a frozen terminal size.
|
|
@@ -3043,6 +3102,7 @@ export class SessionChannel {
|
|
|
3043
3102
|
return true;
|
|
3044
3103
|
}
|
|
3045
3104
|
async stageWindows(target = this.opts.session) {
|
|
3105
|
+
const observedAuthorityOrdinal = this.windowAuthorityOrdinal;
|
|
3046
3106
|
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}"`);
|
|
3047
3107
|
if (lines.length > WINDOW_LINK_MAX_LINKS)
|
|
3048
3108
|
throw new Error("Window link observation exceeds bound");
|
|
@@ -3115,6 +3175,10 @@ export class SessionChannel {
|
|
|
3115
3175
|
throw new Error(`full window layout for ${this.opts.session} is malformed`);
|
|
3116
3176
|
nextLayoutByWindow.set(row.runtimeId, { ...parsed, zoomed: row.zoomed, unzoomed });
|
|
3117
3177
|
}
|
|
3178
|
+
// Publish validated physical membership before any identity repair writes.
|
|
3179
|
+
// The receiving session may introduce duplicate stamps by linking a window.
|
|
3180
|
+
this.opts.onWindowMembership?.([...backingRows.keys()]);
|
|
3181
|
+
await this.opts.beforeIdentityRepair?.();
|
|
3118
3182
|
// Valid unique stamps are identity; missing/invalid/duplicated stamps are
|
|
3119
3183
|
// ALL regenerated and stamped back (the pane policy, applied to windows).
|
|
3120
3184
|
const stampCounts = new Map();
|
|
@@ -3133,7 +3197,7 @@ export class SessionChannel {
|
|
|
3133
3197
|
if (row.stamp && stampCounts.get(row.stamp) === 1) {
|
|
3134
3198
|
semanticId = row.stamp;
|
|
3135
3199
|
}
|
|
3136
|
-
else {
|
|
3200
|
+
else if (!this.opts.hasSharedWindowConflict?.()) {
|
|
3137
3201
|
repairedIdentity = true;
|
|
3138
3202
|
let candidate = null;
|
|
3139
3203
|
for (let attempt = 0; attempt < 32 && !candidate; attempt += 1) {
|
|
@@ -3167,6 +3231,7 @@ export class SessionChannel {
|
|
|
3167
3231
|
});
|
|
3168
3232
|
}
|
|
3169
3233
|
return {
|
|
3234
|
+
observedAuthorityOrdinal,
|
|
3170
3235
|
windows: next,
|
|
3171
3236
|
layouts: nextLayoutByWindow,
|
|
3172
3237
|
currentWindow: nextCurrentWindow,
|
|
@@ -3182,6 +3247,10 @@ export class SessionChannel {
|
|
|
3182
3247
|
async reconcileIdentity(descriptors, listed) {
|
|
3183
3248
|
if (this.disposed)
|
|
3184
3249
|
return false;
|
|
3250
|
+
if (this.opts.hasSharedWindowConflict?.()) {
|
|
3251
|
+
this.settleFirstJoin();
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
3185
3254
|
const snapshots = descriptors
|
|
3186
3255
|
.filter((descriptor) => listed.has(descriptor.runtimePaneId))
|
|
3187
3256
|
.map((descriptor) => ({
|
|
@@ -3309,6 +3378,8 @@ export class SessionChannel {
|
|
|
3309
3378
|
return plan.stampEffects.length > 0;
|
|
3310
3379
|
}
|
|
3311
3380
|
async repairTrustedPaneIdentity(descriptors, layouts) {
|
|
3381
|
+
if (this.opts.hasSharedWindowConflict?.())
|
|
3382
|
+
throw new Error("Linked windows across controlled sessions are unsupported. Unlink the shared window before controlling this session.");
|
|
3312
3383
|
const rectForRuntime = (runtimePaneId) => {
|
|
3313
3384
|
for (const layout of layouts.values()) {
|
|
3314
3385
|
const leaf = layout.leaves.find(({ id }) => id === runtimePaneId);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Event-fed physical window membership for one pinned tmux server. */
|
|
2
|
+
export class SharedWindowIndex {
|
|
3
|
+
#windows = new Map();
|
|
4
|
+
#owners = new Map();
|
|
5
|
+
#conflicts = new Set();
|
|
6
|
+
conflicted(owner) {
|
|
7
|
+
return this.#conflicts.has(owner);
|
|
8
|
+
}
|
|
9
|
+
update(owner, windows) {
|
|
10
|
+
const previous = this.#owners.get(owner) ?? new Set();
|
|
11
|
+
const next = new Set(windows);
|
|
12
|
+
const affected = new Set([owner]);
|
|
13
|
+
for (const window of new Set([...previous, ...next])) {
|
|
14
|
+
for (const peer of this.#windows.get(window) ?? [])
|
|
15
|
+
affected.add(peer);
|
|
16
|
+
if (previous.has(window) && !next.has(window)) {
|
|
17
|
+
const peers = this.#windows.get(window);
|
|
18
|
+
peers.delete(owner);
|
|
19
|
+
if (!peers.size)
|
|
20
|
+
this.#windows.delete(window);
|
|
21
|
+
}
|
|
22
|
+
if (next.has(window)) {
|
|
23
|
+
const peers = this.#windows.get(window) ?? new Set();
|
|
24
|
+
peers.add(owner);
|
|
25
|
+
this.#windows.set(window, peers);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (next.size)
|
|
29
|
+
this.#owners.set(owner, next);
|
|
30
|
+
else
|
|
31
|
+
this.#owners.delete(owner);
|
|
32
|
+
const changed = new Map();
|
|
33
|
+
for (const peer of affected) {
|
|
34
|
+
const conflict = [...(this.#owners.get(peer) ?? [])].some((window) => (this.#windows.get(window)?.size ?? 0) > 1);
|
|
35
|
+
if (conflict === this.#conflicts.has(peer))
|
|
36
|
+
continue;
|
|
37
|
+
if (conflict)
|
|
38
|
+
this.#conflicts.add(peer);
|
|
39
|
+
else
|
|
40
|
+
this.#conflicts.delete(peer);
|
|
41
|
+
changed.set(peer, conflict);
|
|
42
|
+
}
|
|
43
|
+
return changed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1691,7 +1691,17 @@ export class PaneStreamLiveConnection {
|
|
|
1691
1691
|
if (!this.#sessionRuntimeBinding?.requestAuthority ||
|
|
1692
1692
|
!this.#sessionRuntimeBinding.authoritySnapshot)
|
|
1693
1693
|
return this.#failProtocol("protocol-error");
|
|
1694
|
-
|
|
1694
|
+
let lease = null;
|
|
1695
|
+
try {
|
|
1696
|
+
lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
|
|
1697
|
+
}
|
|
1698
|
+
catch (error) {
|
|
1699
|
+
// Topology changes can temporarily prevent ownership. Reject this
|
|
1700
|
+
// request without letting an expected conflict terminate the daemon.
|
|
1701
|
+
if (!(error instanceof SessionRuntimeControllerLeaseError) ||
|
|
1702
|
+
error.code !== "controller-conflict")
|
|
1703
|
+
return this.#failProtocol("input-rejected");
|
|
1704
|
+
}
|
|
1695
1705
|
if (lease)
|
|
1696
1706
|
this.#requestedAuthorities.add(frame.authority);
|
|
1697
1707
|
this.#sendFrame(null, {
|
|
@@ -102,6 +102,7 @@ export function createSessionRuntimeMultiplexerBackend(options) {
|
|
|
102
102
|
if (!ownerAuthorized) {
|
|
103
103
|
throw new Error("Semantic mutation requires a live host, pane, or owner principal");
|
|
104
104
|
}
|
|
105
|
+
await options.registry.verifyWindowOwnership();
|
|
105
106
|
const owner = acquireOwner(session);
|
|
106
107
|
try {
|
|
107
108
|
let lease;
|