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
package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js
CHANGED
|
@@ -84,6 +84,64 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
84
84
|
let diagnosticWindowFrame = null;
|
|
85
85
|
let diagnosticWindowFrameExpiresAtMicros = 0;
|
|
86
86
|
let inputAuthorityIdentity = null;
|
|
87
|
+
// Pane selection belongs to the control connection, which can outlive a
|
|
88
|
+
// terminal renderer rebuild. Never carry selection or input across reconnects.
|
|
89
|
+
const controlIdentity = (snapshot) => {
|
|
90
|
+
if (!snapshot ||
|
|
91
|
+
!["live", "rebinding"].includes(snapshot.status) ||
|
|
92
|
+
!snapshot.connection ||
|
|
93
|
+
!snapshot.client ||
|
|
94
|
+
!snapshot.daemonGeneration)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
const generation = snapshot.client.getSnapshot().generation;
|
|
98
|
+
if (!Number.isSafeInteger(generation))
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
connection: snapshot.connection,
|
|
102
|
+
client: snapshot.client,
|
|
103
|
+
daemon: snapshot.daemonGeneration,
|
|
104
|
+
generation,
|
|
105
|
+
workspace: snapshot.connection.workspaceName,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const sameControl = (left, right) => left !== null &&
|
|
113
|
+
right !== null &&
|
|
114
|
+
left.connection === right.connection &&
|
|
115
|
+
left.client === right.client &&
|
|
116
|
+
left.daemon === right.daemon &&
|
|
117
|
+
left.generation === right.generation &&
|
|
118
|
+
left.workspace === right.workspace;
|
|
119
|
+
let selectedControl = null;
|
|
120
|
+
const waitingInput = new Set();
|
|
121
|
+
let waitingInputBytes = 0;
|
|
122
|
+
let releasingInput = null;
|
|
123
|
+
const awaitInputRuntime = (weight) => {
|
|
124
|
+
const control = controlIdentity(options.generation());
|
|
125
|
+
if (!control || waitingInput.size >= 64 || waitingInputBytes + weight > 1024 * 1024)
|
|
126
|
+
return Promise.resolve(false);
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const entry = {
|
|
129
|
+
control,
|
|
130
|
+
weight,
|
|
131
|
+
settle: (ready) => {
|
|
132
|
+
if (!waitingInput.delete(entry))
|
|
133
|
+
return;
|
|
134
|
+
waitingInputBytes -= weight;
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
resolve(ready);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const timer = setTimeout(() => entry.settle(false), 5000);
|
|
140
|
+
timer.unref?.();
|
|
141
|
+
waitingInput.add(entry);
|
|
142
|
+
waitingInputBytes += weight;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
87
145
|
const armDiagnosticWindowFrame = (pending) => {
|
|
88
146
|
diagnosticWindowFrame = pending;
|
|
89
147
|
diagnosticWindowFrameExpiresAtMicros = pending.startedAtMicros + 6_000_000;
|
|
@@ -356,6 +414,71 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
356
414
|
pendingWindowSwitch = null;
|
|
357
415
|
}
|
|
358
416
|
};
|
|
417
|
+
const beginWindowSwitch = (pane, target, windowLink) => {
|
|
418
|
+
pendingWindowSwitch = null;
|
|
419
|
+
if (options.diagnosticsEnabled) {
|
|
420
|
+
try {
|
|
421
|
+
const active = options.generation();
|
|
422
|
+
const identity = active?.adapter?.paneCanonicalIdentity(pane);
|
|
423
|
+
const clientGeneration = active?.client?.getSnapshot().generation;
|
|
424
|
+
const startedAtMicros = diagnosticNowMicros();
|
|
425
|
+
if (active?.status === "live" &&
|
|
426
|
+
active.daemonGeneration &&
|
|
427
|
+
identity &&
|
|
428
|
+
Number.isSafeInteger(clientGeneration) &&
|
|
429
|
+
startedAtMicros !== null) {
|
|
430
|
+
pendingWindowSwitch = {
|
|
431
|
+
selectionKind: windowLink ? "window-link" : "pane",
|
|
432
|
+
...(windowLink ? { windowLink } : {}),
|
|
433
|
+
traceId: createTraceId(),
|
|
434
|
+
target,
|
|
435
|
+
paneId: pane,
|
|
436
|
+
startedAtMicros,
|
|
437
|
+
daemonGeneration: active.daemonGeneration,
|
|
438
|
+
clientGeneration: clientGeneration,
|
|
439
|
+
rendererEpoch: active.rendererEpoch,
|
|
440
|
+
sourceEpoch: identity.sourceEpoch,
|
|
441
|
+
generation: identity.generation,
|
|
442
|
+
incarnation: identity.incarnation,
|
|
443
|
+
revision: identity.revision,
|
|
444
|
+
stateHash: identity.stateHash,
|
|
445
|
+
cols: identity.cols,
|
|
446
|
+
rows: identity.rows,
|
|
447
|
+
layoutPublished: false,
|
|
448
|
+
selectionApplied: false,
|
|
449
|
+
presentationPublished: false,
|
|
450
|
+
followUpRequested: false,
|
|
451
|
+
...(options.detailedWindowSwitchTiming
|
|
452
|
+
? {
|
|
453
|
+
timing: {
|
|
454
|
+
receiptAtMicros: null,
|
|
455
|
+
layoutAtMicros: null,
|
|
456
|
+
presentationAtMicros: null,
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
: {}),
|
|
460
|
+
};
|
|
461
|
+
armDiagnosticWindowFrame({ kind: "window-switch", ...pendingWindowSwitch });
|
|
462
|
+
diagnose("window-switch-start", { ...pendingWindowSwitch });
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
diagnose("window-switch-untracked", {
|
|
466
|
+
paneId: pane,
|
|
467
|
+
target,
|
|
468
|
+
status: active?.status ?? null,
|
|
469
|
+
daemonGeneration: active?.daemonGeneration ?? null,
|
|
470
|
+
rendererEpoch: active?.rendererEpoch ?? null,
|
|
471
|
+
clientGeneration: clientGeneration ?? null,
|
|
472
|
+
identity: identity ?? null,
|
|
473
|
+
laneCounters: active?.fastLane?.lane.counters() ?? null,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
pendingWindowSwitch = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
359
482
|
let linkSelection = null;
|
|
360
483
|
const finishLinkSelection = (selected) => {
|
|
361
484
|
const pending = linkSelection;
|
|
@@ -363,6 +486,20 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
363
486
|
return;
|
|
364
487
|
linkSelection = null;
|
|
365
488
|
clearTimeout(pending.timer);
|
|
489
|
+
const diagnostic = pending.diagnostic;
|
|
490
|
+
if (diagnostic && pendingWindowSwitch === diagnostic) {
|
|
491
|
+
if (selected) {
|
|
492
|
+
diagnostic.selectionApplied = true;
|
|
493
|
+
maybeRequestWindowSwitchFrame();
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
diagnoseCritical(`window-switch:${diagnostic.traceId}:failed`, "window-switch-failed", {
|
|
497
|
+
...diagnostic,
|
|
498
|
+
reason: "window-link-selection-not-applied",
|
|
499
|
+
});
|
|
500
|
+
pendingWindowSwitch = null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
366
503
|
pending.resolve(selected);
|
|
367
504
|
};
|
|
368
505
|
const reconcileLinkSelection = (snapshot) => {
|
|
@@ -386,11 +523,18 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
386
523
|
select: async (paneId) => {
|
|
387
524
|
finishLinkSelection(false);
|
|
388
525
|
const expected = liveSelectionTarget();
|
|
526
|
+
const selectionControl = controlIdentity(options.generation());
|
|
389
527
|
const selecting = pendingWindowSwitch;
|
|
390
528
|
const operationId = selecting?.traceId;
|
|
391
529
|
const selectionFailure = { current: null };
|
|
392
530
|
const receipt = expected
|
|
393
|
-
? await selectTerminalPane(expected,
|
|
531
|
+
? await selectTerminalPane(expected, () => {
|
|
532
|
+
if (!selectionControl)
|
|
533
|
+
return liveSelectionTarget();
|
|
534
|
+
return sameControl(selectionControl, controlIdentity(options.generation()))
|
|
535
|
+
? expected
|
|
536
|
+
: null;
|
|
537
|
+
}, paneId, operationId, (failure) => {
|
|
394
538
|
selectionFailure.current = failure;
|
|
395
539
|
}, windowLinkForPane(options.layout(), paneId) ?? undefined)
|
|
396
540
|
: null;
|
|
@@ -455,6 +599,16 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
455
599
|
},
|
|
456
600
|
send: async (paneId, routed) => {
|
|
457
601
|
const { input, parserOrigin } = routed;
|
|
602
|
+
const selectionVersion = paneInput.selectionVersion;
|
|
603
|
+
if (options.generation()?.status === "rebinding") {
|
|
604
|
+
if (!(await awaitInputRuntime(Buffer.byteLength(input.data, "utf8"))))
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
else if (releasingInput) {
|
|
608
|
+
await releasingInput;
|
|
609
|
+
}
|
|
610
|
+
if (paneInput.selectionVersion !== selectionVersion || paneInput.focusedPane !== paneId)
|
|
611
|
+
return false;
|
|
458
612
|
const active = options.generation();
|
|
459
613
|
if (active?.status !== "live" || !active.fastLane)
|
|
460
614
|
return false;
|
|
@@ -534,7 +688,35 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
534
688
|
onFocusedPane: options.setFocusedPane,
|
|
535
689
|
});
|
|
536
690
|
return {
|
|
691
|
+
cancelPendingInput() {
|
|
692
|
+
finishLinkSelection(false);
|
|
693
|
+
paneInput.invalidateSelection();
|
|
694
|
+
for (const entry of waitingInput)
|
|
695
|
+
entry.settle(false);
|
|
696
|
+
},
|
|
537
697
|
adoptGeneration(snapshot) {
|
|
698
|
+
const control = controlIdentity(snapshot);
|
|
699
|
+
if (!sameControl(selectedControl, control)) {
|
|
700
|
+
finishLinkSelection(false);
|
|
701
|
+
paneInput.invalidateSelection();
|
|
702
|
+
}
|
|
703
|
+
selectedControl = control;
|
|
704
|
+
if (waitingInput.size > 0 && snapshot?.status === "live" && snapshot.fastLane) {
|
|
705
|
+
// Old waiters resume first; input admitted synchronously by the next
|
|
706
|
+
// layout publication must not overtake them in this microtask turn.
|
|
707
|
+
const barrier = Promise.resolve();
|
|
708
|
+
releasingInput = barrier;
|
|
709
|
+
queueMicrotask(() => {
|
|
710
|
+
if (releasingInput === barrier)
|
|
711
|
+
releasingInput = null;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
for (const entry of waitingInput) {
|
|
715
|
+
if (!sameControl(entry.control, control))
|
|
716
|
+
entry.settle(false);
|
|
717
|
+
else if (snapshot?.status === "live" && snapshot.fastLane)
|
|
718
|
+
entry.settle(true);
|
|
719
|
+
}
|
|
538
720
|
let next = null;
|
|
539
721
|
if (snapshot?.status === "live" &&
|
|
540
722
|
snapshot.daemonGeneration &&
|
|
@@ -570,7 +752,6 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
570
752
|
next.adapter !== inputAuthorityIdentity.adapter);
|
|
571
753
|
if (next === null || replaced) {
|
|
572
754
|
finishLinkSelection(false);
|
|
573
|
-
paneInput.invalidateSelection();
|
|
574
755
|
pendingWindowSwitch = null;
|
|
575
756
|
pendingWindowRename = null;
|
|
576
757
|
diagnosticWindowFrame = null;
|
|
@@ -715,6 +896,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
715
896
|
observed.expectedSemanticWindowId !== target.expectedSemanticWindowId)
|
|
716
897
|
return;
|
|
717
898
|
finishLinkSelection(false);
|
|
899
|
+
beginWindowSwitch(paneId, target.expectedSemanticWindowId, target);
|
|
718
900
|
let resolve;
|
|
719
901
|
const settled = new Promise((done) => {
|
|
720
902
|
resolve = done;
|
|
@@ -724,6 +906,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
724
906
|
paneId,
|
|
725
907
|
layout: snapshot,
|
|
726
908
|
received: false,
|
|
909
|
+
diagnostic: pendingWindowSwitch,
|
|
727
910
|
resolve,
|
|
728
911
|
timer: setTimeout(() => {
|
|
729
912
|
if (linkSelection === pending)
|
|
@@ -751,6 +934,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
751
934
|
}
|
|
752
935
|
const wrapper = (await expected.client.dispatch({
|
|
753
936
|
kind: "semantic-intent",
|
|
937
|
+
...(pending.diagnostic ? { operationId: pending.diagnostic.traceId } : {}),
|
|
754
938
|
intent: {
|
|
755
939
|
verb: "workspace.window.link.select",
|
|
756
940
|
workspaceName: expected.workspaceName,
|
|
@@ -763,6 +947,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
763
947
|
if (wrapper.kind !== "semantic-intent" ||
|
|
764
948
|
!result.success ||
|
|
765
949
|
result.data.operationId !== wrapper.operationId ||
|
|
950
|
+
(pending.diagnostic && wrapper.operationId !== pending.diagnostic.traceId) ||
|
|
766
951
|
result.data.daemonInstanceId !== expected.daemonGeneration ||
|
|
767
952
|
result.data.workspaceName !== expected.workspaceName ||
|
|
768
953
|
result.data.target.linkId !== target.linkId ||
|
|
@@ -772,6 +957,18 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
772
957
|
finishLinkSelection(false);
|
|
773
958
|
return;
|
|
774
959
|
}
|
|
960
|
+
if (pending.diagnostic) {
|
|
961
|
+
const receiptAtMicros = diagnosticNowMicros();
|
|
962
|
+
if (pending.diagnostic.timing)
|
|
963
|
+
pending.diagnostic.timing.receiptAtMicros = receiptAtMicros;
|
|
964
|
+
diagnose("window-switch-receipt", {
|
|
965
|
+
...pending.diagnostic,
|
|
966
|
+
operationId: wrapper.operationId,
|
|
967
|
+
selected: true,
|
|
968
|
+
applied: true,
|
|
969
|
+
...(options.detailedWindowSwitchTiming ? { phaseAtMicros: receiptAtMicros } : {}),
|
|
970
|
+
});
|
|
971
|
+
}
|
|
775
972
|
pending.received = true;
|
|
776
973
|
reconcileLinkSelection(pending.layout);
|
|
777
974
|
await settled;
|
|
@@ -1022,54 +1219,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
1022
1219
|
const target = next?.semanticWindowId ?? next?.windowName;
|
|
1023
1220
|
if (!pane || !target)
|
|
1024
1221
|
return;
|
|
1025
|
-
|
|
1026
|
-
try {
|
|
1027
|
-
const active = options.generation();
|
|
1028
|
-
const identity = active?.adapter?.paneCanonicalIdentity(pane);
|
|
1029
|
-
const clientGeneration = active?.client?.getSnapshot().generation;
|
|
1030
|
-
const startedAtMicros = diagnosticNowMicros();
|
|
1031
|
-
if (active?.status === "live" &&
|
|
1032
|
-
active.daemonGeneration &&
|
|
1033
|
-
identity &&
|
|
1034
|
-
Number.isSafeInteger(clientGeneration) &&
|
|
1035
|
-
startedAtMicros !== null) {
|
|
1036
|
-
pendingWindowSwitch = {
|
|
1037
|
-
traceId: createTraceId(),
|
|
1038
|
-
target,
|
|
1039
|
-
paneId: pane,
|
|
1040
|
-
startedAtMicros,
|
|
1041
|
-
daemonGeneration: active.daemonGeneration,
|
|
1042
|
-
clientGeneration: clientGeneration,
|
|
1043
|
-
rendererEpoch: active.rendererEpoch,
|
|
1044
|
-
sourceEpoch: identity.sourceEpoch,
|
|
1045
|
-
generation: identity.generation,
|
|
1046
|
-
incarnation: identity.incarnation,
|
|
1047
|
-
revision: identity.revision,
|
|
1048
|
-
stateHash: identity.stateHash,
|
|
1049
|
-
cols: identity.cols,
|
|
1050
|
-
rows: identity.rows,
|
|
1051
|
-
layoutPublished: false,
|
|
1052
|
-
selectionApplied: false,
|
|
1053
|
-
presentationPublished: false,
|
|
1054
|
-
followUpRequested: false,
|
|
1055
|
-
...(options.detailedWindowSwitchTiming
|
|
1056
|
-
? {
|
|
1057
|
-
timing: {
|
|
1058
|
-
receiptAtMicros: null,
|
|
1059
|
-
layoutAtMicros: null,
|
|
1060
|
-
presentationAtMicros: null,
|
|
1061
|
-
},
|
|
1062
|
-
}
|
|
1063
|
-
: {}),
|
|
1064
|
-
};
|
|
1065
|
-
armDiagnosticWindowFrame({ kind: "window-switch", ...pendingWindowSwitch });
|
|
1066
|
-
diagnose("window-switch-start", { ...pendingWindowSwitch });
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
catch {
|
|
1070
|
-
pendingWindowSwitch = null;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1222
|
+
beginWindowSwitch(pane, target);
|
|
1073
1223
|
paneInput.selectPane(pane, { presentOptimistically: false });
|
|
1074
1224
|
},
|
|
1075
1225
|
async zoomPane(targetPane) {
|
|
@@ -1265,6 +1415,7 @@ export function createApplicationTerminalInteractionController(options) {
|
|
|
1265
1415
|
? {}
|
|
1266
1416
|
: { phaseAtMicros: pending.timing.presentationAtMicros }),
|
|
1267
1417
|
});
|
|
1418
|
+
maybeRequestWindowSwitchFrame();
|
|
1268
1419
|
},
|
|
1269
1420
|
observeDiagnosticWindowFrame() {
|
|
1270
1421
|
if (!options.diagnosticsEnabled)
|
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
export class BoundedPerformanceRecordWriter {
|
|
3
3
|
#sink;
|
|
4
4
|
#criticalLimit;
|
|
5
|
-
#pendingCritical = new
|
|
5
|
+
#pendingCritical = new Set();
|
|
6
|
+
#pending = [];
|
|
7
|
+
#pendingByteLimit;
|
|
8
|
+
#pendingBytes = 0;
|
|
9
|
+
#flushPromise = null;
|
|
10
|
+
#resolveFlush = null;
|
|
6
11
|
#saturated = false;
|
|
7
12
|
#failed = false;
|
|
8
13
|
#droppedRecords = 0;
|
|
9
|
-
constructor(sink, criticalLimit = 16) {
|
|
14
|
+
constructor(sink, criticalLimit = 16, pendingByteLimit = 64 * 1024) {
|
|
10
15
|
this.#sink = sink;
|
|
11
16
|
this.#criticalLimit = criticalLimit;
|
|
17
|
+
this.#pendingByteLimit = pendingByteLimit;
|
|
12
18
|
}
|
|
13
19
|
write(record) {
|
|
14
20
|
if (this.#failed)
|
|
15
21
|
return false;
|
|
16
22
|
if (this.#saturated) {
|
|
17
|
-
this.#
|
|
18
|
-
return false;
|
|
23
|
+
return this.#enqueue(record);
|
|
19
24
|
}
|
|
20
25
|
return this.#writeAccepted(record);
|
|
21
26
|
}
|
|
@@ -30,22 +35,57 @@ export class BoundedPerformanceRecordWriter {
|
|
|
30
35
|
this.#droppedRecords += 1;
|
|
31
36
|
return false;
|
|
32
37
|
}
|
|
33
|
-
this.#
|
|
34
|
-
return true;
|
|
38
|
+
return this.#enqueue(record, key);
|
|
35
39
|
}
|
|
36
40
|
drain() {
|
|
37
41
|
if (this.#failed)
|
|
38
42
|
return;
|
|
39
43
|
this.#saturated = false;
|
|
40
|
-
|
|
41
|
-
this.#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
while (this.#pending.length > 0 && !this.#saturated && !this.#failed) {
|
|
45
|
+
const next = this.#pending.shift();
|
|
46
|
+
this.#pendingBytes -= next.bytes;
|
|
47
|
+
if (next.key !== undefined)
|
|
48
|
+
this.#pendingCritical.delete(next.key);
|
|
49
|
+
this.#writeAccepted(next.record);
|
|
44
50
|
}
|
|
51
|
+
if (this.#pending.length === 0)
|
|
52
|
+
this.#finishFlush();
|
|
53
|
+
}
|
|
54
|
+
/** Wait until retained records reach the stream; its owner then flushes/end()s it. */
|
|
55
|
+
flush() {
|
|
56
|
+
if (this.#failed || this.#pending.length === 0)
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
this.#flushPromise ??= new Promise((resolve) => {
|
|
59
|
+
this.#resolveFlush = resolve;
|
|
60
|
+
});
|
|
61
|
+
return this.#flushPromise;
|
|
45
62
|
}
|
|
46
63
|
fail() {
|
|
47
64
|
this.#failed = true;
|
|
48
65
|
this.#pendingCritical.clear();
|
|
66
|
+
this.#pending.length = 0;
|
|
67
|
+
this.#pendingBytes = 0;
|
|
68
|
+
this.#finishFlush();
|
|
69
|
+
}
|
|
70
|
+
#enqueue(record, key) {
|
|
71
|
+
const bytes = Buffer.byteLength(record);
|
|
72
|
+
// Ordinary bursts get one extra stream-sized buffer. Critical records have
|
|
73
|
+
// a separate byte reserve as well as their existing key-count limit.
|
|
74
|
+
const limit = this.#pendingByteLimit + (key === undefined ? 0 : 64 * 1024);
|
|
75
|
+
if (this.#pending.length >= 1024 || this.#pendingBytes + bytes > limit) {
|
|
76
|
+
this.#droppedRecords += 1;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (key !== undefined)
|
|
80
|
+
this.#pendingCritical.add(key);
|
|
81
|
+
this.#pending.push({ record, bytes, ...(key === undefined ? {} : { key }) });
|
|
82
|
+
this.#pendingBytes += bytes;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
#finishFlush() {
|
|
86
|
+
this.#resolveFlush?.();
|
|
87
|
+
this.#resolveFlush = null;
|
|
88
|
+
this.#flushPromise = null;
|
|
49
89
|
}
|
|
50
90
|
diagnostics() {
|
|
51
91
|
return Object.freeze({
|
|
@@ -271,12 +271,13 @@ export class TerminalFastLaneRendererAdapter {
|
|
|
271
271
|
]);
|
|
272
272
|
if (seen.has(key))
|
|
273
273
|
continue;
|
|
274
|
+
// Bound retained de-duplication history, not the renderer's lifetime.
|
|
275
|
+
// An evicted identity may be reported again only after another real paint.
|
|
276
|
+
// Pending identities still have their separate per-frame admission limit.
|
|
274
277
|
if (seen.size >= 256)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
identities.push(identity);
|
|
279
|
-
}
|
|
278
|
+
seen.delete(seen.values().next().value);
|
|
279
|
+
seen.add(key);
|
|
280
|
+
identities.push(identity);
|
|
280
281
|
}
|
|
281
282
|
const dropped = this.#droppedCanonicalHostFrames;
|
|
282
283
|
this.#droppedCanonicalHostFrames = 0;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createTerminalFastLane, createWorkspaceClientTerminalSource, } from "@tmux-ide/daemon-client/terminal-fast-lane";
|
|
2
2
|
import { currentTuiPerformanceEventSink } from "../performance-events.js";
|
|
3
|
+
// Limit outstanding low-water sampling chains, not completed observations over
|
|
4
|
+
// the lifetime of a renderer. Each chain owns at most one pending timeout.
|
|
3
5
|
const TERMINAL_RESOURCE_SAMPLE_LIMIT = 512;
|
|
4
6
|
// The ProductRig idle proof reads after 10.1s. Emit just before that boundary
|
|
5
7
|
// so the operation-scoped sample cannot race the proof read.
|
|
@@ -227,7 +229,7 @@ function createOpenTuiTerminalResourceSampler(lane, sink) {
|
|
|
227
229
|
heartbeatTimer = setTimeout(heartbeat, TERMINAL_RESOURCE_HEARTBEAT_MS);
|
|
228
230
|
heartbeatTimer.unref?.();
|
|
229
231
|
const emit = (operation, identity, expectedAt, sampled) => {
|
|
230
|
-
if (disposed || resourceEpochIdentity === null
|
|
232
|
+
if (disposed || resourceEpochIdentity === null)
|
|
231
233
|
return;
|
|
232
234
|
ordinal += 1;
|
|
233
235
|
const now = readDiagnosticNow();
|
|
@@ -373,8 +375,12 @@ function createOpenTuiTerminalResourceSampler(lane, sink) {
|
|
|
373
375
|
};
|
|
374
376
|
return {
|
|
375
377
|
afterFence(identity) {
|
|
376
|
-
if (disposed
|
|
378
|
+
if (disposed)
|
|
379
|
+
return;
|
|
380
|
+
if (pending.size >= TERMINAL_RESOURCE_SAMPLE_LIMIT) {
|
|
381
|
+
recordSamplingFailure();
|
|
377
382
|
return;
|
|
383
|
+
}
|
|
378
384
|
latestIdentity = identity;
|
|
379
385
|
let now;
|
|
380
386
|
if (resourceEpochIdentity === null) {
|
|
@@ -50,3 +50,38 @@ function graphemes(text) {
|
|
|
50
50
|
return [...new Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map((entry) => entry.segment);
|
|
51
51
|
return [...text];
|
|
52
52
|
}
|
|
53
|
+
/** PURE — greedy word-wrap for terminal prose (never returns empty for
|
|
54
|
+
* non-empty text; words longer than the width hard-break). */
|
|
55
|
+
export function wrapText(text, width) {
|
|
56
|
+
if (width <= 0)
|
|
57
|
+
return [text];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const para of text.split("\n")) {
|
|
60
|
+
let line = "";
|
|
61
|
+
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
62
|
+
if (line.length === 0) {
|
|
63
|
+
let w = word;
|
|
64
|
+
while (w.length > width) {
|
|
65
|
+
out.push(w.slice(0, width));
|
|
66
|
+
w = w.slice(width);
|
|
67
|
+
}
|
|
68
|
+
line = w;
|
|
69
|
+
}
|
|
70
|
+
else if (line.length + 1 + word.length <= width) {
|
|
71
|
+
line += ` ${word}`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
out.push(line);
|
|
75
|
+
let w = word;
|
|
76
|
+
while (w.length > width) {
|
|
77
|
+
out.push(w.slice(0, width));
|
|
78
|
+
w = w.slice(width);
|
|
79
|
+
}
|
|
80
|
+
line = w;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (line.length > 0 || para.length === 0)
|
|
84
|
+
out.push(line);
|
|
85
|
+
}
|
|
86
|
+
return out.length > 0 ? out : [""];
|
|
87
|
+
}
|
|
@@ -156,9 +156,11 @@ export function createApplicationConnectionFeedback(onPublish = () => undefined)
|
|
|
156
156
|
failure,
|
|
157
157
|
...(inventory
|
|
158
158
|
? {
|
|
159
|
-
recovery:
|
|
160
|
-
? "A
|
|
161
|
-
:
|
|
159
|
+
recovery: failure.reason === "duplicate-runtime-pane-binding"
|
|
160
|
+
? "A terminal is shared by multiple workspace entries. Unlink shared tmux windows so each belongs to one session, then select Retry."
|
|
161
|
+
: missing
|
|
162
|
+
? "A registered session may have been recreated. Choose that session to restore its identity, then retry here. It may be a different session."
|
|
163
|
+
: "Terminal identities conflict or cannot be verified. Copy connection details for diagnosis; Retry does not repair identity conflicts.",
|
|
162
164
|
}
|
|
163
165
|
: {}),
|
|
164
166
|
};
|
|
@@ -546,16 +546,7 @@ export function createDefaultFleetFactsReaders(): Pick<
|
|
|
546
546
|
return null;
|
|
547
547
|
}
|
|
548
548
|
};
|
|
549
|
-
return
|
|
550
|
-
readSessions: async (signal) => {
|
|
551
|
-
const raw = await execute(SESSION_COMPOSITION_TMUX_ARGS, signal);
|
|
552
|
-
return raw === null ? null : parseSessionCompositionFacts(raw);
|
|
553
|
-
},
|
|
554
|
-
readAgents: async (signal) => {
|
|
555
|
-
const raw = await execute(AGENT_STATE_TMUX_ARGS, signal);
|
|
556
|
-
return raw === null ? null : parseAgentStateFacts(raw);
|
|
557
|
-
},
|
|
558
|
-
};
|
|
549
|
+
return createSharedFleetFactsReaders(execute);
|
|
559
550
|
}
|
|
560
551
|
|
|
561
552
|
export const SESSION_COMPOSITION_TMUX_ARGS = [
|
|
@@ -593,3 +584,102 @@ export const AGENT_STATE_TMUX_ARGS = [
|
|
|
593
584
|
export async function readAgentStateFacts(signal?: AbortSignal): Promise<AgentStateReading | null> {
|
|
594
585
|
return createDefaultFleetFactsReaders().readAgents(signal);
|
|
595
586
|
}
|
|
587
|
+
|
|
588
|
+
/** Agent columns are excluded from the session topology projection. */
|
|
589
|
+
export const FLEET_FACTS_TMUX_ARGS = [
|
|
590
|
+
"list-panes",
|
|
591
|
+
"-a",
|
|
592
|
+
"-F",
|
|
593
|
+
`${SESSION_COMPOSITION_TMUX_ARGS[3]}\t#{@agent_state}\t#{pane_current_command}`,
|
|
594
|
+
] as const;
|
|
595
|
+
|
|
596
|
+
/** Share only an outstanding read; completed snapshots are never cached. */
|
|
597
|
+
export function createSharedFleetFactsReaders(
|
|
598
|
+
run: (args: readonly string[], signal?: AbortSignal) => string | null | Promise<string | null>,
|
|
599
|
+
isAbsentServer: (error: unknown) => boolean = () => false,
|
|
600
|
+
): Pick<DaemonFleetFactsObserverOptions, "readSessions" | "readAgents"> {
|
|
601
|
+
interface Pending {
|
|
602
|
+
controller: AbortController;
|
|
603
|
+
result: Promise<string | null>;
|
|
604
|
+
readers: number;
|
|
605
|
+
}
|
|
606
|
+
let pending: Pending | null = null;
|
|
607
|
+
const read = async (signal?: AbortSignal): Promise<string | null> => {
|
|
608
|
+
if (signal?.aborted) return null;
|
|
609
|
+
if (!pending) {
|
|
610
|
+
const controller = new AbortController();
|
|
611
|
+
const entry: Pending = {
|
|
612
|
+
controller,
|
|
613
|
+
readers: 0,
|
|
614
|
+
result: Promise.resolve().then(() => run(FLEET_FACTS_TMUX_ARGS, controller.signal)),
|
|
615
|
+
};
|
|
616
|
+
entry.result = entry.result.finally(() => {
|
|
617
|
+
if (pending === entry) pending = null;
|
|
618
|
+
});
|
|
619
|
+
pending = entry;
|
|
620
|
+
}
|
|
621
|
+
const entry = pending;
|
|
622
|
+
entry.readers += 1;
|
|
623
|
+
let released = false;
|
|
624
|
+
const release = () => {
|
|
625
|
+
if (released) return;
|
|
626
|
+
released = true;
|
|
627
|
+
entry.readers -= 1;
|
|
628
|
+
if (entry.readers === 0) {
|
|
629
|
+
if (pending === entry) pending = null;
|
|
630
|
+
entry.controller.abort();
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
signal?.addEventListener("abort", release, { once: true });
|
|
634
|
+
try {
|
|
635
|
+
// Keep the promise outstanding for a noncooperative runner so the
|
|
636
|
+
// observer's per-kind quarantine remains effective after cancellation.
|
|
637
|
+
const raw = await entry.result;
|
|
638
|
+
return signal?.aborted ? null : raw;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (signal?.aborted) return null;
|
|
641
|
+
throw error;
|
|
642
|
+
} finally {
|
|
643
|
+
signal?.removeEventListener("abort", release);
|
|
644
|
+
release();
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
return {
|
|
648
|
+
readSessions: async (signal) => {
|
|
649
|
+
try {
|
|
650
|
+
const raw = await read(signal);
|
|
651
|
+
return raw === null
|
|
652
|
+
? null
|
|
653
|
+
: parseSessionCompositionFacts(
|
|
654
|
+
raw
|
|
655
|
+
.split("\n")
|
|
656
|
+
.map((line) => line.split("\t").slice(0, 11).join("\t"))
|
|
657
|
+
.join("\n"),
|
|
658
|
+
);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
// Socket absence is an authoritative empty catalog, but must not
|
|
661
|
+
// manufacture agent turn completions from a failed agent observation.
|
|
662
|
+
return isAbsentServer(error) ? parseSessionCompositionFacts("") : null;
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
readAgents: async (signal) => {
|
|
666
|
+
try {
|
|
667
|
+
const raw = await read(signal);
|
|
668
|
+
return raw === null
|
|
669
|
+
? null
|
|
670
|
+
: parseAgentStateFacts(
|
|
671
|
+
raw
|
|
672
|
+
.split("\n")
|
|
673
|
+
.map((line) => {
|
|
674
|
+
const fields = line.split("\t");
|
|
675
|
+
if (fields.length !== 13) return "";
|
|
676
|
+
return [fields[0], fields[6], fields[9], fields[11], fields[12]].join("\t");
|
|
677
|
+
})
|
|
678
|
+
.join("\n"),
|
|
679
|
+
);
|
|
680
|
+
} catch {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|