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.ts";
|
|
2
|
+
import { RegisteredWindowGuard, type RegisteredWindowReader } from "./registered-window-guard.ts";
|
|
1
3
|
import type { OwnedViewerAdapter } from "./owned-viewer-adapter.ts";
|
|
2
4
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
5
|
import { isAbsolute } from "node:path";
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
} from "./control-mode-ownership.ts";
|
|
48
50
|
|
|
49
51
|
export interface MirrorServiceOptions {
|
|
52
|
+
onSharedWindowConflict?: (session: string, conflicted: boolean) => void;
|
|
50
53
|
createOwnedViewerAdapter?: (session: string) => OwnedViewerAdapter | undefined;
|
|
51
54
|
nativeServerIdentity?: import("../../lib/tmux-server-generation-runner.ts").NativeTmuxServerIdentity;
|
|
52
55
|
resolveSocketPath?: () => string;
|
|
@@ -148,6 +151,47 @@ interface ChannelEntry {
|
|
|
148
151
|
export class MirrorService {
|
|
149
152
|
private readonly opts: MirrorServiceOptions;
|
|
150
153
|
private readonly channels = new Map<string, ChannelEntry>();
|
|
154
|
+
private readonly sharedWindows = new SharedWindowIndex<SessionChannel>();
|
|
155
|
+
private readonly channelSessions = new Map<SessionChannel, string>();
|
|
156
|
+
private registeredWindows: RegisteredWindowGuard | null = null;
|
|
157
|
+
private readonly registeredConflicts = new Set<string>();
|
|
158
|
+
|
|
159
|
+
setRegisteredWindowReader(reader: RegisteredWindowReader): {
|
|
160
|
+
invalidate(): void;
|
|
161
|
+
dispose(): void;
|
|
162
|
+
} {
|
|
163
|
+
this.registeredWindows?.dispose();
|
|
164
|
+
const guard = new RegisteredWindowGuard(reader);
|
|
165
|
+
this.registeredWindows = guard;
|
|
166
|
+
this.invalidateRegisteredWindows();
|
|
167
|
+
return {
|
|
168
|
+
invalidate: () => {
|
|
169
|
+
if (this.registeredWindows === guard) this.invalidateRegisteredWindows();
|
|
170
|
+
},
|
|
171
|
+
dispose: () => guard.dispose(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private invalidateRegisteredWindows(): void {
|
|
176
|
+
this.registeredWindows?.invalidate();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async verifyRegisteredWindows(): Promise<void> {
|
|
180
|
+
const guard = this.registeredWindows;
|
|
181
|
+
if (!guard) return;
|
|
182
|
+
await guard.verify();
|
|
183
|
+
if (guard !== this.registeredWindows || this.disposed)
|
|
184
|
+
throw new Error("Window ownership changed during discovery");
|
|
185
|
+
for (const [channel, session] of this.channelSessions) {
|
|
186
|
+
if (guard.blocked(session)) {
|
|
187
|
+
channel.setGeometryParticipation(false);
|
|
188
|
+
if (!this.registeredConflicts.has(session)) {
|
|
189
|
+
this.registeredConflicts.add(session);
|
|
190
|
+
this.opts.onSharedWindowConflict?.(session, true);
|
|
191
|
+
}
|
|
192
|
+
} else this.registeredConflicts.delete(session);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
151
195
|
private readonly pendingDisposals = new Set<Promise<void>>();
|
|
152
196
|
private readonly drainingChannels = new Map<string, Promise<void>>();
|
|
153
197
|
private readonly sessionExitListeners = new Set<(session: string) => void>();
|
|
@@ -239,8 +283,14 @@ export class MirrorService {
|
|
|
239
283
|
reseed: () => handle.reseed(),
|
|
240
284
|
readHistorySize: () => handle.readHistorySize(),
|
|
241
285
|
captureNativeBacking: () => handle.captureNativeBacking(),
|
|
242
|
-
sendText: (text) =>
|
|
243
|
-
|
|
286
|
+
sendText: (text) => {
|
|
287
|
+
this.assertWindowOwnership(request.session);
|
|
288
|
+
handle.sendText(text);
|
|
289
|
+
},
|
|
290
|
+
sendKey: (key) => {
|
|
291
|
+
this.assertWindowOwnership(request.session);
|
|
292
|
+
handle.sendKey(key);
|
|
293
|
+
},
|
|
244
294
|
close: async () => {
|
|
245
295
|
if (closed) return;
|
|
246
296
|
closed = true;
|
|
@@ -303,6 +353,7 @@ export class MirrorService {
|
|
|
303
353
|
return (args: readonly string[]) => {
|
|
304
354
|
if (entry.retired || this.channels.get(session) !== entry)
|
|
305
355
|
return Promise.reject(new Error("Resize mirror session retired"));
|
|
356
|
+
this.assertWindowOwnership(session);
|
|
306
357
|
return run(args);
|
|
307
358
|
};
|
|
308
359
|
}
|
|
@@ -313,6 +364,7 @@ export class MirrorService {
|
|
|
313
364
|
) {
|
|
314
365
|
const entry = this.channels.get(session);
|
|
315
366
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
367
|
+
this.assertWindowOwnership(session);
|
|
316
368
|
return entry.channel.executeWindowLinkAction(request);
|
|
317
369
|
}
|
|
318
370
|
|
|
@@ -326,6 +378,7 @@ export class MirrorService {
|
|
|
326
378
|
): void {
|
|
327
379
|
const entry = this.channels.get(session);
|
|
328
380
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
381
|
+
this.assertWindowOwnership(session);
|
|
329
382
|
entry.channel.sendText(semanticPaneId, text, performanceTraceId, isolated);
|
|
330
383
|
}
|
|
331
384
|
|
|
@@ -337,24 +390,61 @@ export class MirrorService {
|
|
|
337
390
|
): void {
|
|
338
391
|
const entry = this.channels.get(session);
|
|
339
392
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
393
|
+
this.assertWindowOwnership(session);
|
|
340
394
|
entry.channel.sendBytes(semanticPaneId, data, performanceTraceId);
|
|
341
395
|
}
|
|
342
396
|
|
|
343
397
|
sendKey(session: string, semanticPaneId: string, key: string, performanceTraceId?: string): void {
|
|
344
398
|
const entry = this.channels.get(session);
|
|
345
399
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
400
|
+
this.assertWindowOwnership(session);
|
|
346
401
|
entry.channel.sendKey(semanticPaneId, key, performanceTraceId);
|
|
347
402
|
}
|
|
348
403
|
|
|
404
|
+
private assertWindowOwnership(session: string): void {
|
|
405
|
+
const message = this.windowOwnershipMessage(session);
|
|
406
|
+
if (message) throw new Error(message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
windowOwnershipMessage(session: string): string | null {
|
|
410
|
+
if (this.registeredWindows?.pending())
|
|
411
|
+
return "Window ownership is being verified. Retry after session discovery completes.";
|
|
412
|
+
if (this.hasSharedWindowConflict(session))
|
|
413
|
+
return "Linked windows across registered sessions are unsupported. Unlink the shared window before controlling this session.";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
hasSharedWindowConflict(session: string): boolean {
|
|
418
|
+
const entry = this.channels.get(session);
|
|
419
|
+
return (
|
|
420
|
+
(this.registeredWindows?.blocked(session) ?? false) ||
|
|
421
|
+
(!!entry && this.sharedWindows.conflicted(entry.channel))
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private updateWindowMembership(channel: SessionChannel, windows: readonly string[]): void {
|
|
426
|
+
// A topology read may finish after retirement. It must not resurrect the
|
|
427
|
+
// old channel's membership or conflict with its replacement.
|
|
428
|
+
if (!this.channelSessions.has(channel)) return;
|
|
429
|
+
for (const [peer, conflict] of this.sharedWindows.update(channel, windows)) {
|
|
430
|
+
const session = this.channelSessions.get(peer);
|
|
431
|
+
if (session === undefined) continue;
|
|
432
|
+
if (conflict) peer.setGeometryParticipation(false);
|
|
433
|
+
this.opts.onSharedWindowConflict?.(session, conflict);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
349
437
|
fitViewport(session: string, cols: number, rows: number): void {
|
|
350
438
|
const entry = this.channels.get(session);
|
|
351
439
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
440
|
+
this.assertWindowOwnership(session);
|
|
352
441
|
entry.channel.fitViewport(cols, rows);
|
|
353
442
|
}
|
|
354
443
|
|
|
355
444
|
fitWindowViewport(session: string, semanticWindowId: string, cols: number, rows: number): void {
|
|
356
445
|
const entry = this.channels.get(session);
|
|
357
446
|
if (!entry || entry.retired) throw new Error(`Mirror session ${session} is unavailable`);
|
|
447
|
+
this.assertWindowOwnership(session);
|
|
358
448
|
entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
|
|
359
449
|
}
|
|
360
450
|
|
|
@@ -367,7 +457,7 @@ export class MirrorService {
|
|
|
367
457
|
setGeometryParticipation(session: string, active: boolean): void {
|
|
368
458
|
const entry = this.channels.get(session);
|
|
369
459
|
if (!entry || entry.retired) return;
|
|
370
|
-
entry.channel.setGeometryParticipation(active);
|
|
460
|
+
entry.channel.setGeometryParticipation(active && !this.hasSharedWindowConflict(session));
|
|
371
461
|
}
|
|
372
462
|
|
|
373
463
|
/**
|
|
@@ -412,12 +502,15 @@ export class MirrorService {
|
|
|
412
502
|
|
|
413
503
|
async dispose(): Promise<void> {
|
|
414
504
|
this.disposed = true;
|
|
505
|
+
this.registeredWindows?.dispose();
|
|
415
506
|
this.sessionExitListeners.clear();
|
|
416
507
|
const entries = [...this.channels.values()];
|
|
417
508
|
this.channels.clear();
|
|
418
509
|
await Promise.allSettled(
|
|
419
510
|
entries.map(async (entry) => {
|
|
420
511
|
entry.retired = true;
|
|
512
|
+
this.updateWindowMembership(entry.channel, []);
|
|
513
|
+
this.channelSessions.delete(entry.channel);
|
|
421
514
|
try {
|
|
422
515
|
await entry.channel.dispose();
|
|
423
516
|
} finally {
|
|
@@ -468,6 +561,12 @@ export class MirrorService {
|
|
|
468
561
|
);
|
|
469
562
|
const channelOptions: SessionChannelOptions = {
|
|
470
563
|
session,
|
|
564
|
+
onWindowMembership: (windows) => this.updateWindowMembership(channel, windows),
|
|
565
|
+
hasSharedWindowConflict: () => this.hasSharedWindowConflict(session),
|
|
566
|
+
beforeIdentityRepair: () => this.verifyRegisteredWindows(),
|
|
567
|
+
onWindowTopologyChanged: () => {
|
|
568
|
+
if (this.channelSessions.has(channel)) this.invalidateRegisteredWindows();
|
|
569
|
+
},
|
|
471
570
|
ownedViewer,
|
|
472
571
|
createIo: (handlers) =>
|
|
473
572
|
this.opts.createIo?.(session, handlers) ??
|
|
@@ -547,6 +646,7 @@ export class MirrorService {
|
|
|
547
646
|
releaseAuthority();
|
|
548
647
|
throw cause;
|
|
549
648
|
}
|
|
649
|
+
this.channelSessions.set(channel, session);
|
|
550
650
|
entry = {
|
|
551
651
|
channel,
|
|
552
652
|
started: channel.start(),
|
|
@@ -583,6 +683,8 @@ export class MirrorService {
|
|
|
583
683
|
private retire(session: string, entry: ChannelEntry): void {
|
|
584
684
|
if (entry.retired) return;
|
|
585
685
|
entry.retired = true;
|
|
686
|
+
this.updateWindowMembership(entry.channel, []);
|
|
687
|
+
this.channelSessions.delete(entry.channel);
|
|
586
688
|
if (this.channels.get(session) === entry) this.channels.delete(session);
|
|
587
689
|
const disposal = entry.channel
|
|
588
690
|
.dispose()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type RegisteredWindowReader = (
|
|
2
|
+
signal: AbortSignal,
|
|
3
|
+
) => Promise<readonly { session: string; window: string }[]>;
|
|
4
|
+
|
|
5
|
+
/** A bounded registration/topology proof, shared by all retained channels. */
|
|
6
|
+
export class RegisteredWindowGuard {
|
|
7
|
+
#epoch = 0;
|
|
8
|
+
#current = false;
|
|
9
|
+
#disposed = false;
|
|
10
|
+
#conflicts = new Set<string>();
|
|
11
|
+
#read: { abort: AbortController; promise: Promise<void> } | null = null;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly reader: RegisteredWindowReader) {}
|
|
14
|
+
|
|
15
|
+
blocked(session: string): boolean {
|
|
16
|
+
return !this.#current || this.#conflicts.has(session);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pending(): boolean {
|
|
20
|
+
return !this.#current;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
invalidate(): void {
|
|
24
|
+
this.#epoch++;
|
|
25
|
+
this.#current = false;
|
|
26
|
+
this.#read?.abort.abort();
|
|
27
|
+
this.#read = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dispose(): void {
|
|
31
|
+
this.#disposed = true;
|
|
32
|
+
this.invalidate();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async verify(): Promise<void> {
|
|
36
|
+
if (this.#disposed) throw new Error("Registered window inventory is disposed");
|
|
37
|
+
if (this.#current) return;
|
|
38
|
+
if (this.#read) return this.#read.promise;
|
|
39
|
+
const epoch = this.#epoch;
|
|
40
|
+
const abort = new AbortController();
|
|
41
|
+
const promise = Promise.resolve()
|
|
42
|
+
.then(async () => {
|
|
43
|
+
abort.signal.throwIfAborted();
|
|
44
|
+
const rows = await this.reader(abort.signal);
|
|
45
|
+
abort.signal.throwIfAborted();
|
|
46
|
+
if (epoch !== this.#epoch) throw new Error("Window ownership changed during discovery");
|
|
47
|
+
const owners = new Map<string, Set<string>>();
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const sessions = owners.get(row.window) ?? new Set<string>();
|
|
50
|
+
sessions.add(row.session);
|
|
51
|
+
owners.set(row.window, sessions);
|
|
52
|
+
}
|
|
53
|
+
this.#conflicts = new Set(
|
|
54
|
+
[...owners.values()]
|
|
55
|
+
.filter((sessions) => sessions.size > 1)
|
|
56
|
+
.flatMap((sessions) => [...sessions]),
|
|
57
|
+
);
|
|
58
|
+
this.#current = true;
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
if (this.#read?.abort === abort) this.#read = null;
|
|
62
|
+
});
|
|
63
|
+
this.#read = { abort, promise };
|
|
64
|
+
return promise;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -123,7 +123,10 @@ const STRUCTURAL_NOTIFICATIONS = new Set([
|
|
|
123
123
|
"window-add",
|
|
124
124
|
"window-close",
|
|
125
125
|
"window-renamed",
|
|
126
|
+
"unlinked-window-add",
|
|
126
127
|
"unlinked-window-close",
|
|
128
|
+
"session-renamed",
|
|
129
|
+
"sessions-changed",
|
|
127
130
|
]);
|
|
128
131
|
const NATIVE_CLIENT_NOTIFICATIONS = new Set([
|
|
129
132
|
"client-attached",
|
|
@@ -239,6 +242,10 @@ export interface MirrorFlowRecoveryObservation {
|
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
export interface SessionChannelOptions {
|
|
245
|
+
onWindowMembership?: (windows: readonly string[]) => void;
|
|
246
|
+
hasSharedWindowConflict?: () => boolean;
|
|
247
|
+
beforeIdentityRepair?: () => Promise<void>;
|
|
248
|
+
onWindowTopologyChanged?: () => void;
|
|
242
249
|
ownedViewer?: Pick<OwnedViewerAdapter, "bindIo" | "tryDispatch" | "dispose"> &
|
|
243
250
|
Partial<Pick<OwnedViewerAdapter, "atomicSnapshotEpoch">>;
|
|
244
251
|
executeWindowLinkGuard?: (args: string[]) => Promise<{ status: number | null; stdout: string }>;
|
|
@@ -441,6 +448,7 @@ function layoutIdentitiesEqual(left: WindowLayout, right: WindowLayout): boolean
|
|
|
441
448
|
}
|
|
442
449
|
|
|
443
450
|
interface WindowSyncStage {
|
|
451
|
+
readonly observedAuthorityOrdinal: number;
|
|
444
452
|
readonly windows: Map<string, WindowRecord>;
|
|
445
453
|
readonly layouts: Map<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>;
|
|
446
454
|
readonly currentWindow: string;
|
|
@@ -502,6 +510,7 @@ export class SessionChannel {
|
|
|
502
510
|
private nativeClientProbePending = false;
|
|
503
511
|
// Native captures fence every geometry event; inventory fences identity only.
|
|
504
512
|
private windowAuthorityOrdinal = 0;
|
|
513
|
+
private readonly layoutNotificationOrdinals = new Map<string, number>();
|
|
505
514
|
private windowIdentityOrdinal = 0;
|
|
506
515
|
private paneIncarnation = 0;
|
|
507
516
|
private recoveryOrdinal = 0;
|
|
@@ -1186,9 +1195,31 @@ export class SessionChannel {
|
|
|
1186
1195
|
}
|
|
1187
1196
|
this.input.flush();
|
|
1188
1197
|
this.clearWindowViewports();
|
|
1198
|
+
this.restoreWindowSizing(this.attachedIdentity?.runtimeSessionId ?? `=${this.opts.session}`);
|
|
1189
1199
|
this.io.send(`refresh-client -C ${cols}x${rows}`);
|
|
1190
1200
|
}
|
|
1191
1201
|
|
|
1202
|
+
private restoreWindowSizing(target: string): void {
|
|
1203
|
+
if (!this.geometryParticipating) return;
|
|
1204
|
+
// resize-window pins a window (including through inherited session options).
|
|
1205
|
+
// Evaluate inside the existing control connection: no shell, polling, or extra
|
|
1206
|
+
// request/response round trip. The shell also pre-fits hidden windows;
|
|
1207
|
+
// only the active window may have its manual policy repaired.
|
|
1208
|
+
const quoted = tmuxSingleQuote(target);
|
|
1209
|
+
// if-shell emits its own reply and the selected branch emits another.
|
|
1210
|
+
// Keep both branches at one command so either policy consumes exactly two
|
|
1211
|
+
// FIFO slots; a discard-only send would misroute the next capture replies.
|
|
1212
|
+
this.io.commandListInline(
|
|
1213
|
+
`if-shell -F -t ${quoted} '#{&&:#{window_active},#{==:#{window-size},manual}}' ` +
|
|
1214
|
+
tmuxSingleQuote(`set-option -w -t ${quoted} window-size latest`) +
|
|
1215
|
+
" " +
|
|
1216
|
+
tmuxSingleQuote(`display-message -p -t ${quoted} ''`),
|
|
1217
|
+
2,
|
|
1218
|
+
1,
|
|
1219
|
+
() => {},
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1192
1223
|
/**
|
|
1193
1224
|
* Experimental: window-specific overrides stay private to this control client.
|
|
1194
1225
|
* Native tmux still falls back to the client size for unscoped neighbours;
|
|
@@ -1214,6 +1245,7 @@ export class SessionChannel {
|
|
|
1214
1245
|
const previous = this.fittedWindows.get(window.runtimeId);
|
|
1215
1246
|
if (previous?.cols === cols && previous.rows === rows) return;
|
|
1216
1247
|
this.input.flush();
|
|
1248
|
+
this.restoreWindowSizing(window.runtimeId);
|
|
1217
1249
|
this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}`);
|
|
1218
1250
|
this.fittedWindows.set(window.runtimeId, { cols, rows });
|
|
1219
1251
|
}
|
|
@@ -1288,6 +1320,7 @@ export class SessionChannel {
|
|
|
1288
1320
|
pane.subs.clear();
|
|
1289
1321
|
}
|
|
1290
1322
|
this.pendingLayoutOutput.clear();
|
|
1323
|
+
this.layoutNotificationOrdinals.clear();
|
|
1291
1324
|
this.layoutSubscribers.clear();
|
|
1292
1325
|
this.layoutAuthoritySubscribers.clear();
|
|
1293
1326
|
await this.io.dispose();
|
|
@@ -2851,6 +2884,15 @@ export class SessionChannel {
|
|
|
2851
2884
|
}
|
|
2852
2885
|
|
|
2853
2886
|
private onNotify(name: string, rest: string): void {
|
|
2887
|
+
if (
|
|
2888
|
+
name === "window-add" ||
|
|
2889
|
+
name === "window-close" ||
|
|
2890
|
+
name === "unlinked-window-add" ||
|
|
2891
|
+
name === "unlinked-window-close" ||
|
|
2892
|
+
name === "session-renamed" ||
|
|
2893
|
+
name === "sessions-changed"
|
|
2894
|
+
)
|
|
2895
|
+
this.opts.onWindowTopologyChanged?.();
|
|
2854
2896
|
if (name === "subscription-changed") {
|
|
2855
2897
|
const policy =
|
|
2856
2898
|
/^tmux-ide-scroll-on-clear\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+(%[0-9]+)\s+:\s+([01])\s*$/u.exec(
|
|
@@ -2976,6 +3018,7 @@ export class SessionChannel {
|
|
|
2976
3018
|
this.scheduleSync(); // never guess from a failed parse
|
|
2977
3019
|
return;
|
|
2978
3020
|
}
|
|
3021
|
+
this.layoutNotificationOrdinals.set(change.windowId, this.windowAuthorityOrdinal);
|
|
2979
3022
|
const pendingLayout = {
|
|
2980
3023
|
...parsed,
|
|
2981
3024
|
zoomed: change.zoomed,
|
|
@@ -3294,8 +3337,10 @@ export class SessionChannel {
|
|
|
3294
3337
|
previousCurrentWindow,
|
|
3295
3338
|
syncOrdinal,
|
|
3296
3339
|
);
|
|
3297
|
-
for (const pane of this.panesByRuntime.values())
|
|
3340
|
+
for (const pane of this.panesByRuntime.values()) {
|
|
3341
|
+
if (pane.windowRuntimeId && this.pendingLayoutOutput.has(pane.windowRuntimeId)) continue;
|
|
3298
3342
|
for (const sub of pane.subs) sub.resumeLayoutCapture?.(syncOrdinal);
|
|
3343
|
+
}
|
|
3299
3344
|
this.discovery.discover(listed);
|
|
3300
3345
|
}
|
|
3301
3346
|
|
|
@@ -3593,6 +3638,29 @@ export class SessionChannel {
|
|
|
3593
3638
|
previousCurrentWindow = this.currentWindow,
|
|
3594
3639
|
syncOrdinal?: number,
|
|
3595
3640
|
): void {
|
|
3641
|
+
// Control notifications in the same read chunk run before a promise-based
|
|
3642
|
+
// inventory read resumes. Preserve newer geometry (and its pending border
|
|
3643
|
+
// query) rather than letting that older inventory erase the notification.
|
|
3644
|
+
const newerLayouts = new Set(
|
|
3645
|
+
[...this.layoutNotificationOrdinals]
|
|
3646
|
+
.filter(([, ordinal]) => ordinal > stage.observedAuthorityOrdinal)
|
|
3647
|
+
.map(([runtimeId]) => runtimeId),
|
|
3648
|
+
);
|
|
3649
|
+
if (newerLayouts.size > 0) {
|
|
3650
|
+
stage = { ...stage, windows: new Map(stage.windows), layouts: new Map(stage.layouts) };
|
|
3651
|
+
for (const runtimeId of newerLayouts) {
|
|
3652
|
+
const currentLayout = this.layoutByWindow.get(runtimeId);
|
|
3653
|
+
const currentWindow = this.windowsByRuntime.get(runtimeId);
|
|
3654
|
+
const stagedWindow = stage.windows.get(runtimeId);
|
|
3655
|
+
if (currentLayout && currentWindow && stagedWindow) {
|
|
3656
|
+
stage.layouts.set(runtimeId, currentLayout);
|
|
3657
|
+
stage.windows.set(runtimeId, {
|
|
3658
|
+
...stagedWindow,
|
|
3659
|
+
paneBorderStatus: currentWindow.paneBorderStatus,
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3596
3664
|
this.windowLinkAuthority?.reconcile(stage.links);
|
|
3597
3665
|
this.latestWindowStage = stage;
|
|
3598
3666
|
const changedWindows = new Set<string>();
|
|
@@ -3647,8 +3715,14 @@ export class SessionChannel {
|
|
|
3647
3715
|
),
|
|
3648
3716
|
);
|
|
3649
3717
|
for (const runtimeId of this.pendingLayoutOutput.keys())
|
|
3650
|
-
if (!stage.windows.has(runtimeId)
|
|
3651
|
-
|
|
3718
|
+
if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
|
|
3719
|
+
this.pendingLayoutOutput.delete(runtimeId);
|
|
3720
|
+
for (const runtimeId of this.layoutNotificationOrdinals.keys())
|
|
3721
|
+
if (!stage.windows.has(runtimeId) && !newerLayouts.has(runtimeId))
|
|
3722
|
+
this.layoutNotificationOrdinals.delete(runtimeId);
|
|
3723
|
+
for (const runtimeId of layoutEmits)
|
|
3724
|
+
if (!newerLayouts.has(runtimeId) || !this.pendingLayoutOutput.has(runtimeId))
|
|
3725
|
+
this.releasePendingLayout(runtimeId, syncOrdinal);
|
|
3652
3726
|
this.emitLayoutAuthority();
|
|
3653
3727
|
}
|
|
3654
3728
|
|
|
@@ -3686,6 +3760,7 @@ export class SessionChannel {
|
|
|
3686
3760
|
}
|
|
3687
3761
|
|
|
3688
3762
|
private async stageWindows(target = this.opts.session): Promise<WindowSyncStage> {
|
|
3763
|
+
const observedAuthorityOrdinal = this.windowAuthorityOrdinal;
|
|
3689
3764
|
const lines = await this.io.request(
|
|
3690
3765
|
`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}"`,
|
|
3691
3766
|
);
|
|
@@ -3791,6 +3866,10 @@ export class SessionChannel {
|
|
|
3791
3866
|
if (!unzoomed) throw new Error(`full window layout for ${this.opts.session} is malformed`);
|
|
3792
3867
|
nextLayoutByWindow.set(row.runtimeId, { ...parsed, zoomed: row.zoomed, unzoomed });
|
|
3793
3868
|
}
|
|
3869
|
+
// Publish validated physical membership before any identity repair writes.
|
|
3870
|
+
// The receiving session may introduce duplicate stamps by linking a window.
|
|
3871
|
+
this.opts.onWindowMembership?.([...backingRows.keys()]);
|
|
3872
|
+
await this.opts.beforeIdentityRepair?.();
|
|
3794
3873
|
// Valid unique stamps are identity; missing/invalid/duplicated stamps are
|
|
3795
3874
|
// ALL regenerated and stamped back (the pane policy, applied to windows).
|
|
3796
3875
|
const stampCounts = new Map<string, number>();
|
|
@@ -3808,7 +3887,7 @@ export class SessionChannel {
|
|
|
3808
3887
|
let semanticId: string | null = null;
|
|
3809
3888
|
if (row.stamp && stampCounts.get(row.stamp) === 1) {
|
|
3810
3889
|
semanticId = row.stamp;
|
|
3811
|
-
} else {
|
|
3890
|
+
} else if (!this.opts.hasSharedWindowConflict?.()) {
|
|
3812
3891
|
repairedIdentity = true;
|
|
3813
3892
|
let candidate: string | null = null;
|
|
3814
3893
|
for (let attempt = 0; attempt < 32 && !candidate; attempt += 1) {
|
|
@@ -3846,6 +3925,7 @@ export class SessionChannel {
|
|
|
3846
3925
|
});
|
|
3847
3926
|
}
|
|
3848
3927
|
return {
|
|
3928
|
+
observedAuthorityOrdinal,
|
|
3849
3929
|
windows: next,
|
|
3850
3930
|
layouts: nextLayoutByWindow,
|
|
3851
3931
|
currentWindow: nextCurrentWindow,
|
|
@@ -3864,6 +3944,10 @@ export class SessionChannel {
|
|
|
3864
3944
|
listed: ReadonlySet<string>,
|
|
3865
3945
|
): Promise<boolean> {
|
|
3866
3946
|
if (this.disposed) return false;
|
|
3947
|
+
if (this.opts.hasSharedWindowConflict?.()) {
|
|
3948
|
+
this.settleFirstJoin();
|
|
3949
|
+
return false;
|
|
3950
|
+
}
|
|
3867
3951
|
const snapshots: WorkspaceTmuxPaneSnapshot[] = descriptors
|
|
3868
3952
|
.filter((descriptor) => listed.has(descriptor.runtimePaneId))
|
|
3869
3953
|
.map((descriptor) => ({
|
|
@@ -4005,6 +4089,10 @@ export class SessionChannel {
|
|
|
4005
4089
|
descriptors: readonly SessionPaneDescriptor[],
|
|
4006
4090
|
layouts: ReadonlyMap<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>,
|
|
4007
4091
|
): Promise<boolean> {
|
|
4092
|
+
if (this.opts.hasSharedWindowConflict?.())
|
|
4093
|
+
throw new Error(
|
|
4094
|
+
"Linked windows across controlled sessions are unsupported. Unlink the shared window before controlling this session.",
|
|
4095
|
+
);
|
|
4008
4096
|
const rectForRuntime = (runtimePaneId: string): WorkspacePaneRect => {
|
|
4009
4097
|
for (const layout of layouts.values()) {
|
|
4010
4098
|
const leaf = layout.leaves.find(({ id }) => id === runtimePaneId);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Event-fed physical window membership for one pinned tmux server. */
|
|
2
|
+
export class SharedWindowIndex<Owner> {
|
|
3
|
+
readonly #windows = new Map<string, Set<Owner>>();
|
|
4
|
+
readonly #owners = new Map<Owner, Set<string>>();
|
|
5
|
+
readonly #conflicts = new Set<Owner>();
|
|
6
|
+
|
|
7
|
+
conflicted(owner: Owner): boolean {
|
|
8
|
+
return this.#conflicts.has(owner);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
update(owner: Owner, windows: readonly string[]): Map<Owner, boolean> {
|
|
12
|
+
const previous = this.#owners.get(owner) ?? new Set<string>();
|
|
13
|
+
const next = new Set(windows);
|
|
14
|
+
const affected = new Set<Owner>([owner]);
|
|
15
|
+
for (const window of new Set([...previous, ...next])) {
|
|
16
|
+
for (const peer of this.#windows.get(window) ?? []) affected.add(peer);
|
|
17
|
+
if (previous.has(window) && !next.has(window)) {
|
|
18
|
+
const peers = this.#windows.get(window)!;
|
|
19
|
+
peers.delete(owner);
|
|
20
|
+
if (!peers.size) this.#windows.delete(window);
|
|
21
|
+
}
|
|
22
|
+
if (next.has(window)) {
|
|
23
|
+
const peers = this.#windows.get(window) ?? new Set<Owner>();
|
|
24
|
+
peers.add(owner);
|
|
25
|
+
this.#windows.set(window, peers);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (next.size) this.#owners.set(owner, next);
|
|
29
|
+
else this.#owners.delete(owner);
|
|
30
|
+
const changed = new Map<Owner, boolean>();
|
|
31
|
+
for (const peer of affected) {
|
|
32
|
+
const conflict = [...(this.#owners.get(peer) ?? [])].some(
|
|
33
|
+
(window) => (this.#windows.get(window)?.size ?? 0) > 1,
|
|
34
|
+
);
|
|
35
|
+
if (conflict === this.#conflicts.has(peer)) continue;
|
|
36
|
+
if (conflict) this.#conflicts.add(peer);
|
|
37
|
+
else this.#conflicts.delete(peer);
|
|
38
|
+
changed.set(peer, conflict);
|
|
39
|
+
}
|
|
40
|
+
return changed;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -2219,7 +2219,18 @@ export class PaneStreamLiveConnection {
|
|
|
2219
2219
|
!this.#sessionRuntimeBinding.authoritySnapshot
|
|
2220
2220
|
)
|
|
2221
2221
|
return this.#failProtocol("protocol-error");
|
|
2222
|
-
|
|
2222
|
+
let lease: SessionRuntimeAuthorityLease | null = null;
|
|
2223
|
+
try {
|
|
2224
|
+
lease = this.#sessionRuntimeBinding.requestAuthority(frame.authority);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
// Topology changes can temporarily prevent ownership. Reject this
|
|
2227
|
+
// request without letting an expected conflict terminate the daemon.
|
|
2228
|
+
if (
|
|
2229
|
+
!(error instanceof SessionRuntimeControllerLeaseError) ||
|
|
2230
|
+
error.code !== "controller-conflict"
|
|
2231
|
+
)
|
|
2232
|
+
return this.#failProtocol("input-rejected");
|
|
2233
|
+
}
|
|
2223
2234
|
if (lease) this.#requestedAuthorities.add(frame.authority);
|
|
2224
2235
|
this.#sendFrame(null, {
|
|
2225
2236
|
type: "authority-receipt",
|
|
@@ -26,6 +26,7 @@ export interface SessionRuntimeMultiplexerBackendOptions {
|
|
|
26
26
|
| "bindExecutionSource"
|
|
27
27
|
| "assertExecutionHandle"
|
|
28
28
|
| "submitPaneCredentialIntent"
|
|
29
|
+
| "verifyWindowOwnership"
|
|
29
30
|
>;
|
|
30
31
|
readonly resolveSession: (workspaceName: string) => string | null;
|
|
31
32
|
readonly resolvePaneSourceBinding?: (
|
|
@@ -192,6 +193,7 @@ export function createSessionRuntimeMultiplexerBackend(
|
|
|
192
193
|
if (!ownerAuthorized) {
|
|
193
194
|
throw new Error("Semantic mutation requires a live host, pane, or owner principal");
|
|
194
195
|
}
|
|
196
|
+
await options.registry.verifyWindowOwnership();
|
|
195
197
|
const owner = acquireOwner(session);
|
|
196
198
|
try {
|
|
197
199
|
let lease: ReturnType<SessionRuntimeConsumer["acquireController"]>;
|