tmux-ide 2.9.0-beta.20 → 2.9.0-beta.21
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 +100 -8
- package/package.json +3 -2
- package/packages/daemon/dist/command-center/diagnostics.js +65 -0
- package/packages/daemon/dist/command-center/log-stream.js +9 -0
- package/packages/daemon/dist/command-center/server.js +7 -0
- package/packages/daemon/dist/lib/app-config.js +4 -2
- package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
- package/packages/daemon/dist/lib/soak-verdict.js +185 -35
- package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
- package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
- package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
- package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
- package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
- package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
- package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
- package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
- package/packages/daemon/dist/tui/mirror/theme.js +4 -31
- package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
- package/packages/daemon/src/command-center/diagnostics.ts +75 -0
- package/packages/daemon/src/command-center/log-stream.ts +8 -0
- package/packages/daemon/src/command-center/server.ts +8 -0
- package/packages/daemon/src/lib/app-config.ts +11 -3
- package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
- package/packages/daemon/src/lib/soak-verdict.ts +314 -23
- package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
- package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
- package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
- package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
- package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -0
- package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +9 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
- package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
- package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
- package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
- package/packages/daemon/src/tui/mirror/theme.ts +4 -39
- package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
- package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface ApplicationTerminalInteractionController {
|
|
|
102
102
|
readonly gestureId: string | null;
|
|
103
103
|
}): ApplicationResizePointerIngress | null;
|
|
104
104
|
resizePane(preview: ApplicationPaneResizePreview): void;
|
|
105
|
+
cancelPaneResize(): void;
|
|
105
106
|
keyboardResize(axis: "cols" | "rows", direction: -1 | 1): void;
|
|
106
107
|
routeWorkspaceKey(
|
|
107
108
|
event: Readonly<{ name: string; meta: boolean; ctrl: boolean; shift: boolean }>,
|
|
@@ -222,6 +223,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
222
223
|
startedAtMicros: number;
|
|
223
224
|
preview: ApplicationPaneResizePreview;
|
|
224
225
|
guideDigest: string;
|
|
226
|
+
readonly workspaceName: string;
|
|
225
227
|
readonly daemonGeneration: string;
|
|
226
228
|
readonly clientGeneration: number;
|
|
227
229
|
readonly rendererEpoch: number;
|
|
@@ -236,7 +238,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
236
238
|
readonly presentationBeforeDigest: string;
|
|
237
239
|
pointerIngress: ApplicationResizePointerIngress | null;
|
|
238
240
|
} | null = null;
|
|
239
|
-
|
|
241
|
+
type PendingPaneResize = {
|
|
240
242
|
readonly source: "keyboard" | "pointer";
|
|
241
243
|
readonly operationId: string;
|
|
242
244
|
readonly semanticPaneId: string;
|
|
@@ -261,7 +263,35 @@ export function createApplicationTerminalInteractionController(
|
|
|
261
263
|
receiptOutcome: "applied" | "unchanged" | null;
|
|
262
264
|
layoutCells: number | null;
|
|
263
265
|
frameRequested: boolean;
|
|
264
|
-
}
|
|
266
|
+
};
|
|
267
|
+
let pendingPaneResize: PendingPaneResize | null = null;
|
|
268
|
+
let paneResizeFrame: PendingPaneResize | null = null;
|
|
269
|
+
let resizePaneIdentity: Pick<
|
|
270
|
+
PendingPaneResize,
|
|
271
|
+
"semanticPaneId" | "incarnation" | "generation" | "sourceEpoch"
|
|
272
|
+
> | null = null;
|
|
273
|
+
const captureResizePaneIdentity = (semanticPaneId: string): void => {
|
|
274
|
+
const identity = options.generation()?.adapter?.paneCanonicalIdentity(semanticPaneId);
|
|
275
|
+
resizePaneIdentity = identity
|
|
276
|
+
? {
|
|
277
|
+
semanticPaneId,
|
|
278
|
+
incarnation: identity.incarnation,
|
|
279
|
+
generation: identity.generation,
|
|
280
|
+
sourceEpoch: identity.sourceEpoch,
|
|
281
|
+
}
|
|
282
|
+
: null;
|
|
283
|
+
};
|
|
284
|
+
const resizePaneIdentityCurrent = (): boolean => {
|
|
285
|
+
if (!resizePaneIdentity) return false;
|
|
286
|
+
const current = options
|
|
287
|
+
.generation()
|
|
288
|
+
?.adapter?.paneCanonicalIdentity(resizePaneIdentity.semanticPaneId);
|
|
289
|
+
return (
|
|
290
|
+
current?.incarnation === resizePaneIdentity.incarnation &&
|
|
291
|
+
current?.generation === resizePaneIdentity.generation &&
|
|
292
|
+
current?.sourceEpoch === resizePaneIdentity.sourceEpoch
|
|
293
|
+
);
|
|
294
|
+
};
|
|
265
295
|
let lastResizeGuidePresentationDigest: string | null = null;
|
|
266
296
|
let lastWindowPresentationDigest: string | null = null;
|
|
267
297
|
let diagnosticWindowFrame: DiagnosticWindowFrameContext | null = null;
|
|
@@ -398,23 +428,34 @@ export function createApplicationTerminalInteractionController(
|
|
|
398
428
|
};
|
|
399
429
|
const maybeRequestPaneResizeFrame = (): void => {
|
|
400
430
|
const pending = pendingPaneResize;
|
|
401
|
-
if (
|
|
402
|
-
!pending ||
|
|
403
|
-
pending.frameRequested ||
|
|
404
|
-
pending.receiptCells === null ||
|
|
405
|
-
pending.layoutCells !== pending.receiptCells
|
|
406
|
-
)
|
|
431
|
+
if (!pending || pending.receiptCells === null || pending.layoutCells !== pending.receiptCells)
|
|
407
432
|
return;
|
|
408
|
-
if (!
|
|
433
|
+
if (!resizePaneIdentityCurrent()) {
|
|
409
434
|
pendingPaneResize = null;
|
|
435
|
+
resizeTransaction.retire();
|
|
410
436
|
return;
|
|
411
437
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
438
|
+
// Detach before settlement: it can synchronously dispatch the latest target.
|
|
439
|
+
pendingPaneResize = null;
|
|
440
|
+
if (options.diagnosticsEnabled && pending.receiptOutcome !== "unchanged") {
|
|
441
|
+
if (paneResizeFrame)
|
|
442
|
+
diagnose("pane-resize-frame-superseded", { operationId: paneResizeFrame.operationId });
|
|
443
|
+
paneResizeFrame = pending;
|
|
444
|
+
pending.frameRequested = true;
|
|
445
|
+
try {
|
|
446
|
+
options.requestRender?.();
|
|
447
|
+
} catch {
|
|
448
|
+
paneResizeFrame = null;
|
|
449
|
+
}
|
|
417
450
|
}
|
|
451
|
+
resizeTransaction.observeLayout({
|
|
452
|
+
operationId: pending.operationId,
|
|
453
|
+
authorityGeneration: pending.daemonGeneration,
|
|
454
|
+
workspaceName: pending.workspaceName,
|
|
455
|
+
semanticPaneId: pending.semanticPaneId,
|
|
456
|
+
axis: pending.axis,
|
|
457
|
+
cells: pending.receiptCells,
|
|
458
|
+
});
|
|
418
459
|
};
|
|
419
460
|
const dispatchPaneResize = (
|
|
420
461
|
preview: ApplicationPaneResizePreview,
|
|
@@ -462,6 +503,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
462
503
|
layoutCells: null,
|
|
463
504
|
frameRequested: false,
|
|
464
505
|
};
|
|
506
|
+
diagnose("pane-resize-dispatch", resizeDiagnosticDetails(pendingPaneResize));
|
|
465
507
|
const failure: { current: PaneResizeFailure | null } = { current: null };
|
|
466
508
|
void resizeTerminalPane(
|
|
467
509
|
expected,
|
|
@@ -499,32 +541,14 @@ export function createApplicationTerminalInteractionController(
|
|
|
499
541
|
...resizeDiagnosticDetails(pending),
|
|
500
542
|
verb: "workspace.pane.resize",
|
|
501
543
|
});
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
authorityGeneration: pending.daemonGeneration,
|
|
507
|
-
workspaceName: pending.workspaceName,
|
|
508
|
-
semanticPaneId: pending.semanticPaneId,
|
|
509
|
-
axis: pending.axis,
|
|
510
|
-
cells: receipt.cells,
|
|
511
|
-
});
|
|
544
|
+
// A clamp/no-op still needs current canonical truth, not the guide.
|
|
545
|
+
const observed = paneCells(options.layout(), pending.semanticPaneId, pending.axis);
|
|
546
|
+
if (observed === receipt.cells) pending.layoutCells = observed;
|
|
547
|
+
if (receipt.outcome === "unchanged")
|
|
512
548
|
diagnoseCritical(`pane-resize:${operationId}:unchanged`, "pane-resize-unchanged", {
|
|
513
549
|
...resizeDiagnosticDetails(pending),
|
|
514
550
|
changed: false,
|
|
515
551
|
});
|
|
516
|
-
pendingPaneResize = null;
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
if (pending.layoutCells === receipt.cells)
|
|
520
|
-
resizeTransaction.observeLayout({
|
|
521
|
-
operationId,
|
|
522
|
-
authorityGeneration: pending.daemonGeneration,
|
|
523
|
-
workspaceName: pending.workspaceName,
|
|
524
|
-
semanticPaneId: pending.semanticPaneId,
|
|
525
|
-
axis: pending.axis,
|
|
526
|
-
cells: receipt.cells,
|
|
527
|
-
});
|
|
528
552
|
maybeRequestPaneResizeFrame();
|
|
529
553
|
})
|
|
530
554
|
.catch(() => {
|
|
@@ -551,7 +575,17 @@ export function createApplicationTerminalInteractionController(
|
|
|
551
575
|
submit: ({ operationId, intent }) => {
|
|
552
576
|
const state = resizeTransaction.state();
|
|
553
577
|
const context = resizeCommitContext;
|
|
554
|
-
|
|
578
|
+
const live = liveResizeTarget();
|
|
579
|
+
if (
|
|
580
|
+
state.phase !== "pending" ||
|
|
581
|
+
state.operationId !== operationId ||
|
|
582
|
+
!context ||
|
|
583
|
+
!resizePaneIdentityCurrent() ||
|
|
584
|
+
live?.daemonGeneration !== state.authorityGeneration ||
|
|
585
|
+
live.workspaceName !== state.workspaceName ||
|
|
586
|
+
!options.layout().current?.panes.some((pane) => pane.pane === intent.semanticPaneId)
|
|
587
|
+
)
|
|
588
|
+
throw new Error("resize target retired");
|
|
555
589
|
dispatchPaneResize(
|
|
556
590
|
{
|
|
557
591
|
semanticPaneId: intent.semanticPaneId,
|
|
@@ -566,6 +600,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
566
600
|
);
|
|
567
601
|
},
|
|
568
602
|
onState: (state) => {
|
|
603
|
+
if (state.phase === "idle" && state.outcome?.kind === "settled") resizeCommitContext = null;
|
|
569
604
|
if (state.phase === "idle" && state.outcome?.kind === "reverted") {
|
|
570
605
|
diagnose("pane-resize-transaction-reverted", {
|
|
571
606
|
operationId: state.outcome.operationId,
|
|
@@ -803,6 +838,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
803
838
|
pendingWindowRename = null;
|
|
804
839
|
diagnosticWindowFrame = null;
|
|
805
840
|
pendingPaneResize = null;
|
|
841
|
+
paneResizeFrame = null;
|
|
806
842
|
pendingResizeGuide = null;
|
|
807
843
|
resizeCommitContext = null;
|
|
808
844
|
resizeTransaction.retire();
|
|
@@ -874,20 +910,24 @@ export function createApplicationTerminalInteractionController(
|
|
|
874
910
|
});
|
|
875
911
|
}
|
|
876
912
|
}
|
|
913
|
+
const resizeState = resizeTransaction.state();
|
|
914
|
+
if (
|
|
915
|
+
resizeState.phase !== "idle" &&
|
|
916
|
+
!snapshot.current?.panes.some((pane) => pane.pane === resizeState.semanticPaneId)
|
|
917
|
+
) {
|
|
918
|
+
resizeTransaction.retire();
|
|
919
|
+
pendingPaneResize = null;
|
|
920
|
+
pendingResizeGuide = null;
|
|
921
|
+
}
|
|
877
922
|
if (pendingPaneResize) {
|
|
878
923
|
const cells = paneCells(snapshot, pendingPaneResize.semanticPaneId, pendingPaneResize.axis);
|
|
879
|
-
if (
|
|
924
|
+
if (
|
|
925
|
+
cells !== null &&
|
|
926
|
+
cells !== pendingPaneResize.layoutCells &&
|
|
927
|
+
cells !== pendingPaneResize.beforeCells
|
|
928
|
+
) {
|
|
880
929
|
pendingPaneResize.layoutCells = cells;
|
|
881
930
|
diagnose("pane-resize-layout", resizeDiagnosticDetails(pendingPaneResize));
|
|
882
|
-
if (pendingPaneResize.receiptCells === cells)
|
|
883
|
-
resizeTransaction.observeLayout({
|
|
884
|
-
operationId: pendingPaneResize.operationId,
|
|
885
|
-
authorityGeneration: pendingPaneResize.daemonGeneration,
|
|
886
|
-
workspaceName: pendingPaneResize.workspaceName,
|
|
887
|
-
semanticPaneId: pendingPaneResize.semanticPaneId,
|
|
888
|
-
axis: pendingPaneResize.axis,
|
|
889
|
-
cells,
|
|
890
|
-
});
|
|
891
931
|
}
|
|
892
932
|
}
|
|
893
933
|
maybeRequestWindowSwitchFrame();
|
|
@@ -928,7 +968,9 @@ export function createApplicationTerminalInteractionController(
|
|
|
928
968
|
const live = liveResizeTarget();
|
|
929
969
|
const canonicalCells = paneCells(options.layout(), preview.semanticPaneId, preview.axis);
|
|
930
970
|
const state = resizeTransaction.state();
|
|
931
|
-
if (live && canonicalCells !== null && state.phase === "idle")
|
|
971
|
+
if (!resizeCommitContext && live && canonicalCells !== null && state.phase === "idle") {
|
|
972
|
+
resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
|
|
973
|
+
captureResizePaneIdentity(preview.semanticPaneId);
|
|
932
974
|
resizeTransaction.begin({
|
|
933
975
|
authorityGeneration: live.daemonGeneration,
|
|
934
976
|
workspaceName: live.workspaceName,
|
|
@@ -936,6 +978,9 @@ export function createApplicationTerminalInteractionController(
|
|
|
936
978
|
axis: preview.axis,
|
|
937
979
|
canonicalCells,
|
|
938
980
|
});
|
|
981
|
+
}
|
|
982
|
+
if (resizeCommitContext?.source === "pointer")
|
|
983
|
+
resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
|
|
939
984
|
resizeTransaction.move(preview.cells);
|
|
940
985
|
if (!options.diagnosticsEnabled) return;
|
|
941
986
|
if (pendingResizeGuide) {
|
|
@@ -983,6 +1028,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
983
1028
|
startedAtMicros,
|
|
984
1029
|
preview,
|
|
985
1030
|
guideDigest,
|
|
1031
|
+
workspaceName: active.connection.workspaceName,
|
|
986
1032
|
daemonGeneration: active.daemonGeneration,
|
|
987
1033
|
clientGeneration: clientGeneration!,
|
|
988
1034
|
rendererEpoch: active.rendererEpoch,
|
|
@@ -1014,16 +1060,38 @@ export function createApplicationTerminalInteractionController(
|
|
|
1014
1060
|
resizePane(preview) {
|
|
1015
1061
|
pendingResizeGuide = null;
|
|
1016
1062
|
lastResizeGuidePresentationDigest = null;
|
|
1063
|
+
if (!resizeCommitContext || resizeCommitContext.source !== "pointer") return;
|
|
1064
|
+
resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
|
|
1017
1065
|
resizeTransaction.move(preview.cells);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1066
|
+
resizeTransaction.release();
|
|
1067
|
+
if (options.diagnosticsEnabled)
|
|
1068
|
+
try {
|
|
1069
|
+
const active = options.generation();
|
|
1070
|
+
const identity = active?.adapter?.paneCanonicalIdentity(preview.semanticPaneId);
|
|
1071
|
+
diagnose("pane-resize-release", {
|
|
1072
|
+
semanticPaneId: preview.semanticPaneId,
|
|
1073
|
+
axis: preview.axis,
|
|
1074
|
+
requestedCells: preview.cells,
|
|
1075
|
+
pointerIngress: preview.pointerIngress ?? null,
|
|
1076
|
+
transactionPhase: resizeTransaction.state().phase,
|
|
1077
|
+
workspaceName: active?.connection?.workspaceName,
|
|
1078
|
+
daemonGeneration: active?.daemonGeneration,
|
|
1079
|
+
clientGeneration: active?.client?.getSnapshot().generation,
|
|
1080
|
+
rendererEpoch: active?.rendererEpoch,
|
|
1081
|
+
sourceEpoch: identity?.sourceEpoch,
|
|
1082
|
+
generation: identity?.generation,
|
|
1083
|
+
incarnation: identity?.incarnation,
|
|
1084
|
+
});
|
|
1085
|
+
} catch {
|
|
1086
|
+
/* Diagnostics never own gesture completion. */
|
|
1087
|
+
}
|
|
1088
|
+
// Retain context while a coalesced final target awaits dispatch.
|
|
1089
|
+
if (resizeTransaction.state().phase === "idle") resizeCommitContext = null;
|
|
1090
|
+
},
|
|
1091
|
+
cancelPaneResize() {
|
|
1092
|
+
resizeTransaction.cancelDrag();
|
|
1093
|
+
pendingResizeGuide = null;
|
|
1094
|
+
if (resizeTransaction.state().phase === "idle") resizeCommitContext = null;
|
|
1027
1095
|
},
|
|
1028
1096
|
keyboardResize(axis, direction) {
|
|
1029
1097
|
const paneId = options.focusedPane?.() ?? null;
|
|
@@ -1042,12 +1110,13 @@ export function createApplicationTerminalInteractionController(
|
|
|
1042
1110
|
})
|
|
1043
1111
|
)
|
|
1044
1112
|
return;
|
|
1045
|
-
resizeTransaction.move(Math.max(1, beforeCells + direction));
|
|
1046
1113
|
resizeCommitContext = { source: "keyboard", pointerIngress: null };
|
|
1114
|
+
captureResizePaneIdentity(paneId);
|
|
1115
|
+
resizeTransaction.move(Math.max(1, beforeCells + direction));
|
|
1047
1116
|
try {
|
|
1048
1117
|
resizeTransaction.release();
|
|
1049
1118
|
} finally {
|
|
1050
|
-
resizeCommitContext = null;
|
|
1119
|
+
if (resizeTransaction.state().phase === "idle") resizeCommitContext = null;
|
|
1051
1120
|
}
|
|
1052
1121
|
},
|
|
1053
1122
|
routeWorkspaceKey(event) {
|
|
@@ -1539,6 +1608,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1539
1608
|
pendingResizeGuide = null;
|
|
1540
1609
|
const settledAtMicros = diagnosticNowMicros();
|
|
1541
1610
|
let identityExact: boolean;
|
|
1611
|
+
let canonicalAfter = null;
|
|
1542
1612
|
try {
|
|
1543
1613
|
const active = options.generation();
|
|
1544
1614
|
const identity = active?.adapter?.paneCanonicalIdentity(settled.preview.semanticPaneId);
|
|
@@ -1547,6 +1617,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1547
1617
|
const layoutPane = layoutWindow?.panes.find(
|
|
1548
1618
|
({ pane }) => pane === settled.preview.semanticPaneId,
|
|
1549
1619
|
);
|
|
1620
|
+
canonicalAfter = identity ?? null;
|
|
1550
1621
|
identityExact =
|
|
1551
1622
|
active?.status === "live" &&
|
|
1552
1623
|
active.daemonGeneration === settled.daemonGeneration &&
|
|
@@ -1556,11 +1627,8 @@ export function createApplicationTerminalInteractionController(
|
|
|
1556
1627
|
identity?.sourceEpoch === settled.sourceEpoch &&
|
|
1557
1628
|
identity.generation === settled.generation &&
|
|
1558
1629
|
identity.incarnation === settled.incarnation &&
|
|
1559
|
-
identity.revision
|
|
1560
|
-
|
|
1561
|
-
identity.cols === settled.cols &&
|
|
1562
|
-
identity.rows === settled.rows &&
|
|
1563
|
-
layoutPane?.width === settled.cols &&
|
|
1630
|
+
identity.revision >= settled.revision &&
|
|
1631
|
+
layoutPane?.width === identity.cols &&
|
|
1564
1632
|
layoutWindow !== undefined &&
|
|
1565
1633
|
layoutWindow !== null &&
|
|
1566
1634
|
nativePaneResizeCells(
|
|
@@ -1568,7 +1636,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1568
1636
|
"rows",
|
|
1569
1637
|
layoutWindow.paneBorderStatus,
|
|
1570
1638
|
layoutWindow.rows,
|
|
1571
|
-
) ===
|
|
1639
|
+
) === identity.rows;
|
|
1572
1640
|
} catch {
|
|
1573
1641
|
identityExact = false;
|
|
1574
1642
|
}
|
|
@@ -1588,6 +1656,7 @@ export function createApplicationTerminalInteractionController(
|
|
|
1588
1656
|
settled.presentationBeforeDigest.length === 64 &&
|
|
1589
1657
|
presentationDigest !== settled.presentationBeforeDigest,
|
|
1590
1658
|
identityExact,
|
|
1659
|
+
canonicalAfter,
|
|
1591
1660
|
durationMicros:
|
|
1592
1661
|
settledAtMicros === null ? null : settledAtMicros - settled.startedAtMicros,
|
|
1593
1662
|
};
|
|
@@ -1605,9 +1674,8 @@ export function createApplicationTerminalInteractionController(
|
|
|
1605
1674
|
writerHealth,
|
|
1606
1675
|
});
|
|
1607
1676
|
}
|
|
1608
|
-
const pending =
|
|
1677
|
+
const pending = paneResizeFrame;
|
|
1609
1678
|
if (!pending?.frameRequested) return;
|
|
1610
|
-
pendingPaneResize = null;
|
|
1611
1679
|
let canonicalAfter = null;
|
|
1612
1680
|
let identityLineageExact = false;
|
|
1613
1681
|
try {
|
|
@@ -1625,13 +1693,24 @@ export function createApplicationTerminalInteractionController(
|
|
|
1625
1693
|
identity.sourceEpoch === pending.sourceEpoch &&
|
|
1626
1694
|
identity.generation === pending.generation &&
|
|
1627
1695
|
identity.incarnation === pending.incarnation &&
|
|
1628
|
-
identity.revision >= pending.revision
|
|
1629
|
-
|
|
1696
|
+
identity.revision >= pending.revision;
|
|
1697
|
+
// Layout publication can precede the canonical terminal snapshot. Keep
|
|
1698
|
+
// the bounded diagnostic slot until a coherent frame consumes both.
|
|
1699
|
+
// A newer operation explicitly supersedes it; diagnostics never hold
|
|
1700
|
+
// the resize transaction or request a polling render loop.
|
|
1701
|
+
if (
|
|
1702
|
+
identityLineageExact &&
|
|
1703
|
+
(identity.cols !== paneCells(options.layout(), pending.semanticPaneId, "cols") ||
|
|
1704
|
+
identity.rows !== paneCells(options.layout(), pending.semanticPaneId, "rows") ||
|
|
1705
|
+
layoutCells !== pending.layoutCells)
|
|
1706
|
+
)
|
|
1707
|
+
return;
|
|
1630
1708
|
}
|
|
1631
1709
|
} catch {
|
|
1632
1710
|
canonicalAfter = null;
|
|
1633
1711
|
identityLineageExact = false;
|
|
1634
1712
|
}
|
|
1713
|
+
paneResizeFrame = null;
|
|
1635
1714
|
const presentationDigest = resizePresentationDigest(null);
|
|
1636
1715
|
const details = {
|
|
1637
1716
|
...resizeDiagnosticDetails(pending),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseTerminalHostColor, terminalHostMode } from "../../../lib/terminal-host-color.ts";
|
|
1
2
|
import type { ResolvedThemeMode } from "../../../lib/theme-mode.ts";
|
|
2
3
|
|
|
3
4
|
export type ApplicationTerminalPaletteAvailability = "pending" | "available" | "unavailable";
|
|
@@ -62,6 +63,7 @@ export interface ApplicationTerminalPaletteOwner {
|
|
|
62
63
|
readonly ready: Promise<void>;
|
|
63
64
|
getSnapshot(): ApplicationTerminalPaletteSnapshot;
|
|
64
65
|
subscribe(listener: () => void): () => void;
|
|
66
|
+
/** Query now with a bounded retry pair for terminals still changing theme. */
|
|
65
67
|
refresh(): Promise<void>;
|
|
66
68
|
dispose(): void;
|
|
67
69
|
}
|
|
@@ -74,7 +76,7 @@ const EMPTY_PALETTE = Object.freeze(Array<string | null>(PALETTE_SIZE).fill(null
|
|
|
74
76
|
function normalizeColor(value: unknown): string | null {
|
|
75
77
|
if (typeof value !== "string") return null;
|
|
76
78
|
const normalized = value.trim().toLowerCase();
|
|
77
|
-
return normalized
|
|
79
|
+
return parseTerminalHostColor(normalized) ? normalized : null;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
function normalizePalette(values: unknown): readonly (string | null)[] {
|
|
@@ -99,19 +101,6 @@ function immutableCapabilities(value: unknown): Readonly<Record<string, unknown>
|
|
|
99
101
|
return immutableValue(value) as Readonly<Record<string, unknown>>;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
function detectedMode(background: string | null, fallback: ResolvedThemeMode): ResolvedThemeMode {
|
|
103
|
-
if (!background) return fallback;
|
|
104
|
-
const match = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(background);
|
|
105
|
-
if (!match) return fallback;
|
|
106
|
-
const hex = match[1]!;
|
|
107
|
-
const expanded =
|
|
108
|
-
hex.length === 3 ? `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}` : hex;
|
|
109
|
-
const red = Number.parseInt(expanded.slice(0, 2), 16) / 255;
|
|
110
|
-
const green = Number.parseInt(expanded.slice(2, 4), 16) / 255;
|
|
111
|
-
const blue = Number.parseInt(expanded.slice(4, 6), 16) / 255;
|
|
112
|
-
return 0.299 * red + 0.587 * green + 0.114 * blue > 0.5 ? "light" : "dark";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
104
|
function snapshotSignature(input: Omit<ApplicationTerminalPaletteSnapshot, "signature">): string {
|
|
116
105
|
return JSON.stringify(input);
|
|
117
106
|
}
|
|
@@ -162,7 +151,7 @@ function availableSnapshot(
|
|
|
162
151
|
tekBackground: normalizeColor(result.tekBackground),
|
|
163
152
|
highlightBackground: normalizeColor(result.highlightBackground),
|
|
164
153
|
highlightForeground: normalizeColor(result.highlightForeground),
|
|
165
|
-
detectedMode:
|
|
154
|
+
detectedMode: terminalHostMode(defaultBackground) ?? fallbackMode,
|
|
166
155
|
capabilities,
|
|
167
156
|
});
|
|
168
157
|
}
|
|
@@ -191,6 +180,9 @@ export function createApplicationTerminalPaletteOwner(
|
|
|
191
180
|
let lastValid: ApplicationTerminalPaletteSnapshot | null = null;
|
|
192
181
|
let active: Promise<void> | null = null;
|
|
193
182
|
let queued = false;
|
|
183
|
+
let modeHint: ResolvedThemeMode | null = null;
|
|
184
|
+
let modeGeneration = 0;
|
|
185
|
+
let awaitingModePalette = false;
|
|
194
186
|
let disposed = false;
|
|
195
187
|
|
|
196
188
|
const publish = (next: ApplicationTerminalPaletteSnapshot): void => {
|
|
@@ -206,22 +198,40 @@ export function createApplicationTerminalPaletteOwner(
|
|
|
206
198
|
return active;
|
|
207
199
|
}
|
|
208
200
|
renderer.clearPaletteCache();
|
|
201
|
+
const requestedModeGeneration = modeGeneration;
|
|
202
|
+
const guardModeForQuery = awaitingModePalette;
|
|
209
203
|
const generation = Promise.resolve()
|
|
210
204
|
.then(() => renderer.getPalette({ size: PALETTE_SIZE, timeout: queryTimeoutMs }))
|
|
211
205
|
.then((result) => {
|
|
212
|
-
if (disposed) return;
|
|
206
|
+
if (disposed || requestedModeGeneration !== modeGeneration) return;
|
|
207
|
+
const reportedMode = terminalHostMode(result.defaultBackground);
|
|
208
|
+
// A terminal can still answer with its previous palette while switching.
|
|
209
|
+
// Queries issued during the transition wait for a matching reply.
|
|
210
|
+
if (guardModeForQuery && modeHint && reportedMode && reportedMode !== modeHint) return;
|
|
213
211
|
if (!hasReportedColor(result)) {
|
|
214
212
|
if (!lastValid)
|
|
215
|
-
publish(
|
|
213
|
+
publish(
|
|
214
|
+
fallbackSnapshot(
|
|
215
|
+
"unavailable",
|
|
216
|
+
modeHint ?? renderer.themeMode ?? "dark",
|
|
217
|
+
capabilities,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
|
-
const next = availableSnapshot(
|
|
222
|
+
const next = availableSnapshot(
|
|
223
|
+
result,
|
|
224
|
+
modeHint ?? renderer.themeMode ?? "dark",
|
|
225
|
+
capabilities,
|
|
226
|
+
);
|
|
219
227
|
lastValid = next;
|
|
220
228
|
publish(next);
|
|
221
229
|
})
|
|
222
230
|
.catch(() => {
|
|
223
|
-
if (disposed || lastValid) return;
|
|
224
|
-
publish(
|
|
231
|
+
if (disposed || requestedModeGeneration !== modeGeneration || lastValid) return;
|
|
232
|
+
publish(
|
|
233
|
+
fallbackSnapshot("unavailable", modeHint ?? renderer.themeMode ?? "dark", capabilities),
|
|
234
|
+
);
|
|
225
235
|
})
|
|
226
236
|
.finally(() => {
|
|
227
237
|
if (active !== generation) return;
|
|
@@ -234,20 +244,37 @@ export function createApplicationTerminalPaletteOwner(
|
|
|
234
244
|
return generation;
|
|
235
245
|
};
|
|
236
246
|
|
|
237
|
-
const scheduleFollowUps = (): void => {
|
|
238
|
-
if (disposed) return;
|
|
239
|
-
|
|
247
|
+
const scheduleFollowUps = (): Promise<void> => {
|
|
248
|
+
if (disposed) return Promise.resolve();
|
|
249
|
+
const first = refresh();
|
|
250
|
+
// A burst owns only one bounded pair of retries, never one pair per event.
|
|
251
|
+
for (const timer of timers) cancelTimeout(timer);
|
|
252
|
+
timers.clear();
|
|
240
253
|
for (const delay of FOLLOW_UP_DELAYS_MS) {
|
|
241
254
|
const timer = scheduleTimeout(() => {
|
|
242
255
|
timers.delete(timer);
|
|
256
|
+
// After the bounded transition window, a fresh measured OSC 11 color
|
|
257
|
+
// wins even when a custom theme's luminance disagrees with its hint.
|
|
258
|
+
if (delay === FOLLOW_UP_DELAYS_MS[FOLLOW_UP_DELAYS_MS.length - 1])
|
|
259
|
+
awaitingModePalette = false;
|
|
243
260
|
if (!disposed) void refresh();
|
|
244
261
|
}, delay);
|
|
245
262
|
timers.add(timer);
|
|
246
263
|
}
|
|
264
|
+
return first;
|
|
247
265
|
};
|
|
248
266
|
|
|
249
|
-
const onThemeMode = (): void => {
|
|
250
|
-
|
|
267
|
+
const onThemeMode = (mode: ResolvedThemeMode): void => {
|
|
268
|
+
const changed = modeHint !== mode;
|
|
269
|
+
if (changed) modeGeneration += 1;
|
|
270
|
+
modeHint = mode;
|
|
271
|
+
if (changed && snapshot.detectedMode !== mode) {
|
|
272
|
+
awaitingModePalette = true;
|
|
273
|
+
// Track hints even while locked, so unlocking cannot resurrect old colors.
|
|
274
|
+
lastValid = null;
|
|
275
|
+
publish(fallbackSnapshot("unavailable", mode, capabilities));
|
|
276
|
+
}
|
|
277
|
+
if (isThemeModeUnlocked()) void scheduleFollowUps();
|
|
251
278
|
};
|
|
252
279
|
const onCapabilities = (next: unknown): void => {
|
|
253
280
|
capabilities = immutableCapabilities(next);
|
|
@@ -261,14 +288,14 @@ export function createApplicationTerminalPaletteOwner(
|
|
|
261
288
|
publish(
|
|
262
289
|
fallbackSnapshot(
|
|
263
290
|
snapshot.availability === "pending" ? "pending" : "unavailable",
|
|
264
|
-
renderer.themeMode ?? "dark",
|
|
291
|
+
modeHint ?? renderer.themeMode ?? "dark",
|
|
265
292
|
capabilities,
|
|
266
293
|
),
|
|
267
294
|
);
|
|
268
295
|
};
|
|
269
296
|
const onThemeNotification = (sequence: string): boolean => {
|
|
270
297
|
if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n") return false;
|
|
271
|
-
|
|
298
|
+
onThemeMode(sequence === "\x1b[?997;1n" ? "dark" : "light");
|
|
272
299
|
return false;
|
|
273
300
|
};
|
|
274
301
|
|
|
@@ -285,7 +312,7 @@ export function createApplicationTerminalPaletteOwner(
|
|
|
285
312
|
listeners.add(listener);
|
|
286
313
|
return () => listeners.delete(listener);
|
|
287
314
|
},
|
|
288
|
-
refresh,
|
|
315
|
+
refresh: scheduleFollowUps,
|
|
289
316
|
dispose() {
|
|
290
317
|
if (disposed) return;
|
|
291
318
|
disposed = true;
|