tmux-ide 2.9.0-beta.49 → 2.9.0-beta.50

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/bin/cli.js CHANGED
@@ -11932,7 +11932,7 @@ var require_package = __commonJS({
11932
11932
  "package.json"(exports, module) {
11933
11933
  module.exports = {
11934
11934
  name: "tmux-ide",
11935
- version: "2.9.0-beta.49",
11935
+ version: "2.9.0-beta.50",
11936
11936
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11937
11937
  type: "module",
11938
11938
  bin: {
@@ -32671,6 +32671,10 @@ function snapshotFingerprint2(captureLines, cursorLine, fallbackSize) {
32671
32671
  function tmuxSingleQuote(value) {
32672
32672
  return `'${value.replaceAll("'", "'\\''")}'`;
32673
32673
  }
32674
+ function layoutIdentitiesEqual(left, right) {
32675
+ const paneIds = (layout) => layout?.leaves.map((leaf) => leaf.id);
32676
+ return left.zoomed === right.zoomed && JSON.stringify(paneIds(left)) === JSON.stringify(paneIds(right)) && JSON.stringify(paneIds(left.unzoomed)) === JSON.stringify(paneIds(right.unzoomed));
32677
+ }
32674
32678
  function defaultMirrorPaneId() {
32675
32679
  return `pane.mirror.${randomBytes3(8).toString("hex")}`;
32676
32680
  }
@@ -32793,7 +32797,9 @@ var init_session_channel = __esm({
32793
32797
  disposed = false;
32794
32798
  nativeGrid;
32795
32799
  nativeClientProbePending = false;
32800
+ // Native captures fence every geometry event; inventory fences identity only.
32796
32801
  windowAuthorityOrdinal = 0;
32802
+ windowIdentityOrdinal = 0;
32797
32803
  paneIncarnation = 0;
32798
32804
  recoveryOrdinal = 0;
32799
32805
  plainReseedActive = null;
@@ -34709,11 +34715,13 @@ var init_session_channel = __esm({
34709
34715
  rest
34710
34716
  ) || /^tmux-ide-copy-keys\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+-\s*:\s*(emacs|vi)\s*$/u.test(rest))) {
34711
34717
  this.windowAuthorityOrdinal += 1;
34718
+ this.windowIdentityOrdinal += 1;
34712
34719
  this.scheduleSync();
34713
34720
  return;
34714
34721
  }
34715
34722
  if (name === "layout-change" || name === "window-pane-changed" || name === "session-window-changed" || STRUCTURAL_NOTIFICATIONS.has(name)) {
34716
34723
  this.windowAuthorityOrdinal += 1;
34724
+ if (name !== "layout-change") this.windowIdentityOrdinal += 1;
34717
34725
  }
34718
34726
  if (this.opts.onNativeClientActivity && (NATIVE_CLIENT_NOTIFICATIONS.has(name) || name === "layout-change")) {
34719
34727
  this.probeNativeClientActivity();
@@ -34764,9 +34772,13 @@ var init_session_channel = __esm({
34764
34772
  }
34765
34773
  if (name === "layout-change") {
34766
34774
  const change = parseLayoutChange(rest);
34767
- if (!change) return;
34775
+ if (!change) {
34776
+ this.windowIdentityOrdinal += 1;
34777
+ return;
34778
+ }
34768
34779
  const parsed = parseLayout(change.visible);
34769
34780
  if (!parsed) {
34781
+ this.windowIdentityOrdinal += 1;
34770
34782
  this.scheduleSync();
34771
34783
  return;
34772
34784
  }
@@ -34775,6 +34787,9 @@ var init_session_channel = __esm({
34775
34787
  zoomed: change.zoomed,
34776
34788
  unzoomed: parseLayout(change.layout) ?? void 0
34777
34789
  };
34790
+ const previousLayout = this.pendingLayoutOutput.get(change.windowId)?.layout ?? this.layoutByWindow.get(change.windowId);
34791
+ if (!pendingLayout.unzoomed || !previousLayout || !layoutIdentitiesEqual(previousLayout, pendingLayout))
34792
+ this.windowIdentityOrdinal += 1;
34778
34793
  const membership = change.zoomed ? parseLayout(change.layout) : parsed;
34779
34794
  if (!membership) {
34780
34795
  this.scheduleSync();
@@ -35048,7 +35063,7 @@ var init_session_channel = __esm({
35048
35063
  return { listed, movedWindowRuntimeIds };
35049
35064
  }
35050
35065
  async refreshTrustedInventory(expectedRuntimeSessionId, attempt = 0) {
35051
- const authorityOrdinal = this.windowAuthorityOrdinal;
35066
+ const authorityOrdinal = this.windowIdentityOrdinal;
35052
35067
  const beforeLines = await this.io.request(
35053
35068
  `list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`
35054
35069
  );
@@ -35112,7 +35127,7 @@ var init_session_channel = __esm({
35112
35127
  );
35113
35128
  const confirmedWindowStage = await this.stageWindows(expectedRuntimeSessionId);
35114
35129
  const coherent = beforeLines.length === afterLines.length && beforeLines.every((line, index) => line === afterLines[index]);
35115
- if (windowStage.repairedIdentity || confirmedWindowStage.repairedIdentity || repairedPanes || !coherent || !this.windowStagesEqual(windowStage, confirmedWindowStage) || authorityOrdinal !== this.windowAuthorityOrdinal) {
35130
+ if (windowStage.repairedIdentity || confirmedWindowStage.repairedIdentity || repairedPanes || !coherent || !this.windowStageIdentitiesEqual(windowStage, confirmedWindowStage) || authorityOrdinal !== this.windowIdentityOrdinal) {
35116
35131
  if (attempt >= 1)
35117
35132
  throw new Error(`trusted inventory for ${this.opts.session} did not settle`);
35118
35133
  return await this.refreshTrustedInventory(expectedRuntimeSessionId, attempt + 1);
@@ -35252,7 +35267,11 @@ var init_session_channel = __esm({
35252
35267
  for (const runtimeId of layoutEmits) this.releasePendingLayout(runtimeId, syncOrdinal);
35253
35268
  this.emitLayoutAuthority();
35254
35269
  }
35255
- windowStagesEqual(left, right) {
35270
+ // Inventory establishes pane/window identity, not a frozen terminal size.
35271
+ // Native resize notifications independently publish geometry. Requiring two
35272
+ // identical sizes here makes continuous dragging exhaust the inventory retry
35273
+ // and revoke otherwise unchanged window links, stalling layout publication.
35274
+ windowStageIdentitiesEqual(left, right) {
35256
35275
  if (JSON.stringify(left.links) !== JSON.stringify(right.links) || left.currentWindow !== right.currentWindow || left.windows.size !== right.windows.size || left.layouts.size !== right.layouts.size) {
35257
35276
  return false;
35258
35277
  }
@@ -35260,10 +35279,7 @@ var init_session_channel = __esm({
35260
35279
  const rightWindow = right.windows.get(runtimeId);
35261
35280
  const leftLayout = left.layouts.get(runtimeId);
35262
35281
  const rightLayout = right.layouts.get(runtimeId);
35263
- if (!rightWindow || !leftLayout || !rightLayout || leftWindow.semanticId !== rightWindow.semanticId || leftWindow.name !== rightWindow.name || leftWindow.paneBorderStatus !== rightWindow.paneBorderStatus || leftWindow.modeKeys !== rightWindow.modeKeys || leftLayout.zoomed !== rightLayout.zoomed || JSON.stringify(leftLayout.unzoomed) !== JSON.stringify(rightLayout.unzoomed) || leftLayout.width !== rightLayout.width || leftLayout.height !== rightLayout.height || leftLayout.leaves.length !== rightLayout.leaves.length || leftLayout.leaves.some((leaf, index) => {
35264
- const candidate = rightLayout.leaves[index];
35265
- return !candidate || leaf.id !== candidate.id || leaf.left !== candidate.left || leaf.top !== candidate.top || leaf.width !== candidate.width || leaf.height !== candidate.height;
35266
- })) {
35282
+ if (!rightWindow || !leftLayout || !rightLayout || leftWindow.semanticId !== rightWindow.semanticId || leftWindow.name !== rightWindow.name || leftWindow.paneBorderStatus !== rightWindow.paneBorderStatus || leftWindow.modeKeys !== rightWindow.modeKeys || !layoutIdentitiesEqual(leftLayout, rightLayout)) {
35267
35283
  return false;
35268
35284
  }
35269
35285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.49",
3
+ "version": "2.9.0-beta.50",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -104,6 +104,7 @@ describe("experience-kernel import boundary", () => {
104
104
  "mission-projections.ts",
105
105
  "owner-scope-identity.ts",
106
106
  "pane-appearance.ts",
107
+ "pane-team.ts",
107
108
  "semantic-identity.ts",
108
109
  "visual-recipes.ts",
109
110
  "visual-tokens.ts",
@@ -145,6 +145,12 @@ function snapshotFingerprint(captureLines, cursorLine, fallbackSize) {
145
145
  function tmuxSingleQuote(value) {
146
146
  return `'${value.replaceAll("'", "'\\''")}'`;
147
147
  }
148
+ function layoutIdentitiesEqual(left, right) {
149
+ const paneIds = (layout) => layout?.leaves.map((leaf) => leaf.id);
150
+ return (left.zoomed === right.zoomed &&
151
+ JSON.stringify(paneIds(left)) === JSON.stringify(paneIds(right)) &&
152
+ JSON.stringify(paneIds(left.unzoomed)) === JSON.stringify(paneIds(right.unzoomed)));
153
+ }
148
154
  export function defaultMirrorPaneId() {
149
155
  return `pane.mirror.${randomBytes(8).toString("hex")}`;
150
156
  }
@@ -185,7 +191,9 @@ export class SessionChannel {
185
191
  disposed = false;
186
192
  nativeGrid;
187
193
  nativeClientProbePending = false;
194
+ // Native captures fence every geometry event; inventory fences identity only.
188
195
  windowAuthorityOrdinal = 0;
196
+ windowIdentityOrdinal = 0;
189
197
  paneIncarnation = 0;
190
198
  recoveryOrdinal = 0;
191
199
  plainReseedActive = null;
@@ -2334,6 +2342,7 @@ export class SessionChannel {
2334
2342
  (/^tmux-ide-pane-borders\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+-\s*:\s*(top|bottom|off)\s*$/u.test(rest) ||
2335
2343
  /^tmux-ide-copy-keys\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+-\s*:\s*(emacs|vi)\s*$/u.test(rest))) {
2336
2344
  this.windowAuthorityOrdinal += 1;
2345
+ this.windowIdentityOrdinal += 1;
2337
2346
  this.scheduleSync();
2338
2347
  return;
2339
2348
  }
@@ -2342,6 +2351,8 @@ export class SessionChannel {
2342
2351
  name === "session-window-changed" ||
2343
2352
  STRUCTURAL_NOTIFICATIONS.has(name)) {
2344
2353
  this.windowAuthorityOrdinal += 1;
2354
+ if (name !== "layout-change")
2355
+ this.windowIdentityOrdinal += 1;
2345
2356
  }
2346
2357
  // Layout changes remain a second honest wake-up: a native resize can arrive
2347
2358
  // before the once-per-second subscription notification. The inventory
@@ -2399,10 +2410,13 @@ export class SessionChannel {
2399
2410
  }
2400
2411
  if (name === "layout-change") {
2401
2412
  const change = parseLayoutChange(rest);
2402
- if (!change)
2413
+ if (!change) {
2414
+ this.windowIdentityOrdinal += 1;
2403
2415
  return;
2416
+ }
2404
2417
  const parsed = parseLayout(change.visible);
2405
2418
  if (!parsed) {
2419
+ this.windowIdentityOrdinal += 1;
2406
2420
  this.scheduleSync(); // never guess from a failed parse
2407
2421
  return;
2408
2422
  }
@@ -2411,6 +2425,12 @@ export class SessionChannel {
2411
2425
  zoomed: change.zoomed,
2412
2426
  unzoomed: parseLayout(change.layout) ?? undefined,
2413
2427
  };
2428
+ const previousLayout = this.pendingLayoutOutput.get(change.windowId)?.layout ??
2429
+ this.layoutByWindow.get(change.windowId);
2430
+ if (!pendingLayout.unzoomed ||
2431
+ !previousLayout ||
2432
+ !layoutIdentitiesEqual(previousLayout, pendingLayout))
2433
+ this.windowIdentityOrdinal += 1;
2414
2434
  // Resync on BOTH structural deltas: an unknown leaf (new pane) and a
2415
2435
  // known pane of this window missing from the leaves (a killed pane in a
2416
2436
  // surviving window emits only %layout-change — without this, its
@@ -2745,7 +2765,7 @@ export class SessionChannel {
2745
2765
  return { listed, movedWindowRuntimeIds };
2746
2766
  }
2747
2767
  async refreshTrustedInventory(expectedRuntimeSessionId, attempt = 0) {
2748
- const authorityOrdinal = this.windowAuthorityOrdinal;
2768
+ const authorityOrdinal = this.windowIdentityOrdinal;
2749
2769
  const beforeLines = await this.io.request(`list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`);
2750
2770
  if (this.disposed)
2751
2771
  throw new Error(`mirror session ${this.opts.session} is disposed`);
@@ -2817,8 +2837,8 @@ export class SessionChannel {
2817
2837
  confirmedWindowStage.repairedIdentity ||
2818
2838
  repairedPanes ||
2819
2839
  !coherent ||
2820
- !this.windowStagesEqual(windowStage, confirmedWindowStage) ||
2821
- authorityOrdinal !== this.windowAuthorityOrdinal) {
2840
+ !this.windowStageIdentitiesEqual(windowStage, confirmedWindowStage) ||
2841
+ authorityOrdinal !== this.windowIdentityOrdinal) {
2822
2842
  if (attempt >= 1)
2823
2843
  throw new Error(`trusted inventory for ${this.opts.session} did not settle`);
2824
2844
  return await this.refreshTrustedInventory(expectedRuntimeSessionId, attempt + 1);
@@ -2994,7 +3014,11 @@ export class SessionChannel {
2994
3014
  this.releasePendingLayout(runtimeId, syncOrdinal);
2995
3015
  this.emitLayoutAuthority();
2996
3016
  }
2997
- windowStagesEqual(left, right) {
3017
+ // Inventory establishes pane/window identity, not a frozen terminal size.
3018
+ // Native resize notifications independently publish geometry. Requiring two
3019
+ // identical sizes here makes continuous dragging exhaust the inventory retry
3020
+ // and revoke otherwise unchanged window links, stalling layout publication.
3021
+ windowStageIdentitiesEqual(left, right) {
2998
3022
  if (JSON.stringify(left.links) !== JSON.stringify(right.links) ||
2999
3023
  left.currentWindow !== right.currentWindow ||
3000
3024
  left.windows.size !== right.windows.size ||
@@ -3012,20 +3036,7 @@ export class SessionChannel {
3012
3036
  leftWindow.name !== rightWindow.name ||
3013
3037
  leftWindow.paneBorderStatus !== rightWindow.paneBorderStatus ||
3014
3038
  leftWindow.modeKeys !== rightWindow.modeKeys ||
3015
- leftLayout.zoomed !== rightLayout.zoomed ||
3016
- JSON.stringify(leftLayout.unzoomed) !== JSON.stringify(rightLayout.unzoomed) ||
3017
- leftLayout.width !== rightLayout.width ||
3018
- leftLayout.height !== rightLayout.height ||
3019
- leftLayout.leaves.length !== rightLayout.leaves.length ||
3020
- leftLayout.leaves.some((leaf, index) => {
3021
- const candidate = rightLayout.leaves[index];
3022
- return (!candidate ||
3023
- leaf.id !== candidate.id ||
3024
- leaf.left !== candidate.left ||
3025
- leaf.top !== candidate.top ||
3026
- leaf.width !== candidate.width ||
3027
- leaf.height !== candidate.height);
3028
- })) {
3039
+ !layoutIdentitiesEqual(leftLayout, rightLayout)) {
3029
3040
  return false;
3030
3041
  }
3031
3042
  }
@@ -1270,22 +1270,18 @@ export function ApplicationTerminalWorkspace(props) {
1270
1270
  }
1271
1271
  : null;
1272
1272
  });
1273
- const guideCells = createMemo(() => {
1273
+ // A single retained text surface moves with the divider. Recreating one
1274
+ // renderable per cell on every layout tick adds churn during rapid drags.
1275
+ const guideText = createMemo(() => {
1274
1276
  const active = guide();
1275
1277
  if (!active?.active)
1276
- return Object.freeze([]);
1278
+ return "";
1277
1279
  const axis = resizePreview()?.axis;
1278
1280
  if (!axis)
1279
- return Object.freeze([]);
1280
- const cells = [];
1281
- for (let y = 0; y < active.rect.height; y += 1)
1282
- for (let x = 0; x < active.rect.width; x += 1)
1283
- cells.push({
1284
- x: active.rect.x + x,
1285
- y: active.rect.y + y + topOffset(),
1286
- marker: ACTIVE_RESIZE_GUIDE_CELL[axis],
1287
- });
1288
- return Object.freeze(cells);
1281
+ return "";
1282
+ return Array(active.rect.height)
1283
+ .fill(ACTIVE_RESIZE_GUIDE_CELL[axis].repeat(active.rect.width))
1284
+ .join("\n");
1289
1285
  });
1290
1286
  return (<>
1291
1287
  <box position="absolute" left={0} top={topOffset()} width={props.width} height={props.height} onMouse={routePointer}/>
@@ -1441,9 +1437,7 @@ export function ApplicationTerminalWorkspace(props) {
1441
1437
  }}
1442
1438
  </For>
1443
1439
  <box position="absolute" left={guide()?.rect.x ?? 0} top={(guide()?.rect.y ?? 0) + topOffset()} width={guide()?.rect.width ?? 0} height={guide()?.rect.height ?? 0} backgroundColor={guide()?.active ? props.theme.colors.accent : props.theme.colors.accentMuted} onMouse={routePointer}/>
1444
- <For each={guideCells()}>
1445
- {(cell) => (<text position="absolute" left={cell.x} top={cell.y} width={1} height={1} zIndex={5} selectable={false} fg={props.theme.roles.text.primary} content={cell.marker} onMouse={routePointer}/>)}
1446
- </For>
1440
+ <text id="terminal-resize-guide" position="absolute" left={guide()?.rect.x ?? 0} top={(guide()?.rect.y ?? 0) + topOffset()} width={guide()?.rect.width ?? 0} height={guide()?.rect.height ?? 0} zIndex={5} selectable={false} fg={props.theme.roles.text.primary} content={guideText()} onMouse={routePointer}/>
1447
1441
  <Show when={pointerSelecting()}>
1448
1442
  <box position="absolute" left={0} top={0} width={props.width} height={props.height + topOffset()} zIndex={40} onMouse={routePointer}/>
1449
1443
  </Show>
@@ -429,6 +429,17 @@ interface WindowRecord {
429
429
  modeKeys?: "emacs" | "vi";
430
430
  }
431
431
 
432
+ type WindowLayout = ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout };
433
+
434
+ function layoutIdentitiesEqual(left: WindowLayout, right: WindowLayout): boolean {
435
+ const paneIds = (layout: ParsedLayout | undefined) => layout?.leaves.map((leaf) => leaf.id);
436
+ return (
437
+ left.zoomed === right.zoomed &&
438
+ JSON.stringify(paneIds(left)) === JSON.stringify(paneIds(right)) &&
439
+ JSON.stringify(paneIds(left.unzoomed)) === JSON.stringify(paneIds(right.unzoomed))
440
+ );
441
+ }
442
+
432
443
  interface WindowSyncStage {
433
444
  readonly windows: Map<string, WindowRecord>;
434
445
  readonly layouts: Map<string, ParsedLayout & { zoomed: boolean; unzoomed?: ParsedLayout }>;
@@ -489,7 +500,9 @@ export class SessionChannel {
489
500
  private disposed = false;
490
501
  private readonly nativeGrid: NativeGridCaptureReader;
491
502
  private nativeClientProbePending = false;
503
+ // Native captures fence every geometry event; inventory fences identity only.
492
504
  private windowAuthorityOrdinal = 0;
505
+ private windowIdentityOrdinal = 0;
493
506
  private paneIncarnation = 0;
494
507
  private recoveryOrdinal = 0;
495
508
  private plainReseedActive: PlainReseedLease | null = null;
@@ -2885,6 +2898,7 @@ export class SessionChannel {
2885
2898
  /^tmux-ide-copy-keys\s+\$[0-9]+\s+@[0-9]+\s+[0-9]+\s+-\s*:\s*(emacs|vi)\s*$/u.test(rest))
2886
2899
  ) {
2887
2900
  this.windowAuthorityOrdinal += 1;
2901
+ this.windowIdentityOrdinal += 1;
2888
2902
  this.scheduleSync();
2889
2903
  return;
2890
2904
  }
@@ -2895,6 +2909,7 @@ export class SessionChannel {
2895
2909
  STRUCTURAL_NOTIFICATIONS.has(name)
2896
2910
  ) {
2897
2911
  this.windowAuthorityOrdinal += 1;
2912
+ if (name !== "layout-change") this.windowIdentityOrdinal += 1;
2898
2913
  }
2899
2914
  // Layout changes remain a second honest wake-up: a native resize can arrive
2900
2915
  // before the once-per-second subscription notification. The inventory
@@ -2951,9 +2966,13 @@ export class SessionChannel {
2951
2966
  }
2952
2967
  if (name === "layout-change") {
2953
2968
  const change = parseLayoutChange(rest);
2954
- if (!change) return;
2969
+ if (!change) {
2970
+ this.windowIdentityOrdinal += 1;
2971
+ return;
2972
+ }
2955
2973
  const parsed = parseLayout(change.visible);
2956
2974
  if (!parsed) {
2975
+ this.windowIdentityOrdinal += 1;
2957
2976
  this.scheduleSync(); // never guess from a failed parse
2958
2977
  return;
2959
2978
  }
@@ -2962,6 +2981,15 @@ export class SessionChannel {
2962
2981
  zoomed: change.zoomed,
2963
2982
  unzoomed: parseLayout(change.layout) ?? undefined,
2964
2983
  };
2984
+ const previousLayout =
2985
+ this.pendingLayoutOutput.get(change.windowId)?.layout ??
2986
+ this.layoutByWindow.get(change.windowId);
2987
+ if (
2988
+ !pendingLayout.unzoomed ||
2989
+ !previousLayout ||
2990
+ !layoutIdentitiesEqual(previousLayout, pendingLayout)
2991
+ )
2992
+ this.windowIdentityOrdinal += 1;
2965
2993
  // Resync on BOTH structural deltas: an unknown leaf (new pane) and a
2966
2994
  // known pane of this window missing from the leaves (a killed pane in a
2967
2995
  // surviving window emits only %layout-change — without this, its
@@ -3325,7 +3353,7 @@ export class SessionChannel {
3325
3353
  expectedRuntimeSessionId: string,
3326
3354
  attempt = 0,
3327
3355
  ): Promise<TrustedMirrorSessionInventory> {
3328
- const authorityOrdinal = this.windowAuthorityOrdinal;
3356
+ const authorityOrdinal = this.windowIdentityOrdinal;
3329
3357
  const beforeLines = await this.io.request(
3330
3358
  `list-panes -s -t "${expectedRuntimeSessionId}" -F "${SESSION_PANE_DESCRIPTOR_FORMAT}"`,
3331
3359
  );
@@ -3418,8 +3446,8 @@ export class SessionChannel {
3418
3446
  confirmedWindowStage.repairedIdentity ||
3419
3447
  repairedPanes ||
3420
3448
  !coherent ||
3421
- !this.windowStagesEqual(windowStage, confirmedWindowStage) ||
3422
- authorityOrdinal !== this.windowAuthorityOrdinal
3449
+ !this.windowStageIdentitiesEqual(windowStage, confirmedWindowStage) ||
3450
+ authorityOrdinal !== this.windowIdentityOrdinal
3423
3451
  ) {
3424
3452
  if (attempt >= 1)
3425
3453
  throw new Error(`trusted inventory for ${this.opts.session} did not settle`);
@@ -3624,7 +3652,11 @@ export class SessionChannel {
3624
3652
  this.emitLayoutAuthority();
3625
3653
  }
3626
3654
 
3627
- private windowStagesEqual(left: WindowSyncStage, right: WindowSyncStage): boolean {
3655
+ // Inventory establishes pane/window identity, not a frozen terminal size.
3656
+ // Native resize notifications independently publish geometry. Requiring two
3657
+ // identical sizes here makes continuous dragging exhaust the inventory retry
3658
+ // and revoke otherwise unchanged window links, stalling layout publication.
3659
+ private windowStageIdentitiesEqual(left: WindowSyncStage, right: WindowSyncStage): boolean {
3628
3660
  if (
3629
3661
  JSON.stringify(left.links) !== JSON.stringify(right.links) ||
3630
3662
  left.currentWindow !== right.currentWindow ||
@@ -3645,22 +3677,7 @@ export class SessionChannel {
3645
3677
  leftWindow.name !== rightWindow.name ||
3646
3678
  leftWindow.paneBorderStatus !== rightWindow.paneBorderStatus ||
3647
3679
  leftWindow.modeKeys !== rightWindow.modeKeys ||
3648
- leftLayout.zoomed !== rightLayout.zoomed ||
3649
- JSON.stringify(leftLayout.unzoomed) !== JSON.stringify(rightLayout.unzoomed) ||
3650
- leftLayout.width !== rightLayout.width ||
3651
- leftLayout.height !== rightLayout.height ||
3652
- leftLayout.leaves.length !== rightLayout.leaves.length ||
3653
- leftLayout.leaves.some((leaf, index) => {
3654
- const candidate = rightLayout.leaves[index];
3655
- return (
3656
- !candidate ||
3657
- leaf.id !== candidate.id ||
3658
- leaf.left !== candidate.left ||
3659
- leaf.top !== candidate.top ||
3660
- leaf.width !== candidate.width ||
3661
- leaf.height !== candidate.height
3662
- );
3663
- })
3680
+ !layoutIdentitiesEqual(leftLayout, rightLayout)
3664
3681
  ) {
3665
3682
  return false;
3666
3683
  }
@@ -1735,20 +1735,16 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1735
1735
  }
1736
1736
  : null;
1737
1737
  });
1738
- const guideCells = createMemo(() => {
1738
+ // A single retained text surface moves with the divider. Recreating one
1739
+ // renderable per cell on every layout tick adds churn during rapid drags.
1740
+ const guideText = createMemo(() => {
1739
1741
  const active = guide();
1740
- if (!active?.active) return Object.freeze([]);
1742
+ if (!active?.active) return "";
1741
1743
  const axis = resizePreview()?.axis;
1742
- if (!axis) return Object.freeze([]);
1743
- const cells: Array<{ x: number; y: number; marker: string }> = [];
1744
- for (let y = 0; y < active.rect.height; y += 1)
1745
- for (let x = 0; x < active.rect.width; x += 1)
1746
- cells.push({
1747
- x: active.rect.x + x,
1748
- y: active.rect.y + y + topOffset(),
1749
- marker: ACTIVE_RESIZE_GUIDE_CELL[axis],
1750
- });
1751
- return Object.freeze(cells);
1744
+ if (!axis) return "";
1745
+ return Array(active.rect.height)
1746
+ .fill(ACTIVE_RESIZE_GUIDE_CELL[axis].repeat(active.rect.width))
1747
+ .join("\n");
1752
1748
  });
1753
1749
 
1754
1750
  return (
@@ -2115,22 +2111,19 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
2115
2111
  }
2116
2112
  onMouse={routePointer}
2117
2113
  />
2118
- <For each={guideCells()}>
2119
- {(cell) => (
2120
- <text
2121
- position="absolute"
2122
- left={cell.x}
2123
- top={cell.y}
2124
- width={1}
2125
- height={1}
2126
- zIndex={5}
2127
- selectable={false}
2128
- fg={props.theme.roles.text.primary}
2129
- content={cell.marker}
2130
- onMouse={routePointer}
2131
- />
2132
- )}
2133
- </For>
2114
+ <text
2115
+ id="terminal-resize-guide"
2116
+ position="absolute"
2117
+ left={guide()?.rect.x ?? 0}
2118
+ top={(guide()?.rect.y ?? 0) + topOffset()}
2119
+ width={guide()?.rect.width ?? 0}
2120
+ height={guide()?.rect.height ?? 0}
2121
+ zIndex={5}
2122
+ selectable={false}
2123
+ fg={props.theme.roles.text.primary}
2124
+ content={guideText()}
2125
+ onMouse={routePointer}
2126
+ />
2134
2127
  <Show when={pointerSelecting()}>
2135
2128
  <box
2136
2129
  position="absolute"