tmux-ide 2.9.6 → 2.9.8

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.
Files changed (84) hide show
  1. package/bin/cli.js +2881 -1336
  2. package/package.json +2 -2
  3. package/packages/contracts/src/index.ts +5 -0
  4. package/packages/contracts/src/pane-stream.ts +54 -0
  5. package/packages/contracts/src/terminal-graphics.ts +118 -0
  6. package/packages/contracts/src/terminal-history-anchor.ts +61 -0
  7. package/packages/contracts/src/tmux-server-diagnostics.ts +16 -0
  8. package/packages/core/src/index.ts +1 -0
  9. package/packages/core/src/kitty-placeholders.ts +208 -0
  10. package/packages/daemon/dist/command-center/tmux-server-diagnostics.js +24 -0
  11. package/packages/daemon/dist/command-center/tmux-server-history-anchor.js +48 -0
  12. package/packages/daemon/dist/command-center/tmux-servers.js +11 -0
  13. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +23 -1
  14. package/packages/daemon/dist/lib/daemon-embed.js +21 -0
  15. package/packages/daemon/dist/lib/tmux-server-owner.js +10 -1
  16. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +2 -1
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  18. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  21. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libevent_core-2.1.7.dylib +0 -0
  22. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.3.0.dylib +0 -0
  23. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +12 -4
  24. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  25. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +9 -1
  26. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  27. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +9 -1
  28. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  29. package/packages/daemon/dist/terminal/attachments/native-runtime.js +25 -12
  30. package/packages/daemon/dist/terminal/graphics/kitty-graphics.js +699 -0
  31. package/packages/daemon/dist/terminal/mirror/control-channel.js +17 -1
  32. package/packages/daemon/dist/terminal/mirror/mirror-service.js +7 -0
  33. package/packages/daemon/dist/terminal/mirror/native-grid-capture.js +8 -0
  34. package/packages/daemon/dist/terminal/mirror/native-grid-reader.js +6 -1
  35. package/packages/daemon/dist/terminal/mirror/session-channel.js +56 -4
  36. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +180 -1
  37. package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +3 -2
  38. package/packages/daemon/dist/terminal/session-runtime/history-anchor-owner.js +171 -0
  39. package/packages/daemon/dist/terminal/session-runtime/registry.js +29 -0
  40. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +20 -1
  41. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +85 -1
  42. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +30 -0
  43. package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +53 -21
  44. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +11 -0
  45. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +7 -1
  46. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +35 -5
  47. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-paste.js +2 -2
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +2 -2
  49. package/packages/daemon/src/command-center/tmux-server-diagnostics.ts +38 -0
  50. package/packages/daemon/src/command-center/tmux-server-history-anchor.ts +69 -0
  51. package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
  52. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +20 -2
  53. package/packages/daemon/src/lib/daemon-embed.ts +26 -0
  54. package/packages/daemon/src/lib/tmux-server-owner.ts +10 -0
  55. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +2 -1
  56. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  57. package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client-recorded.ctl +87 -0
  58. package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client.ctl +10 -0
  59. package/packages/daemon/src/terminal/graphics/kitty-graphics.ts +756 -0
  60. package/packages/daemon/src/terminal/mirror/control-channel.ts +18 -1
  61. package/packages/daemon/src/terminal/mirror/mirror-service.ts +14 -0
  62. package/packages/daemon/src/terminal/mirror/native-grid-capture.ts +11 -0
  63. package/packages/daemon/src/terminal/mirror/native-grid-reader.ts +10 -1
  64. package/packages/daemon/src/terminal/mirror/session-channel.ts +75 -3
  65. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +199 -0
  66. package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +3 -2
  67. package/packages/daemon/src/terminal/session-runtime/history-anchor-owner.ts +190 -0
  68. package/packages/daemon/src/terminal/session-runtime/registry.ts +38 -0
  69. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +38 -0
  70. package/packages/daemon/src/terminal/session-runtime/terminal-interpreter-backend.ts +4 -0
  71. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +99 -1
  72. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +39 -0
  73. package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +58 -24
  74. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +13 -0
  75. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +6 -1
  76. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +44 -9
  77. package/packages/daemon/src/tui/mirror/runtime/application-terminal-paste.ts +2 -1
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +5 -2
  79. package/packages/daemon-client/src/fleet-connection-status.ts +1 -1
  80. package/packages/daemon-client/src/pane-creation-refusal.ts +48 -0
  81. package/packages/daemon-client/src/pane-stream-client.ts +37 -0
  82. package/packages/daemon-client/src/tmux-server-client.ts +133 -6
  83. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.2.3.dylib +0 -0
  84. /package/packages/daemon/dist/native/tmux/darwin-arm64/licenses/{libutf8proc.3.2.3.dylib.txt → libutf8proc.3.3.0.dylib.txt} +0 -0
@@ -32,6 +32,13 @@ function spawnOwner(entryPath, cwd) {
32
32
  });
33
33
  });
34
34
  }
35
+ class ShutdownTransportError extends Error {
36
+ original;
37
+ constructor(original) {
38
+ super("Canonical daemon shutdown transport failed.", { cause: original });
39
+ this.original = original;
40
+ }
41
+ }
35
42
  async function shutdownOlderOwner(info) {
36
43
  const headers = { "content-type": "application/json" };
37
44
  if (info.authToken)
@@ -45,6 +52,8 @@ async function shutdownOlderOwner(info) {
45
52
  expectedInstanceId: info.instanceId,
46
53
  }),
47
54
  signal: AbortSignal.timeout(2_000),
55
+ }).catch((error) => {
56
+ throw new ShutdownTransportError(error);
48
57
  });
49
58
  const envelope = (await response.json().catch(() => null));
50
59
  // The authenticated handler verifies expectedInstanceId before reporting this
@@ -141,7 +150,18 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
141
150
  "tmux-ide --headless --supervised <id> before restarting it. " +
142
151
  "The existing daemon has been left running.", { reason: "canonical-record-invalid" });
143
152
  }
144
- await deps.shutdownOlderOwner(info);
153
+ let transportFailure;
154
+ try {
155
+ await deps.shutdownOlderOwner(info);
156
+ }
157
+ catch (error) {
158
+ if (!(error instanceof ShutdownTransportError))
159
+ throw error;
160
+ transportFailure = error;
161
+ }
162
+ // A competing upgrader may close the authenticated owner's listener before
163
+ // removing its record. Observe retirement, without repeating the mutation.
164
+ // A transport failure alone never authorizes starting a replacement.
145
165
  const deadline = deps.now() + timeoutMs;
146
166
  while (deps.now() < deadline) {
147
167
  const state = deps.inspect();
@@ -153,6 +173,8 @@ async function replaceOlderCanonicalDaemon(deps, info, timeoutMs, expectedProduc
153
173
  return;
154
174
  await deps.sleep(25);
155
175
  }
176
+ if (transportFailure)
177
+ throw transportFailure.original;
156
178
  throw new DaemonBootstrapError("control-timeout", "The older canonical daemon did not retire after accepting the version upgrade.", { reason: "protocol-mismatch" });
157
179
  }
158
180
  async function assertServerIntent(deps, info, identity, intent) {
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
9
10
  import { createNativeSplitCapabilityProbe, createNativeSplitReadiness, } from "./native-split-capability.js";
10
11
  import { executeCanonicalSplitMutation } from "./canonical-split-mutation.js";
11
12
  import { createGuardedNativeSplitResize } from "./guarded-native-split-resize.js";
@@ -956,6 +957,7 @@ async function startEmbeddedDaemonGeneration(opts) {
956
957
  let paneStreamRuntime = null;
957
958
  let serverOwners = null;
958
959
  let defaultEditorContextRetired = false;
960
+ const defaultDiagnosticsLifetime = new AbortController();
959
961
  const defaultSessionCreator = createNativeTmuxSessionCreator({
960
962
  generation: instanceId,
961
963
  registry: workspaceRegistry,
@@ -1287,6 +1289,24 @@ async function startEmbeddedDaemonGeneration(opts) {
1287
1289
  stateDirectory: resolveRuntimeNamespace().runtimeDir,
1288
1290
  webSocketBaseUrl: canonicalDaemonUrl("ws", bindHostname, port),
1289
1291
  defaultOwner: {
1292
+ readServerVersion: async (signal) => {
1293
+ const assertCurrent = () => {
1294
+ if (defaultEditorContextRetired ||
1295
+ serverOwners?.defaultRetired ||
1296
+ !initialNativeServerIdentity)
1297
+ throw new Error("Default tmux diagnostics owner is unavailable");
1298
+ };
1299
+ assertCurrent();
1300
+ const combined = AbortSignal.any([
1301
+ defaultDiagnosticsLifetime.signal,
1302
+ ...(signal ? [signal] : []),
1303
+ ]);
1304
+ combined.throwIfAborted();
1305
+ const output = await fleetFactsTmuxRunner(["display-message", "-p", "#{version}"], combined);
1306
+ combined.throwIfAborted();
1307
+ assertCurrent();
1308
+ return TmuxServerVersionSchemaZ.parse(output.trim());
1309
+ },
1290
1310
  get interactionObservation() {
1291
1311
  return interactionObservation;
1292
1312
  },
@@ -1375,6 +1395,7 @@ async function startEmbeddedDaemonGeneration(opts) {
1375
1395
  paneStreamRuntime,
1376
1396
  dispose: async () => {
1377
1397
  defaultEditorContextRetired = true;
1398
+ defaultDiagnosticsLifetime.abort();
1378
1399
  sessionMonitor?.stop();
1379
1400
  paneSourceCredentials.dispose();
1380
1401
  const transportResults = await Promise.allSettled([
@@ -17,7 +17,7 @@ import { createNativeTmuxSessionOpener } from "./tmux-server-session-open.js";
17
17
  import { observedAgentSchema } from "./tmux-server-agent-admission.js";
18
18
  import { createPaneEditorContextResolver } from "./tmux-pane-editor-context.js";
19
19
  import { createHash, randomUUID } from "node:crypto";
20
- import { WorkspaceIdSchemaZ, WindowLinkTargetSchemaZ, } from "@tmux-ide/contracts";
20
+ import { TmuxServerVersionSchemaZ, WorkspaceIdSchemaZ, WindowLinkTargetSchemaZ, } from "@tmux-ide/contracts";
21
21
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.js";
22
22
  import { mkdirSync } from "node:fs";
23
23
  import { z } from "zod";
@@ -483,6 +483,15 @@ export async function createNativeTmuxServerOwner(options) {
483
483
  throw error;
484
484
  }
485
485
  return {
486
+ readServerVersion: async (signal) => {
487
+ assertOpen();
488
+ const combined = AbortSignal.any([credentialLifetime.signal, ...(signal ? [signal] : [])]);
489
+ combined.throwIfAborted();
490
+ const output = await generationRunAsync(["display-message", "-p", "#{version}"], combined);
491
+ combined.throwIfAborted();
492
+ assertOpen();
493
+ return TmuxServerVersionSchemaZ.parse(output.trim());
494
+ },
486
495
  serverId: options.serverId,
487
496
  generation,
488
497
  resolvePaneEditorContext,
@@ -23,7 +23,8 @@ export function createTmuxSessionMutationFence() {
23
23
  const output = run([
24
24
  "if-shell",
25
25
  "-t",
26
- `=${selected.name}`,
26
+ // if-shell resolves a pane target: the colon selects this exact session.
27
+ `=${selected.name}:`,
27
28
  "-F",
28
29
  `#{&&:#{==:#{session_id},${selected.id}},#{==:#{session_created},${selected.created}}}`,
29
30
  args.map((arg) => (arg === ";" ? ";" : shellEscape(arg))).join(" "),
@@ -21,9 +21,9 @@
21
21
  <key>CFBundlePackageType</key>
22
22
  <string>APPL</string>
23
23
  <key>CFBundleShortVersionString</key>
24
- <string>2.9.6</string>
24
+ <string>2.9.8</string>
25
25
  <key>CFBundleVersion</key>
26
- <string>2.9.6</string>
26
+ <string>2.9.8</string>
27
27
  <key>LSMinimumSystemVersion</key>
28
28
  <string>11.0</string>
29
29
  <key>LSUIElement</key>
@@ -10,7 +10,7 @@
10
10
  </data>
11
11
  <key>Resources/Assets.car</key>
12
12
  <data>
13
- EvwqetqXdPImCl5x7nS2kIPD49Y=
13
+ l0+pnfS5aykXdUJ9hqLbM8D+kxA=
14
14
  </data>
15
15
  </dict>
16
16
  <key>files2</key>
@@ -26,7 +26,7 @@
26
26
  <dict>
27
27
  <key>hash2</key>
28
28
  <data>
29
- SkyYNOjv6tCr/MP0BYaswjWbcppNxwnvXBYKtGTpF4w=
29
+ w7jEtdTuow3Sd//xEnaf1qdzU3Mu7zdDF3IKDCN08Yw=
30
30
  </data>
31
31
  </dict>
32
32
  </dict>
@@ -22,6 +22,14 @@
22
22
  {
23
23
  "patch": "split-resize.patch",
24
24
  "patchSha256": "4d010271fb4eb52cf36fd29b66e7b3c52c44f6ba90296b944014d74bf3d4222a"
25
+ },
26
+ {
27
+ "patch": "kitty-graphics.patch",
28
+ "patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
29
+ },
30
+ {
31
+ "patch": "precise-cell-pixels.patch",
32
+ "patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
25
33
  }
26
34
  ],
27
35
  "experimentalExtensions": [
@@ -40,12 +48,12 @@
40
48
  "platform": "darwin",
41
49
  "arch": "arm64",
42
50
  "files": {
43
- "tmux": "83024593f485cd4ed363aff6f898823f4401fc7e34c9a46e5732ae6fb8588666",
44
- "lib/libutf8proc.3.2.3.dylib": "9d044f05ab5e8b19ae954485cfd8b7dd06e0cb66f0e02fe80be99533b21168ae",
51
+ "tmux": "78b5efc7cb7308e892487a0ea046c531a271680c812c8b8851796dbdf57b0fe8",
52
+ "lib/libutf8proc.3.3.0.dylib": "da17133f84767a9873ff61402ac1a9d6a19ddfc1efb90cffc7c894df68532ce2",
45
53
  "lib/libncursesw.6.dylib": "c9b3c77b5874b0c694590774f7b7b1568b2bb13e82707bdf29cb7296446fe596",
46
- "lib/libevent_core-2.1.7.dylib": "5d8dc45957013ce78a17dbacd6f5ed80cec148983d8c7eeeb4408d2f7f74743e",
54
+ "lib/libevent_core-2.1.7.dylib": "65a205eee103b07356c1d6b1102b6b6ca08b0abaad44d960a0dad809722d0a9d",
47
55
  "licenses/interaction-journal.txt": "9f02e74957c4c5efb1b65cf35d14b291a7e83807d89e8afedf6e9cb5dbfed39c",
48
- "licenses/libutf8proc.3.2.3.dylib.txt": "3b510150d34f248a221bb88e1d811238d6c6c18b51231822c42974c39bb07256",
56
+ "licenses/libutf8proc.3.3.0.dylib.txt": "3b510150d34f248a221bb88e1d811238d6c6c18b51231822c42974c39bb07256",
49
57
  "licenses/libncursesw.6.dylib.txt": "708999f95527e1ffa670c6fce288c6c600cb477dd04afcc1171422b3dd4ee226",
50
58
  "licenses/libevent_core-2.1.7.dylib.txt": "ff02effc9b331edcdac387d198691bfa3e575e7d244ad10cb826aa51ef085670",
51
59
  "share/terminfo/31/1178": "e7172b912d33529169b2f877bf18009c2947d7045a845ec2178ada7bba2b8b48",
@@ -22,6 +22,14 @@
22
22
  {
23
23
  "patch": "split-resize.patch",
24
24
  "patchSha256": "4d010271fb4eb52cf36fd29b66e7b3c52c44f6ba90296b944014d74bf3d4222a"
25
+ },
26
+ {
27
+ "patch": "kitty-graphics.patch",
28
+ "patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
29
+ },
30
+ {
31
+ "patch": "precise-cell-pixels.patch",
32
+ "patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
25
33
  }
26
34
  ],
27
35
  "experimentalExtensions": [
@@ -68,7 +76,7 @@
68
76
  "platform": "linux",
69
77
  "arch": "arm64",
70
78
  "files": {
71
- "tmux": "eb7c528ae08224bde1142be8b904d62cbf9e8dcba303fcc782c32acabb860938",
79
+ "tmux": "23ce24278d4c778f6ce974c492e46fcc09e20de577125a841f1d5f91b21a79ce",
72
80
  "lib/libutf8proc.so.3": "83cd7bce65037a46494b154b0f7bfd2479bc1659f56321b9ec896f4b64fb490c",
73
81
  "lib/libtinfo.so.6": "22952a4f434fd0f356fad23b92de42084e6666d8a594eadd11511b059cc5bb66",
74
82
  "lib/libevent_core-2.1.so.7": "80ad9f14cdea14f65c7a778fef4d831d51d23edc55efd263d7f265587eab73d4",
@@ -22,6 +22,14 @@
22
22
  {
23
23
  "patch": "split-resize.patch",
24
24
  "patchSha256": "4d010271fb4eb52cf36fd29b66e7b3c52c44f6ba90296b944014d74bf3d4222a"
25
+ },
26
+ {
27
+ "patch": "kitty-graphics.patch",
28
+ "patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
29
+ },
30
+ {
31
+ "patch": "precise-cell-pixels.patch",
32
+ "patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
25
33
  }
26
34
  ],
27
35
  "experimentalExtensions": [
@@ -68,7 +76,7 @@
68
76
  "platform": "linux",
69
77
  "arch": "x64",
70
78
  "files": {
71
- "tmux": "4fc64502841e5944e6f26a0afed510203d0dfeb3ea63844366c4ee036da8583e",
79
+ "tmux": "ef3549656f724cb37e5e94f718d9997570fb3cf55bdafa2585fa1f827e66ba3c",
72
80
  "lib/libutf8proc.so.3": "a5aa10c99ac2e9a30c4e01fa4baa85a03dadf71eb73119c76fb3f9f54798962d",
73
81
  "lib/libtinfo.so.6": "170db4dd0c324ee73d8d5440a38708726066e1288aba942d57191c6f002240d9",
74
82
  "lib/libevent_core-2.1.so.7": "adf343350b739d3dba2b8a6cccbe0729cd9e9ee32b38ba69c08bd14f24a8431e",
@@ -583,6 +583,9 @@ function parsePaneSnapshot(stdout, expected) {
583
583
  * application-shell inventory. Names are resolved exactly to `$session_id`
584
584
  * before any pane query, and a second identity/topology snapshot closes races.
585
585
  */
586
+ const INVENTORY_FENCE_ATTEMPTS = 4;
587
+ /** 25, 50, 100 ms: enough for a mutation's identity stamps, bounded for every reader. */
588
+ const settleDelay = (attempt) => new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
586
589
  export async function discoverWorkspaceRegistryTerminalInventory(registry, runner, signal) {
587
590
  const workspaces = registry.list();
588
591
  if (workspaces.length > MAX_DISCOVERED_WORKSPACES) {
@@ -604,13 +607,6 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
604
607
  const before = await requiredTmuxResult(runner, argv, signal);
605
608
  if (before === null)
606
609
  continue;
607
- const first = parsePaneSnapshot(before, identity);
608
- const panes = first.panes;
609
- const after = await requiredTmuxResult(runner, argv, signal);
610
- if (after === null)
611
- throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
612
- const second = parsePaneSnapshot(after, identity);
613
- const latest = second.panes;
614
610
  // Foreground commands, titles, focus and paths can change while the same
615
611
  // pane remains valid. Fence binding/topology only, then publish one complete
616
612
  // latest snapshot rather than mixing presentation values across reads.
@@ -625,12 +621,29 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
625
621
  "windowStamp",
626
622
  "index",
627
623
  ];
628
- if (first.linksProof !== second.linksProof ||
629
- panes.length !== latest.length ||
630
- panes.some((pane, index) => proofKeys.some((key) => pane[key] !== latest[index][key]))) {
631
- throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
624
+ const matches = (first, second) => first.linksProof === second.linksProof &&
625
+ first.panes.length === second.panes.length &&
626
+ first.panes.every((pane, index) => proofKeys.every((key) => pane[key] === second.panes[index][key]));
627
+ // A just-created window or split is still being stamped, so topology can
628
+ // move between two reads. Let it settle for a bounded number of re-reads;
629
+ // only two consecutive matching reads are ever published.
630
+ let previous = parsePaneSnapshot(before, identity);
631
+ let stable = null;
632
+ for (let attempt = 0; attempt < INVENTORY_FENCE_ATTEMPTS && !stable; attempt += 1) {
633
+ if (attempt > 0)
634
+ await settleDelay(attempt);
635
+ const after = await requiredTmuxResult(runner, argv, signal);
636
+ if (after === null)
637
+ throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
638
+ const next = parsePaneSnapshot(after, identity);
639
+ if (matches(previous, next))
640
+ stable = next;
641
+ else
642
+ previous = next;
632
643
  }
633
- bySessionName.set(sessionName, latest);
644
+ if (!stable)
645
+ throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
646
+ bySessionName.set(sessionName, stable.panes);
634
647
  }
635
648
  const panes = [];
636
649
  for (const workspace of workspaces) {