tmux-ide 2.9.2 → 2.9.3
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/README.md +1 -1
- package/bin/cli.js +2071 -625
- package/package.json +14 -5
- 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/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/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/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
|
@@ -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"]>;
|
|
@@ -273,7 +273,10 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
273
273
|
readonly #createTraceCorrelator: (scheduler: SessionRuntimeScheduler) => RuntimeTraceCorrelator;
|
|
274
274
|
readonly #sessions = new Map<string, SessionRuntime>();
|
|
275
275
|
readonly #proofPrewarmOwnership = new Map<SessionRuntime, { owned: boolean; claims: number }>();
|
|
276
|
-
readonly #trustedInventoryTokens = new WeakMap<
|
|
276
|
+
readonly #trustedInventoryTokens = new WeakMap<
|
|
277
|
+
object,
|
|
278
|
+
{ runtime: SessionRuntime; epoch: number }
|
|
279
|
+
>();
|
|
277
280
|
readonly #executionHandles = new WeakMap<object, ExecutionHandleState>();
|
|
278
281
|
readonly #stopExitObserver: () => void;
|
|
279
282
|
#disposed = false;
|
|
@@ -397,6 +400,10 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
397
400
|
this.#mirror = new MirrorService({
|
|
398
401
|
...options.mirror,
|
|
399
402
|
...diagnosticMirrorOptions,
|
|
403
|
+
onSharedWindowConflict: (session, conflict) => {
|
|
404
|
+
options.mirror?.onSharedWindowConflict?.(session, conflict);
|
|
405
|
+
if (conflict) this.#sessions.get(session)?.invalidateSharedWindowAuthority();
|
|
406
|
+
},
|
|
400
407
|
onNativeClientActivity: (session) => {
|
|
401
408
|
options.mirror?.onNativeClientActivity?.(session);
|
|
402
409
|
this.#sessions.get(session)?.noteNativeGeometryActivity();
|
|
@@ -433,6 +440,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
433
440
|
signal?.throwIfAborted();
|
|
434
441
|
const runtime = this.#runtime(session);
|
|
435
442
|
await abortable(runtime.whenReady(), signal);
|
|
443
|
+
await abortable(this.#mirror.verifyRegisteredWindows(), signal);
|
|
436
444
|
if (this.#sessions.get(session) !== runtime) {
|
|
437
445
|
throw new Error(`SessionRuntime ${session} was retired while prewarming`);
|
|
438
446
|
}
|
|
@@ -493,6 +501,7 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
493
501
|
throw new Error(`SessionRuntime ${session} is attached to a different tmux identity`);
|
|
494
502
|
}
|
|
495
503
|
signal?.throwIfAborted();
|
|
504
|
+
await abortable(this.#mirror.verifyRegisteredWindows(), signal);
|
|
496
505
|
runtime.qualifyTrustedInventory(runtimeSessionId);
|
|
497
506
|
} finally {
|
|
498
507
|
this.#releaseProofPrewarmRuntime(runtime);
|
|
@@ -521,28 +530,34 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
521
530
|
if (!runtime?.trustedInventoryQualified()) {
|
|
522
531
|
throw new Error(`SessionRuntime ${session} has no proof-qualified inventory authority`);
|
|
523
532
|
}
|
|
533
|
+
const epoch = runtime.trustedInventoryEpoch;
|
|
524
534
|
const inventory = await this.#observeTerminalAttempt(
|
|
525
535
|
"terminal-trusted-inventory-attempt",
|
|
526
536
|
abortable(runtime.describeTrustedInventory(), signal),
|
|
527
537
|
);
|
|
528
|
-
if (
|
|
538
|
+
if (
|
|
539
|
+
this.#sessions.get(session) !== runtime ||
|
|
540
|
+
!runtime.trustedInventoryQualified() ||
|
|
541
|
+
runtime.trustedInventoryEpoch !== epoch
|
|
542
|
+
) {
|
|
529
543
|
throw new Error(`SessionRuntime ${session} changed during trusted inventory discovery`);
|
|
530
544
|
}
|
|
531
545
|
if (!inventory) {
|
|
532
546
|
throw new Error(`SessionRuntime ${session} lost its retained inventory authority`);
|
|
533
547
|
}
|
|
534
548
|
const token = Object.freeze({});
|
|
535
|
-
this.#trustedInventoryTokens.set(token, runtime);
|
|
549
|
+
this.#trustedInventoryTokens.set(token, { runtime, epoch });
|
|
536
550
|
return Object.freeze({ inventory, token });
|
|
537
551
|
}
|
|
538
552
|
|
|
539
553
|
isTrustedSessionInventoryCandidateCurrent(session: string, token: object): boolean {
|
|
540
554
|
if (this.#disposed) return false;
|
|
541
|
-
const
|
|
555
|
+
const candidate = this.#trustedInventoryTokens.get(token);
|
|
542
556
|
return (
|
|
543
|
-
|
|
544
|
-
this.#sessions.get(session) === runtime &&
|
|
545
|
-
runtime.
|
|
557
|
+
candidate !== undefined &&
|
|
558
|
+
this.#sessions.get(session) === candidate.runtime &&
|
|
559
|
+
candidate.runtime.trustedInventoryEpoch === candidate.epoch &&
|
|
560
|
+
candidate.runtime.trustedInventoryQualified()
|
|
546
561
|
);
|
|
547
562
|
}
|
|
548
563
|
|
|
@@ -567,6 +582,18 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
567
582
|
return this.#mirror.executeWindowLinkAction(session, request);
|
|
568
583
|
}
|
|
569
584
|
|
|
585
|
+
/** Resolve pending ownership before an asynchronous owner-authorized mutation. */
|
|
586
|
+
async verifyWindowOwnership(): Promise<void> {
|
|
587
|
+
await this.#mirror.verifyRegisteredWindows();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Install the bounded registered-session membership reader. */
|
|
591
|
+
setRegisteredWindowReader(
|
|
592
|
+
reader: import("../mirror/registered-window-guard.ts").RegisteredWindowReader,
|
|
593
|
+
): ReturnType<MirrorService["setRegisteredWindowReader"]> {
|
|
594
|
+
return this.#mirror.setRegisteredWindowReader(reader);
|
|
595
|
+
}
|
|
596
|
+
|
|
570
597
|
/** Retire one no-longer-registered session without disturbing siblings. */
|
|
571
598
|
async retireSession(session: string): Promise<void> {
|
|
572
599
|
const runtime = this.#sessions.get(session);
|
|
@@ -1105,6 +1132,11 @@ class SessionRuntime {
|
|
|
1105
1132
|
readonly #releasedLeases = new Set<string>();
|
|
1106
1133
|
#disposed = false;
|
|
1107
1134
|
#trustedInventoryRuntimeSessionId: string | null = null;
|
|
1135
|
+
#trustedInventoryEpoch = 0;
|
|
1136
|
+
|
|
1137
|
+
get trustedInventoryEpoch(): number {
|
|
1138
|
+
return this.#trustedInventoryEpoch;
|
|
1139
|
+
}
|
|
1108
1140
|
#activeCausalCellProbes = 0;
|
|
1109
1141
|
|
|
1110
1142
|
constructor(
|
|
@@ -1195,6 +1227,7 @@ class SessionRuntime {
|
|
|
1195
1227
|
clientId: string,
|
|
1196
1228
|
authority: SessionRuntimeAuthorityKind,
|
|
1197
1229
|
): SessionRuntimeAuthorityLease | null {
|
|
1230
|
+
this.assertNoSharedWindow();
|
|
1198
1231
|
if (authority === "input" && this.#controllerClientId !== clientId) {
|
|
1199
1232
|
// Input's executable proof is the compatibility controller lease. The
|
|
1200
1233
|
// transport synchronization seam must establish/handoff it first.
|
|
@@ -1261,6 +1294,7 @@ class SessionRuntime {
|
|
|
1261
1294
|
}
|
|
1262
1295
|
|
|
1263
1296
|
acquireController(clientId: string): SessionRuntimeControllerLease {
|
|
1297
|
+
this.assertNoSharedWindow();
|
|
1264
1298
|
this.#assertConnected(clientId);
|
|
1265
1299
|
if (this.#controllerClientId === clientId) {
|
|
1266
1300
|
this.#authority.updatePresence(clientId, "foreground");
|
|
@@ -1335,6 +1369,7 @@ class SessionRuntime {
|
|
|
1335
1369
|
}
|
|
1336
1370
|
|
|
1337
1371
|
assertController(lease: SessionRuntimeControllerLease, callerClientId?: string): void {
|
|
1372
|
+
this.assertNoSharedWindow();
|
|
1338
1373
|
const parsedLease = this.#validatedLease(lease);
|
|
1339
1374
|
if (
|
|
1340
1375
|
parsedLease.generation !== this.generation ||
|
|
@@ -1547,6 +1582,7 @@ class SessionRuntime {
|
|
|
1547
1582
|
rows: number,
|
|
1548
1583
|
semanticWindowId?: string,
|
|
1549
1584
|
): void {
|
|
1585
|
+
this.assertNoSharedWindow();
|
|
1550
1586
|
const parsed = SessionRuntimeAuthorityLeaseSchemaZ.parse(lease);
|
|
1551
1587
|
let exact: SessionRuntimeAuthorityLease;
|
|
1552
1588
|
try {
|
|
@@ -1594,6 +1630,7 @@ class SessionRuntime {
|
|
|
1594
1630
|
}
|
|
1595
1631
|
|
|
1596
1632
|
qualifyTrustedInventory(runtimeSessionId: string): void {
|
|
1633
|
+
this.assertNoSharedWindow();
|
|
1597
1634
|
if (this.#disposed || !this.#retention) {
|
|
1598
1635
|
throw new Error(`SessionRuntime ${this.session} is not retained`);
|
|
1599
1636
|
}
|
|
@@ -1601,7 +1638,11 @@ class SessionRuntime {
|
|
|
1601
1638
|
}
|
|
1602
1639
|
|
|
1603
1640
|
trustedInventoryQualified(): boolean {
|
|
1604
|
-
return
|
|
1641
|
+
return (
|
|
1642
|
+
!this.#disposed &&
|
|
1643
|
+
this.#trustedInventoryRuntimeSessionId !== null &&
|
|
1644
|
+
!this.#mirror.hasSharedWindowConflict(this.session)
|
|
1645
|
+
);
|
|
1605
1646
|
}
|
|
1606
1647
|
|
|
1607
1648
|
async describeTrustedInventory(): Promise<TrustedMirrorSessionInventory | null> {
|
|
@@ -1812,8 +1853,28 @@ class SessionRuntime {
|
|
|
1812
1853
|
this.#publishAuthority();
|
|
1813
1854
|
}
|
|
1814
1855
|
|
|
1856
|
+
private assertNoSharedWindow(): void {
|
|
1857
|
+
const message = this.#mirror.windowOwnershipMessage(this.session);
|
|
1858
|
+
if (message) {
|
|
1859
|
+
throw new SessionRuntimeControllerLeaseError("controller-conflict", message);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
invalidateSharedWindowAuthority(): void {
|
|
1864
|
+
this.#trustedInventoryRuntimeSessionId = null;
|
|
1865
|
+
this.#trustedInventoryEpoch += 1;
|
|
1866
|
+
this.#clearController();
|
|
1867
|
+
for (const clientId of this.#consumersByClientId.keys()) {
|
|
1868
|
+
for (const authority of ["input", "focus", "geometry"] as const)
|
|
1869
|
+
this.#authority.release(clientId, authority);
|
|
1870
|
+
}
|
|
1871
|
+
this.#completedHandoffs.clear();
|
|
1872
|
+
this.#publishAuthority();
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1815
1875
|
noteControlExit(): void {
|
|
1816
1876
|
this.#trustedInventoryRuntimeSessionId = null;
|
|
1877
|
+
this.#trustedInventoryEpoch += 1;
|
|
1817
1878
|
const retention = this.#retention;
|
|
1818
1879
|
this.#retention = null;
|
|
1819
1880
|
this.#startPromise = null;
|
|
@@ -1833,6 +1894,7 @@ class SessionRuntime {
|
|
|
1833
1894
|
if (this.#disposed) return;
|
|
1834
1895
|
this.#disposed = true;
|
|
1835
1896
|
this.#trustedInventoryRuntimeSessionId = null;
|
|
1897
|
+
this.#trustedInventoryEpoch += 1;
|
|
1836
1898
|
const consumers = [...this.#consumers];
|
|
1837
1899
|
await Promise.allSettled(consumers.map((consumer) => consumer.close()));
|
|
1838
1900
|
this.#clearController();
|
|
@@ -509,7 +509,9 @@ export class SessionSemanticMutationExecutor {
|
|
|
509
509
|
if (this.#observability.enabled && this.#options.traceAuthority) {
|
|
510
510
|
try {
|
|
511
511
|
trace = this.#observability.beginTrace(
|
|
512
|
-
intent.verb === "workspace.pane.select"
|
|
512
|
+
intent.verb === "workspace.pane.select" || intent.verb === "workspace.window.link.select"
|
|
513
|
+
? "window-switch"
|
|
514
|
+
: "semantic-mutation",
|
|
513
515
|
this.#options.traceAuthority,
|
|
514
516
|
operationId,
|
|
515
517
|
);
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* Rows span the box interior in x: [left+1, left+width-1).
|
|
17
17
|
*/
|
|
18
18
|
import { fuzzyFilter } from "../team/fuzzy.ts";
|
|
19
|
+
import { wrapText } from "./terminal-text.ts";
|
|
20
|
+
export { wrapText } from "./terminal-text.ts";
|
|
19
21
|
|
|
20
22
|
// ── Specs ────────────────────────────────────────────────────────────────────
|
|
21
23
|
|
|
@@ -176,38 +178,6 @@ export function dialogPos(
|
|
|
176
178
|
};
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
/** PURE — greedy word-wrap for the confirm body (never returns empty for
|
|
180
|
-
* non-empty text; words longer than the width hard-break). */
|
|
181
|
-
export function wrapText(text: string, width: number): string[] {
|
|
182
|
-
if (width <= 0) return [text];
|
|
183
|
-
const out: string[] = [];
|
|
184
|
-
for (const para of text.split("\n")) {
|
|
185
|
-
let line = "";
|
|
186
|
-
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
187
|
-
if (line.length === 0) {
|
|
188
|
-
let w = word;
|
|
189
|
-
while (w.length > width) {
|
|
190
|
-
out.push(w.slice(0, width));
|
|
191
|
-
w = w.slice(width);
|
|
192
|
-
}
|
|
193
|
-
line = w;
|
|
194
|
-
} else if (line.length + 1 + word.length <= width) {
|
|
195
|
-
line += ` ${word}`;
|
|
196
|
-
} else {
|
|
197
|
-
out.push(line);
|
|
198
|
-
let w = word;
|
|
199
|
-
while (w.length > width) {
|
|
200
|
-
out.push(w.slice(0, width));
|
|
201
|
-
w = w.slice(width);
|
|
202
|
-
}
|
|
203
|
-
line = w;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (line.length > 0 || para.length === 0) out.push(line);
|
|
207
|
-
}
|
|
208
|
-
return out.length > 0 ? out : [""];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
181
|
/** PURE — the interior text width of the box (borders + 1-cell padding each side). */
|
|
212
182
|
export function dialogInnerW(width: number): number {
|
|
213
183
|
return width - 4;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
openSshDaemonTransport,
|
|
21
21
|
probeSshDaemonIdentity,
|
|
22
22
|
} from "../../../lib/ssh-daemon-transport.ts";
|
|
23
|
+
import { tuiPerfMark } from "./application-performance-log.ts";
|
|
23
24
|
|
|
24
25
|
type Connection = Awaited<ReturnType<typeof openSshDaemonTransport>>;
|
|
25
26
|
type Listener = (generation: string | null) => void;
|
|
@@ -136,6 +137,13 @@ export function createApplicationDaemonAuthority(
|
|
|
136
137
|
const statusListeners = new Set<() => void>();
|
|
137
138
|
const updateStatus = (next: FleetConnectionStatus) => {
|
|
138
139
|
diagnostic = Object.freeze(next);
|
|
140
|
+
tuiPerfMark("machine-connection-status", {
|
|
141
|
+
machine: label,
|
|
142
|
+
...next,
|
|
143
|
+
instanceId: remote?.instanceId ?? null,
|
|
144
|
+
pid: remote?.pid ?? null,
|
|
145
|
+
port: remote?.port ?? null,
|
|
146
|
+
});
|
|
139
147
|
for (const listener of statusListeners) {
|
|
140
148
|
try {
|
|
141
149
|
listener();
|
|
@@ -80,6 +80,22 @@ export function projectHomeFleet(
|
|
|
80
80
|
machine.state !== "ready" ||
|
|
81
81
|
!groups.find((group) => group.machineId === machine.id)?.available,
|
|
82
82
|
);
|
|
83
|
+
const activityPending = (machineId: string) => {
|
|
84
|
+
const observation = groups.find((group) => group.machineId === machineId)?.observation;
|
|
85
|
+
// A live observation whose binding is no longer available belongs to a
|
|
86
|
+
// retiring endpoint; its replacement must publish before it is current.
|
|
87
|
+
return !observation || observation.phase === "loading" || observation.phase === "live";
|
|
88
|
+
};
|
|
89
|
+
const coverageNotes = missing.map((machine) => {
|
|
90
|
+
if (machine.state !== "ready") return `${machine.label}: ${machine.state}`;
|
|
91
|
+
const observation = groups.find((group) => group.machineId === machine.id)?.observation;
|
|
92
|
+
const status = activityPending(machine.id)
|
|
93
|
+
? "loading agent activity"
|
|
94
|
+
: observation?.phase === "partial"
|
|
95
|
+
? "agent activity incomplete"
|
|
96
|
+
: "agent activity unavailable";
|
|
97
|
+
return `${machine.label}: connected; ${status}`;
|
|
98
|
+
});
|
|
83
99
|
const query = filter.query?.trim().toLocaleLowerCase() ?? "";
|
|
84
100
|
const matching = query
|
|
85
101
|
? rows.filter((row) =>
|
|
@@ -98,7 +114,8 @@ export function projectHomeFleet(
|
|
|
98
114
|
phase: missing.length
|
|
99
115
|
? rows.length || observedSessions
|
|
100
116
|
? "partial"
|
|
101
|
-
: machines.some((machine) => machine.state === "connecting")
|
|
117
|
+
: machines.some((machine) => machine.state === "connecting") ||
|
|
118
|
+
missing.some((machine) => machine.state === "ready" && activityPending(machine.id))
|
|
102
119
|
? "loading"
|
|
103
120
|
: "unavailable"
|
|
104
121
|
: "live",
|
|
@@ -111,7 +128,7 @@ export function projectHomeFleet(
|
|
|
111
128
|
refreshingSessionKeys: [],
|
|
112
129
|
unavailableSessionKeys: [...unavailable],
|
|
113
130
|
note: missing.length
|
|
114
|
-
? `${
|
|
131
|
+
? `${coverageNotes.join(" · ")}${unavailable.size > 0 ? " · unavailable rows show last observed activity" : ""}`
|
|
115
132
|
: rows.length === 0 && filter.attentionOnly
|
|
116
133
|
? "No agents need attention in this view."
|
|
117
134
|
: null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { machineResourceKey } from "@tmux-ide/core";
|
|
2
3
|
import {
|
|
3
4
|
applicationMachineAuthorityManager,
|
|
@@ -69,7 +70,7 @@ export function createApplicationMachineAgents(options: {
|
|
|
69
70
|
let started = false;
|
|
70
71
|
const publish = () => {
|
|
71
72
|
if (disposed) return;
|
|
72
|
-
|
|
73
|
+
const next = Object.freeze(
|
|
73
74
|
catalog.groups.map((group) => {
|
|
74
75
|
const entry = entries.get(group.id);
|
|
75
76
|
const current = !!entry && entry.binding === bindingFor(entry.handle);
|
|
@@ -95,6 +96,10 @@ export function createApplicationMachineAgents(options: {
|
|
|
95
96
|
});
|
|
96
97
|
}),
|
|
97
98
|
);
|
|
99
|
+
// Fresh authority checks above still run on every observation. Identical
|
|
100
|
+
// presentation data must not invalidate the Home/sidebar/tab subscribers.
|
|
101
|
+
if (isDeepStrictEqual(snapshot, next)) return;
|
|
102
|
+
snapshot = next;
|
|
98
103
|
for (const listener of listeners) {
|
|
99
104
|
try {
|
|
100
105
|
listener(snapshot);
|