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 +25 -9
- package/package.json +1 -1
- package/packages/contracts/src/__tests__/experience-kernel-boundary.test.ts +1 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +30 -19
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +9 -15
- package/packages/daemon/src/terminal/mirror/session-channel.ts +38 -21
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +21 -28
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.
|
|
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)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 ||
|
|
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
|
@@ -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.
|
|
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.
|
|
2821
|
-
authorityOrdinal !== this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1278
|
+
return "";
|
|
1277
1279
|
const axis = resizePreview()?.axis;
|
|
1278
1280
|
if (!axis)
|
|
1279
|
-
return
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
<
|
|
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)
|
|
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.
|
|
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.
|
|
3422
|
-
authorityOrdinal !== this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1742
|
+
if (!active?.active) return "";
|
|
1741
1743
|
const axis = resizePreview()?.axis;
|
|
1742
|
-
if (!axis) return
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
<
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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"
|