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
|
@@ -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
|
);
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code agent teams — opt-out enablement in the user's Claude settings.
|
|
3
|
+
*
|
|
4
|
+
* Agent teams let one Claude Code session spawn teammates; with
|
|
5
|
+
* `teammateMode: "auto"` Claude puts them in split panes when it runs inside
|
|
6
|
+
* tmux (in-process elsewhere), which is exactly what tmux-ide shows and
|
|
7
|
+
* routes. The installer enables this by default and `tmux-ide integration
|
|
8
|
+
* agent-teams enable|disable|status` manages it explicitly.
|
|
9
|
+
*
|
|
10
|
+
* Rules (pure, in {@link mergeAgentTeams}):
|
|
11
|
+
* - `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set to "1" only when absent.
|
|
12
|
+
* An explicit off value ("0", "false", false, 0…) is the user's opt-out: the
|
|
13
|
+
* installer never overrides it and touches nothing else. Only an explicit
|
|
14
|
+
* `tmux-ide integration agent-teams enable` (overrideOptOut) turns it back on.
|
|
15
|
+
* - `teammateMode: "auto"` is set only when `teammateMode` is absent.
|
|
16
|
+
* - Every other key is preserved. Nothing changed → nothing written.
|
|
17
|
+
*
|
|
18
|
+
* Writes (io) reuse the hooks integration's settings path resolution, including
|
|
19
|
+
* the `TMUX_IDE_CLAUDE_SETTINGS` override: one-time timestamped backup before
|
|
20
|
+
* the first modification, then an atomic temp-file + rename. Invalid JSON is
|
|
21
|
+
* reported and never rewritten.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
chmodSync,
|
|
25
|
+
existsSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
renameSync,
|
|
30
|
+
statSync,
|
|
31
|
+
copyFileSync,
|
|
32
|
+
unlinkSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
accessSync,
|
|
35
|
+
constants,
|
|
36
|
+
} from "node:fs";
|
|
37
|
+
import { basename, delimiter, dirname, join } from "node:path";
|
|
38
|
+
import { resolveRuntimeNamespace, runtimeOwnedPath } from "../../lib/runtime-namespace.ts";
|
|
39
|
+
|
|
40
|
+
export const AGENT_TEAMS_ENV = "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS";
|
|
41
|
+
export const DEFAULT_TEAMMATE_MODE = "auto";
|
|
42
|
+
const BACKUP_INFIX = ".tmux-ide-backup-";
|
|
43
|
+
|
|
44
|
+
type Settings = Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
export type AgentTeamsSetting = "enabled" | "disabled-by-user" | "not-configured";
|
|
47
|
+
|
|
48
|
+
/** PURE — an explicit off value is the user's opt-out. */
|
|
49
|
+
export function isOptOut(value: unknown): boolean {
|
|
50
|
+
if (value === false || value === 0) return true;
|
|
51
|
+
return typeof value === "string" && /^\s*(0|false|off|no)\s*$/iu.test(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function envOf(settings: Settings): Record<string, unknown> | undefined {
|
|
55
|
+
const env = settings.env;
|
|
56
|
+
if (env === undefined) return undefined;
|
|
57
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
58
|
+
throw new Error("settings env must be an object");
|
|
59
|
+
return env as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** PURE — how the settings currently stand for agent teams. */
|
|
63
|
+
export function agentTeamsSetting(settings: Settings): AgentTeamsSetting {
|
|
64
|
+
const value = envOf(settings)?.[AGENT_TEAMS_ENV];
|
|
65
|
+
if (value === undefined) return "not-configured";
|
|
66
|
+
return isOptOut(value) ? "disabled-by-user" : "enabled";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AgentTeamsMerge {
|
|
70
|
+
readonly settings: Settings;
|
|
71
|
+
/** Human-readable `key=value` list of what was set; empty when unchanged. */
|
|
72
|
+
readonly changes: readonly string[];
|
|
73
|
+
readonly setting: AgentTeamsSetting;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* PURE — enable agent teams without overriding anything the user chose, unless
|
|
78
|
+
* `overrideOptOut` (the user explicitly asked to enable) replaces their "0".
|
|
79
|
+
*/
|
|
80
|
+
export function mergeAgentTeams(
|
|
81
|
+
settings: Settings,
|
|
82
|
+
{ overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
|
|
83
|
+
): AgentTeamsMerge {
|
|
84
|
+
const setting = agentTeamsSetting(settings);
|
|
85
|
+
if (setting === "disabled-by-user" && !overrideOptOut) return { settings, changes: [], setting };
|
|
86
|
+
const changes: string[] = [];
|
|
87
|
+
let next: Settings = settings;
|
|
88
|
+
if (setting !== "enabled") {
|
|
89
|
+
next = { ...next, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "1" } };
|
|
90
|
+
changes.push(`env.${AGENT_TEAMS_ENV}=1`);
|
|
91
|
+
}
|
|
92
|
+
if (!("teammateMode" in settings)) {
|
|
93
|
+
next = { ...next, teammateMode: DEFAULT_TEAMMATE_MODE };
|
|
94
|
+
changes.push(`teammateMode=${DEFAULT_TEAMMATE_MODE}`);
|
|
95
|
+
}
|
|
96
|
+
return { settings: next, changes, setting: "enabled" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** PURE — record an explicit opt-out ("0"), which later enables respect. */
|
|
100
|
+
export function disableAgentTeams(settings: Settings): AgentTeamsMerge {
|
|
101
|
+
if (agentTeamsSetting(settings) === "disabled-by-user")
|
|
102
|
+
return { settings, changes: [], setting: "disabled-by-user" };
|
|
103
|
+
return {
|
|
104
|
+
settings: { ...settings, env: { ...(envOf(settings) ?? {}), [AGENT_TEAMS_ENV]: "0" } },
|
|
105
|
+
changes: [`env.${AGENT_TEAMS_ENV}=0`],
|
|
106
|
+
setting: "disabled-by-user",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface AgentTeamsPaths {
|
|
111
|
+
/** Claude Code's user settings file. */
|
|
112
|
+
readonly settingsPath: string;
|
|
113
|
+
/** Claude Code's home (~/.claude); its presence means Claude is installed. */
|
|
114
|
+
readonly claudeDir: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function agentTeamsPaths(): AgentTeamsPaths {
|
|
118
|
+
const namespace = resolveRuntimeNamespace();
|
|
119
|
+
return { settingsPath: namespace.claudeSettingsPath, claudeDir: namespace.claudeDir };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `claude` on PATH, without spawning anything. */
|
|
123
|
+
export function claudeOnPath(path = process.env.PATH ?? ""): boolean {
|
|
124
|
+
return path
|
|
125
|
+
.split(delimiter)
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.some((dir) => {
|
|
128
|
+
try {
|
|
129
|
+
accessSync(join(dir, "claude"), constants.X_OK);
|
|
130
|
+
return statSync(join(dir, "claude")).isFile();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export class InvalidClaudeSettingsError extends Error {}
|
|
138
|
+
|
|
139
|
+
function readSettings(path: string): { settings: Settings; exists: boolean } {
|
|
140
|
+
if (!existsSync(path)) return { settings: {}, exists: false };
|
|
141
|
+
try {
|
|
142
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
143
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
144
|
+
throw new Error("not an object");
|
|
145
|
+
envOf(parsed as Settings);
|
|
146
|
+
return { settings: parsed as Settings, exists: true };
|
|
147
|
+
} catch {
|
|
148
|
+
throw new InvalidClaudeSettingsError(
|
|
149
|
+
`${path} is not valid settings JSON — fix or move it, then retry`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Existing one-time backup, if tmux-ide already made one for this file. */
|
|
155
|
+
function existingBackup(settingsPath: string): string | null {
|
|
156
|
+
const dir = dirname(settingsPath);
|
|
157
|
+
const prefix = `${basename(settingsPath)}${BACKUP_INFIX}`;
|
|
158
|
+
try {
|
|
159
|
+
const found = readdirSync(dir).find((name) => name.startsWith(prefix));
|
|
160
|
+
return found ? join(dir, found) : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeSettings(path: string, settings: Settings, exists: boolean): string | null {
|
|
167
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
168
|
+
let backup: string | null = null;
|
|
169
|
+
if (exists) {
|
|
170
|
+
backup = existingBackup(path);
|
|
171
|
+
if (!backup) {
|
|
172
|
+
const stamp = new Date()
|
|
173
|
+
.toISOString()
|
|
174
|
+
.replace(/[-:]/gu, "")
|
|
175
|
+
.replace(/\.\d+Z$/u, "Z");
|
|
176
|
+
backup = `${path}${BACKUP_INFIX}${stamp}`;
|
|
177
|
+
copyFileSync(path, backup);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const temporary = join(dirname(path), `.${basename(path)}.tmux-ide-${process.pid}.tmp`);
|
|
181
|
+
try {
|
|
182
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600 });
|
|
183
|
+
if (exists) chmodSync(temporary, statSync(path).mode & 0o777);
|
|
184
|
+
renameSync(temporary, path);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
try {
|
|
187
|
+
unlinkSync(temporary);
|
|
188
|
+
} catch {
|
|
189
|
+
/* already renamed or never written */
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
return backup;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface AgentTeamsResult {
|
|
197
|
+
readonly action:
|
|
198
|
+
| "enabled"
|
|
199
|
+
| "disabled"
|
|
200
|
+
| "unchanged"
|
|
201
|
+
| "disabled-by-user"
|
|
202
|
+
| "skipped"
|
|
203
|
+
| "invalid";
|
|
204
|
+
readonly settingsPath: string;
|
|
205
|
+
readonly changes: readonly string[];
|
|
206
|
+
readonly backup: string | null;
|
|
207
|
+
readonly claudeDetected: boolean;
|
|
208
|
+
readonly message: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function detected(paths: AgentTeamsPaths, onPath: () => boolean): boolean {
|
|
212
|
+
return existsSync(paths.claudeDir) || onPath();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function guarded(paths: AgentTeamsPaths | undefined): AgentTeamsPaths {
|
|
216
|
+
if (!paths && resolveRuntimeNamespace().development)
|
|
217
|
+
throw new Error("Development agent-teams changes require explicit fixture paths");
|
|
218
|
+
const resolved = paths ?? agentTeamsPaths();
|
|
219
|
+
runtimeOwnedPath(resolved.settingsPath);
|
|
220
|
+
return resolved;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Enable agent teams when Claude Code is installed and the user hasn't opted out. */
|
|
224
|
+
export function enableClaudeAgentTeams(
|
|
225
|
+
paths?: AgentTeamsPaths,
|
|
226
|
+
onPath: () => boolean = () => claudeOnPath(),
|
|
227
|
+
{ overrideOptOut = false }: { readonly overrideOptOut?: boolean } = {},
|
|
228
|
+
): AgentTeamsResult {
|
|
229
|
+
const resolved = guarded(paths);
|
|
230
|
+
const { settingsPath } = resolved;
|
|
231
|
+
const base = { settingsPath, changes: [], backup: null } as const;
|
|
232
|
+
if (!detected(resolved, onPath))
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
action: "skipped",
|
|
236
|
+
claudeDetected: false,
|
|
237
|
+
message: `Claude Code not found (no ${resolved.claudeDir}, no claude on PATH); agent teams not configured.`,
|
|
238
|
+
};
|
|
239
|
+
let read: ReturnType<typeof readSettings>;
|
|
240
|
+
try {
|
|
241
|
+
read = readSettings(settingsPath);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return { ...base, action: "invalid", claudeDetected: true, message: (error as Error).message };
|
|
244
|
+
}
|
|
245
|
+
const merged = mergeAgentTeams(read.settings, { overrideOptOut });
|
|
246
|
+
if (merged.setting === "disabled-by-user")
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
action: "disabled-by-user",
|
|
250
|
+
claudeDetected: true,
|
|
251
|
+
message:
|
|
252
|
+
`Claude Code agent teams are disabled by user in ${settingsPath} (${AGENT_TEAMS_ENV}); ` +
|
|
253
|
+
"left unchanged. Enable explicitly: tmux-ide integration agent-teams enable",
|
|
254
|
+
};
|
|
255
|
+
if (merged.changes.length === 0)
|
|
256
|
+
return {
|
|
257
|
+
...base,
|
|
258
|
+
action: "unchanged",
|
|
259
|
+
claudeDetected: true,
|
|
260
|
+
message: `Claude Code agent teams already enabled in ${settingsPath}.`,
|
|
261
|
+
};
|
|
262
|
+
const backup = writeSettings(settingsPath, merged.settings, read.exists);
|
|
263
|
+
return {
|
|
264
|
+
settingsPath,
|
|
265
|
+
changes: merged.changes,
|
|
266
|
+
backup,
|
|
267
|
+
action: "enabled",
|
|
268
|
+
claudeDetected: true,
|
|
269
|
+
message:
|
|
270
|
+
`Enabled Claude Code agent teams in ${settingsPath} (${merged.changes.join(", ")}` +
|
|
271
|
+
`${backup ? `; backup: ${backup}` : ""}). Disable: tmux-ide integration agent-teams disable`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Record the user's opt-out so neither the installer nor `enable` turns it back on silently. */
|
|
276
|
+
export function disableClaudeAgentTeams(paths?: AgentTeamsPaths): AgentTeamsResult {
|
|
277
|
+
const resolved = guarded(paths);
|
|
278
|
+
const { settingsPath } = resolved;
|
|
279
|
+
const base = { settingsPath, changes: [], backup: null, claudeDetected: true } as const;
|
|
280
|
+
let read: ReturnType<typeof readSettings>;
|
|
281
|
+
try {
|
|
282
|
+
read = readSettings(settingsPath);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return { ...base, action: "invalid", message: (error as Error).message };
|
|
285
|
+
}
|
|
286
|
+
const next = disableAgentTeams(read.settings);
|
|
287
|
+
if (next.changes.length === 0)
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
action: "unchanged",
|
|
291
|
+
message: `Claude Code agent teams already disabled in ${settingsPath}.`,
|
|
292
|
+
};
|
|
293
|
+
const backup = writeSettings(settingsPath, next.settings, read.exists);
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
changes: next.changes,
|
|
297
|
+
backup,
|
|
298
|
+
action: "disabled",
|
|
299
|
+
message:
|
|
300
|
+
`Disabled Claude Code agent teams in ${settingsPath} (${next.changes.join(", ")}` +
|
|
301
|
+
`${backup ? `; backup: ${backup}` : ""}). Re-enable: tmux-ide integration agent-teams enable`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface AgentTeamsStatus {
|
|
306
|
+
readonly settingsPath: string;
|
|
307
|
+
readonly claudeDetected: boolean;
|
|
308
|
+
readonly setting: AgentTeamsSetting | "invalid-settings";
|
|
309
|
+
readonly teammateMode: string | null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function claudeAgentTeamsStatus(
|
|
313
|
+
paths: AgentTeamsPaths = agentTeamsPaths(),
|
|
314
|
+
onPath: () => boolean = () => claudeOnPath(),
|
|
315
|
+
): AgentTeamsStatus {
|
|
316
|
+
const claudeDetected = detected(paths, onPath);
|
|
317
|
+
try {
|
|
318
|
+
const { settings } = readSettings(paths.settingsPath);
|
|
319
|
+
const mode = settings.teammateMode;
|
|
320
|
+
return {
|
|
321
|
+
settingsPath: paths.settingsPath,
|
|
322
|
+
claudeDetected,
|
|
323
|
+
setting: agentTeamsSetting(settings),
|
|
324
|
+
teammateMode: typeof mode === "string" ? mode : null,
|
|
325
|
+
};
|
|
326
|
+
} catch {
|
|
327
|
+
return {
|
|
328
|
+
settingsPath: paths.settingsPath,
|
|
329
|
+
claudeDetected,
|
|
330
|
+
setting: "invalid-settings",
|
|
331
|
+
teammateMode: null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -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);
|