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.
Files changed (49) hide show
  1. package/bin/cli.js +100 -8
  2. package/package.json +3 -2
  3. package/packages/daemon/dist/command-center/diagnostics.js +65 -0
  4. package/packages/daemon/dist/command-center/log-stream.js +9 -0
  5. package/packages/daemon/dist/command-center/server.js +7 -0
  6. package/packages/daemon/dist/lib/app-config.js +4 -2
  7. package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
  8. package/packages/daemon/dist/lib/soak-verdict.js +185 -35
  9. package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
  10. package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
  11. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
  12. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
  13. package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
  14. package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
  15. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
  16. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
  17. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
  18. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
  19. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
  20. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
  21. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
  22. package/packages/daemon/dist/tui/mirror/theme.js +4 -31
  23. package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
  24. package/packages/daemon/src/command-center/diagnostics.ts +75 -0
  25. package/packages/daemon/src/command-center/log-stream.ts +8 -0
  26. package/packages/daemon/src/command-center/server.ts +8 -0
  27. package/packages/daemon/src/lib/app-config.ts +11 -3
  28. package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
  29. package/packages/daemon/src/lib/soak-verdict.ts +314 -23
  30. package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
  31. package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
  32. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
  33. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
  34. package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
  35. package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
  36. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
  37. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +1 -0
  38. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +9 -2
  39. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  40. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
  41. package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
  42. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
  43. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
  44. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
  45. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
  46. package/packages/daemon/src/tui/mirror/theme.ts +4 -39
  47. package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
  48. package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
  49. package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
@@ -54,6 +54,29 @@ export function createApplicationTerminalInteractionController(options) {
54
54
  let previousCurrentWindow = null;
55
55
  let pendingResizeGuide = null;
56
56
  let pendingPaneResize = null;
57
+ let paneResizeFrame = null;
58
+ let resizePaneIdentity = null;
59
+ const captureResizePaneIdentity = (semanticPaneId) => {
60
+ const identity = options.generation()?.adapter?.paneCanonicalIdentity(semanticPaneId);
61
+ resizePaneIdentity = identity
62
+ ? {
63
+ semanticPaneId,
64
+ incarnation: identity.incarnation,
65
+ generation: identity.generation,
66
+ sourceEpoch: identity.sourceEpoch,
67
+ }
68
+ : null;
69
+ };
70
+ const resizePaneIdentityCurrent = () => {
71
+ if (!resizePaneIdentity)
72
+ return false;
73
+ const current = options
74
+ .generation()
75
+ ?.adapter?.paneCanonicalIdentity(resizePaneIdentity.semanticPaneId);
76
+ return (current?.incarnation === resizePaneIdentity.incarnation &&
77
+ current?.generation === resizePaneIdentity.generation &&
78
+ current?.sourceEpoch === resizePaneIdentity.sourceEpoch);
79
+ };
57
80
  let lastResizeGuidePresentationDigest = null;
58
81
  let lastWindowPresentationDigest = null;
59
82
  let diagnosticWindowFrame = null;
@@ -144,22 +167,35 @@ export function createApplicationTerminalInteractionController(options) {
144
167
  };
145
168
  const maybeRequestPaneResizeFrame = () => {
146
169
  const pending = pendingPaneResize;
147
- if (!pending ||
148
- pending.frameRequested ||
149
- pending.receiptCells === null ||
150
- pending.layoutCells !== pending.receiptCells)
170
+ if (!pending || pending.receiptCells === null || pending.layoutCells !== pending.receiptCells)
151
171
  return;
152
- if (!options.diagnosticsEnabled) {
172
+ if (!resizePaneIdentityCurrent()) {
153
173
  pendingPaneResize = null;
174
+ resizeTransaction.retire();
154
175
  return;
155
176
  }
156
- pending.frameRequested = true;
157
- try {
158
- options.requestRender?.();
159
- }
160
- catch {
161
- pendingPaneResize = null;
177
+ // Detach before settlement: it can synchronously dispatch the latest target.
178
+ pendingPaneResize = null;
179
+ if (options.diagnosticsEnabled && pending.receiptOutcome !== "unchanged") {
180
+ if (paneResizeFrame)
181
+ diagnose("pane-resize-frame-superseded", { operationId: paneResizeFrame.operationId });
182
+ paneResizeFrame = pending;
183
+ pending.frameRequested = true;
184
+ try {
185
+ options.requestRender?.();
186
+ }
187
+ catch {
188
+ paneResizeFrame = null;
189
+ }
162
190
  }
191
+ resizeTransaction.observeLayout({
192
+ operationId: pending.operationId,
193
+ authorityGeneration: pending.daemonGeneration,
194
+ workspaceName: pending.workspaceName,
195
+ semanticPaneId: pending.semanticPaneId,
196
+ axis: pending.axis,
197
+ cells: pending.receiptCells,
198
+ });
163
199
  };
164
200
  const dispatchPaneResize = (preview, source, operationId, beforeCells) => {
165
201
  const expected = liveResizeTarget();
@@ -205,6 +241,7 @@ export function createApplicationTerminalInteractionController(options) {
205
241
  layoutCells: null,
206
242
  frameRequested: false,
207
243
  };
244
+ diagnose("pane-resize-dispatch", resizeDiagnosticDetails(pendingPaneResize));
208
245
  const failure = { current: null };
209
246
  void resizeTerminalPane(expected, liveResizeTarget, {
210
247
  operationId,
@@ -238,32 +275,15 @@ export function createApplicationTerminalInteractionController(options) {
238
275
  ...resizeDiagnosticDetails(pending),
239
276
  verb: "workspace.pane.resize",
240
277
  });
241
- if (receipt.outcome === "unchanged" && receipt.cells === pending.beforeCells) {
242
- pending.layoutCells = receipt.cells;
243
- resizeTransaction.observeLayout({
244
- operationId,
245
- authorityGeneration: pending.daemonGeneration,
246
- workspaceName: pending.workspaceName,
247
- semanticPaneId: pending.semanticPaneId,
248
- axis: pending.axis,
249
- cells: receipt.cells,
250
- });
278
+ // A clamp/no-op still needs current canonical truth, not the guide.
279
+ const observed = paneCells(options.layout(), pending.semanticPaneId, pending.axis);
280
+ if (observed === receipt.cells)
281
+ pending.layoutCells = observed;
282
+ if (receipt.outcome === "unchanged")
251
283
  diagnoseCritical(`pane-resize:${operationId}:unchanged`, "pane-resize-unchanged", {
252
284
  ...resizeDiagnosticDetails(pending),
253
285
  changed: false,
254
286
  });
255
- pendingPaneResize = null;
256
- return;
257
- }
258
- if (pending.layoutCells === receipt.cells)
259
- resizeTransaction.observeLayout({
260
- operationId,
261
- authorityGeneration: pending.daemonGeneration,
262
- workspaceName: pending.workspaceName,
263
- semanticPaneId: pending.semanticPaneId,
264
- axis: pending.axis,
265
- cells: receipt.cells,
266
- });
267
287
  maybeRequestPaneResizeFrame();
268
288
  })
269
289
  .catch(() => {
@@ -288,8 +308,15 @@ export function createApplicationTerminalInteractionController(options) {
288
308
  submit: ({ operationId, intent }) => {
289
309
  const state = resizeTransaction.state();
290
310
  const context = resizeCommitContext;
291
- if (state.phase !== "pending" || state.operationId !== operationId || !context)
292
- return;
311
+ const live = liveResizeTarget();
312
+ if (state.phase !== "pending" ||
313
+ state.operationId !== operationId ||
314
+ !context ||
315
+ !resizePaneIdentityCurrent() ||
316
+ live?.daemonGeneration !== state.authorityGeneration ||
317
+ live.workspaceName !== state.workspaceName ||
318
+ !options.layout().current?.panes.some((pane) => pane.pane === intent.semanticPaneId))
319
+ throw new Error("resize target retired");
293
320
  dispatchPaneResize({
294
321
  semanticPaneId: intent.semanticPaneId,
295
322
  axis: intent.axis,
@@ -299,6 +326,8 @@ export function createApplicationTerminalInteractionController(options) {
299
326
  }, context.source, operationId, state.canonicalCells);
300
327
  },
301
328
  onState: (state) => {
329
+ if (state.phase === "idle" && state.outcome?.kind === "settled")
330
+ resizeCommitContext = null;
302
331
  if (state.phase === "idle" && state.outcome?.kind === "reverted") {
303
332
  diagnose("pane-resize-transaction-reverted", {
304
333
  operationId: state.outcome.operationId,
@@ -516,6 +545,7 @@ export function createApplicationTerminalInteractionController(options) {
516
545
  pendingWindowRename = null;
517
546
  diagnosticWindowFrame = null;
518
547
  pendingPaneResize = null;
548
+ paneResizeFrame = null;
519
549
  pendingResizeGuide = null;
520
550
  resizeCommitContext = null;
521
551
  resizeTransaction.retire();
@@ -586,20 +616,20 @@ export function createApplicationTerminalInteractionController(options) {
586
616
  });
587
617
  }
588
618
  }
619
+ const resizeState = resizeTransaction.state();
620
+ if (resizeState.phase !== "idle" &&
621
+ !snapshot.current?.panes.some((pane) => pane.pane === resizeState.semanticPaneId)) {
622
+ resizeTransaction.retire();
623
+ pendingPaneResize = null;
624
+ pendingResizeGuide = null;
625
+ }
589
626
  if (pendingPaneResize) {
590
627
  const cells = paneCells(snapshot, pendingPaneResize.semanticPaneId, pendingPaneResize.axis);
591
- if (cells !== null && cells !== pendingPaneResize.beforeCells) {
628
+ if (cells !== null &&
629
+ cells !== pendingPaneResize.layoutCells &&
630
+ cells !== pendingPaneResize.beforeCells) {
592
631
  pendingPaneResize.layoutCells = cells;
593
632
  diagnose("pane-resize-layout", resizeDiagnosticDetails(pendingPaneResize));
594
- if (pendingPaneResize.receiptCells === cells)
595
- resizeTransaction.observeLayout({
596
- operationId: pendingPaneResize.operationId,
597
- authorityGeneration: pendingPaneResize.daemonGeneration,
598
- workspaceName: pendingPaneResize.workspaceName,
599
- semanticPaneId: pendingPaneResize.semanticPaneId,
600
- axis: pendingPaneResize.axis,
601
- cells,
602
- });
603
633
  }
604
634
  }
605
635
  maybeRequestWindowSwitchFrame();
@@ -639,7 +669,9 @@ export function createApplicationTerminalInteractionController(options) {
639
669
  const live = liveResizeTarget();
640
670
  const canonicalCells = paneCells(options.layout(), preview.semanticPaneId, preview.axis);
641
671
  const state = resizeTransaction.state();
642
- if (live && canonicalCells !== null && state.phase === "idle")
672
+ if (!resizeCommitContext && live && canonicalCells !== null && state.phase === "idle") {
673
+ resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
674
+ captureResizePaneIdentity(preview.semanticPaneId);
643
675
  resizeTransaction.begin({
644
676
  authorityGeneration: live.daemonGeneration,
645
677
  workspaceName: live.workspaceName,
@@ -647,6 +679,9 @@ export function createApplicationTerminalInteractionController(options) {
647
679
  axis: preview.axis,
648
680
  canonicalCells,
649
681
  });
682
+ }
683
+ if (resizeCommitContext?.source === "pointer")
684
+ resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
650
685
  resizeTransaction.move(preview.cells);
651
686
  if (!options.diagnosticsEnabled)
652
687
  return;
@@ -691,6 +726,7 @@ export function createApplicationTerminalInteractionController(options) {
691
726
  startedAtMicros,
692
727
  preview,
693
728
  guideDigest,
729
+ workspaceName: active.connection.workspaceName,
694
730
  daemonGeneration: active.daemonGeneration,
695
731
  clientGeneration: clientGeneration,
696
732
  rendererEpoch: active.rendererEpoch,
@@ -722,17 +758,42 @@ export function createApplicationTerminalInteractionController(options) {
722
758
  resizePane(preview) {
723
759
  pendingResizeGuide = null;
724
760
  lastResizeGuidePresentationDigest = null;
761
+ if (!resizeCommitContext || resizeCommitContext.source !== "pointer")
762
+ return;
763
+ resizeCommitContext = { source: "pointer", pointerIngress: preview.pointerIngress ?? null };
725
764
  resizeTransaction.move(preview.cells);
726
- resizeCommitContext = {
727
- source: "pointer",
728
- pointerIngress: preview.pointerIngress?.action === "up" ? preview.pointerIngress : null,
729
- };
730
- try {
731
- resizeTransaction.release();
732
- }
733
- finally {
765
+ resizeTransaction.release();
766
+ if (options.diagnosticsEnabled)
767
+ try {
768
+ const active = options.generation();
769
+ const identity = active?.adapter?.paneCanonicalIdentity(preview.semanticPaneId);
770
+ diagnose("pane-resize-release", {
771
+ semanticPaneId: preview.semanticPaneId,
772
+ axis: preview.axis,
773
+ requestedCells: preview.cells,
774
+ pointerIngress: preview.pointerIngress ?? null,
775
+ transactionPhase: resizeTransaction.state().phase,
776
+ workspaceName: active?.connection?.workspaceName,
777
+ daemonGeneration: active?.daemonGeneration,
778
+ clientGeneration: active?.client?.getSnapshot().generation,
779
+ rendererEpoch: active?.rendererEpoch,
780
+ sourceEpoch: identity?.sourceEpoch,
781
+ generation: identity?.generation,
782
+ incarnation: identity?.incarnation,
783
+ });
784
+ }
785
+ catch {
786
+ /* Diagnostics never own gesture completion. */
787
+ }
788
+ // Retain context while a coalesced final target awaits dispatch.
789
+ if (resizeTransaction.state().phase === "idle")
790
+ resizeCommitContext = null;
791
+ },
792
+ cancelPaneResize() {
793
+ resizeTransaction.cancelDrag();
794
+ pendingResizeGuide = null;
795
+ if (resizeTransaction.state().phase === "idle")
734
796
  resizeCommitContext = null;
735
- }
736
797
  },
737
798
  keyboardResize(axis, direction) {
738
799
  const paneId = options.focusedPane?.() ?? null;
@@ -752,13 +813,15 @@ export function createApplicationTerminalInteractionController(options) {
752
813
  canonicalCells: beforeCells,
753
814
  }))
754
815
  return;
755
- resizeTransaction.move(Math.max(1, beforeCells + direction));
756
816
  resizeCommitContext = { source: "keyboard", pointerIngress: null };
817
+ captureResizePaneIdentity(paneId);
818
+ resizeTransaction.move(Math.max(1, beforeCells + direction));
757
819
  try {
758
820
  resizeTransaction.release();
759
821
  }
760
822
  finally {
761
- resizeCommitContext = null;
823
+ if (resizeTransaction.state().phase === "idle")
824
+ resizeCommitContext = null;
762
825
  }
763
826
  },
764
827
  routeWorkspaceKey(event) {
@@ -1235,12 +1298,14 @@ export function createApplicationTerminalInteractionController(options) {
1235
1298
  pendingResizeGuide = null;
1236
1299
  const settledAtMicros = diagnosticNowMicros();
1237
1300
  let identityExact;
1301
+ let canonicalAfter = null;
1238
1302
  try {
1239
1303
  const active = options.generation();
1240
1304
  const identity = active?.adapter?.paneCanonicalIdentity(settled.preview.semanticPaneId);
1241
1305
  const clientGeneration = active?.client?.getSnapshot().generation;
1242
1306
  const layoutWindow = options.layout().current;
1243
1307
  const layoutPane = layoutWindow?.panes.find(({ pane }) => pane === settled.preview.semanticPaneId);
1308
+ canonicalAfter = identity ?? null;
1244
1309
  identityExact =
1245
1310
  active?.status === "live" &&
1246
1311
  active.daemonGeneration === settled.daemonGeneration &&
@@ -1250,14 +1315,11 @@ export function createApplicationTerminalInteractionController(options) {
1250
1315
  identity?.sourceEpoch === settled.sourceEpoch &&
1251
1316
  identity.generation === settled.generation &&
1252
1317
  identity.incarnation === settled.incarnation &&
1253
- identity.revision === settled.revision &&
1254
- identity.stateHash === settled.stateHash &&
1255
- identity.cols === settled.cols &&
1256
- identity.rows === settled.rows &&
1257
- layoutPane?.width === settled.cols &&
1318
+ identity.revision >= settled.revision &&
1319
+ layoutPane?.width === identity.cols &&
1258
1320
  layoutWindow !== undefined &&
1259
1321
  layoutWindow !== null &&
1260
- nativePaneResizeCells(layoutPane, "rows", layoutWindow.paneBorderStatus, layoutWindow.rows) === settled.rows;
1322
+ nativePaneResizeCells(layoutPane, "rows", layoutWindow.paneBorderStatus, layoutWindow.rows) === identity.rows;
1261
1323
  }
1262
1324
  catch {
1263
1325
  identityExact = false;
@@ -1277,6 +1339,7 @@ export function createApplicationTerminalInteractionController(options) {
1277
1339
  settled.presentationBeforeDigest.length === 64 &&
1278
1340
  presentationDigest !== settled.presentationBeforeDigest,
1279
1341
  identityExact,
1342
+ canonicalAfter,
1280
1343
  durationMicros: settledAtMicros === null ? null : settledAtMicros - settled.startedAtMicros,
1281
1344
  };
1282
1345
  diagnoseCritical(`resize-guide:${settled.traceId}:settled`, "resize-guide-settled", {
@@ -1294,10 +1357,9 @@ export function createApplicationTerminalInteractionController(options) {
1294
1357
  writerHealth,
1295
1358
  });
1296
1359
  }
1297
- const pending = pendingPaneResize;
1360
+ const pending = paneResizeFrame;
1298
1361
  if (!pending?.frameRequested)
1299
1362
  return;
1300
- pendingPaneResize = null;
1301
1363
  let canonicalAfter = null;
1302
1364
  let identityLineageExact = false;
1303
1365
  try {
@@ -1315,14 +1377,23 @@ export function createApplicationTerminalInteractionController(options) {
1315
1377
  identity.sourceEpoch === pending.sourceEpoch &&
1316
1378
  identity.generation === pending.generation &&
1317
1379
  identity.incarnation === pending.incarnation &&
1318
- identity.revision >= pending.revision &&
1319
- layoutCells === pending.layoutCells;
1380
+ identity.revision >= pending.revision;
1381
+ // Layout publication can precede the canonical terminal snapshot. Keep
1382
+ // the bounded diagnostic slot until a coherent frame consumes both.
1383
+ // A newer operation explicitly supersedes it; diagnostics never hold
1384
+ // the resize transaction or request a polling render loop.
1385
+ if (identityLineageExact &&
1386
+ (identity.cols !== paneCells(options.layout(), pending.semanticPaneId, "cols") ||
1387
+ identity.rows !== paneCells(options.layout(), pending.semanticPaneId, "rows") ||
1388
+ layoutCells !== pending.layoutCells))
1389
+ return;
1320
1390
  }
1321
1391
  }
1322
1392
  catch {
1323
1393
  canonicalAfter = null;
1324
1394
  identityLineageExact = false;
1325
1395
  }
1396
+ paneResizeFrame = null;
1326
1397
  const presentationDigest = resizePresentationDigest(null);
1327
1398
  const details = {
1328
1399
  ...resizeDiagnosticDetails(pending),
@@ -1,3 +1,4 @@
1
+ import { parseTerminalHostColor, terminalHostMode } from "../../../lib/terminal-host-color.js";
1
2
  const PALETTE_SIZE = 16;
2
3
  const QUERY_TIMEOUT_MS = 1_000;
3
4
  const FOLLOW_UP_DELAYS_MS = [250, 1_000];
@@ -6,7 +7,7 @@ function normalizeColor(value) {
6
7
  if (typeof value !== "string")
7
8
  return null;
8
9
  const normalized = value.trim().toLowerCase();
9
- return normalized.length > 0 ? normalized : null;
10
+ return parseTerminalHostColor(normalized) ? normalized : null;
10
11
  }
11
12
  function normalizePalette(values) {
12
13
  const palette = Array(PALETTE_SIZE).fill(null);
@@ -31,19 +32,6 @@ function immutableCapabilities(value) {
31
32
  return null;
32
33
  return immutableValue(value);
33
34
  }
34
- function detectedMode(background, fallback) {
35
- if (!background)
36
- return fallback;
37
- const match = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(background);
38
- if (!match)
39
- return fallback;
40
- const hex = match[1];
41
- const expanded = hex.length === 3 ? `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}` : hex;
42
- const red = Number.parseInt(expanded.slice(0, 2), 16) / 255;
43
- const green = Number.parseInt(expanded.slice(2, 4), 16) / 255;
44
- const blue = Number.parseInt(expanded.slice(4, 6), 16) / 255;
45
- return 0.299 * red + 0.587 * green + 0.114 * blue > 0.5 ? "light" : "dark";
46
- }
47
35
  function snapshotSignature(input) {
48
36
  return JSON.stringify(input);
49
37
  }
@@ -81,7 +69,7 @@ function availableSnapshot(result, fallbackMode, capabilities) {
81
69
  tekBackground: normalizeColor(result.tekBackground),
82
70
  highlightBackground: normalizeColor(result.highlightBackground),
83
71
  highlightForeground: normalizeColor(result.highlightForeground),
84
- detectedMode: detectedMode(defaultBackground, fallbackMode),
72
+ detectedMode: terminalHostMode(defaultBackground) ?? fallbackMode,
85
73
  capabilities,
86
74
  });
87
75
  }
@@ -103,6 +91,9 @@ export function createApplicationTerminalPaletteOwner(renderer, options = {}) {
103
91
  let lastValid = null;
104
92
  let active = null;
105
93
  let queued = false;
94
+ let modeHint = null;
95
+ let modeGeneration = 0;
96
+ let awaitingModePalette = false;
106
97
  let disposed = false;
107
98
  const publish = (next) => {
108
99
  if (disposed || next.signature === snapshot.signature)
@@ -119,24 +110,31 @@ export function createApplicationTerminalPaletteOwner(renderer, options = {}) {
119
110
  return active;
120
111
  }
121
112
  renderer.clearPaletteCache();
113
+ const requestedModeGeneration = modeGeneration;
114
+ const guardModeForQuery = awaitingModePalette;
122
115
  const generation = Promise.resolve()
123
116
  .then(() => renderer.getPalette({ size: PALETTE_SIZE, timeout: queryTimeoutMs }))
124
117
  .then((result) => {
125
- if (disposed)
118
+ if (disposed || requestedModeGeneration !== modeGeneration)
119
+ return;
120
+ const reportedMode = terminalHostMode(result.defaultBackground);
121
+ // A terminal can still answer with its previous palette while switching.
122
+ // Queries issued during the transition wait for a matching reply.
123
+ if (guardModeForQuery && modeHint && reportedMode && reportedMode !== modeHint)
126
124
  return;
127
125
  if (!hasReportedColor(result)) {
128
126
  if (!lastValid)
129
- publish(fallbackSnapshot("unavailable", renderer.themeMode ?? "dark", capabilities));
127
+ publish(fallbackSnapshot("unavailable", modeHint ?? renderer.themeMode ?? "dark", capabilities));
130
128
  return;
131
129
  }
132
- const next = availableSnapshot(result, renderer.themeMode ?? "dark", capabilities);
130
+ const next = availableSnapshot(result, modeHint ?? renderer.themeMode ?? "dark", capabilities);
133
131
  lastValid = next;
134
132
  publish(next);
135
133
  })
136
134
  .catch(() => {
137
- if (disposed || lastValid)
135
+ if (disposed || requestedModeGeneration !== modeGeneration || lastValid)
138
136
  return;
139
- publish(fallbackSnapshot("unavailable", renderer.themeMode ?? "dark", capabilities));
137
+ publish(fallbackSnapshot("unavailable", modeHint ?? renderer.themeMode ?? "dark", capabilities));
140
138
  })
141
139
  .finally(() => {
142
140
  if (active !== generation)
@@ -152,20 +150,39 @@ export function createApplicationTerminalPaletteOwner(renderer, options = {}) {
152
150
  };
153
151
  const scheduleFollowUps = () => {
154
152
  if (disposed)
155
- return;
156
- void refresh();
153
+ return Promise.resolve();
154
+ const first = refresh();
155
+ // A burst owns only one bounded pair of retries, never one pair per event.
156
+ for (const timer of timers)
157
+ cancelTimeout(timer);
158
+ timers.clear();
157
159
  for (const delay of FOLLOW_UP_DELAYS_MS) {
158
160
  const timer = scheduleTimeout(() => {
159
161
  timers.delete(timer);
162
+ // After the bounded transition window, a fresh measured OSC 11 color
163
+ // wins even when a custom theme's luminance disagrees with its hint.
164
+ if (delay === FOLLOW_UP_DELAYS_MS[FOLLOW_UP_DELAYS_MS.length - 1])
165
+ awaitingModePalette = false;
160
166
  if (!disposed)
161
167
  void refresh();
162
168
  }, delay);
163
169
  timers.add(timer);
164
170
  }
171
+ return first;
165
172
  };
166
- const onThemeMode = () => {
173
+ const onThemeMode = (mode) => {
174
+ const changed = modeHint !== mode;
175
+ if (changed)
176
+ modeGeneration += 1;
177
+ modeHint = mode;
178
+ if (changed && snapshot.detectedMode !== mode) {
179
+ awaitingModePalette = true;
180
+ // Track hints even while locked, so unlocking cannot resurrect old colors.
181
+ lastValid = null;
182
+ publish(fallbackSnapshot("unavailable", mode, capabilities));
183
+ }
167
184
  if (isThemeModeUnlocked())
168
- scheduleFollowUps();
185
+ void scheduleFollowUps();
169
186
  };
170
187
  const onCapabilities = (next) => {
171
188
  capabilities = immutableCapabilities(next);
@@ -176,13 +193,12 @@ export function createApplicationTerminalPaletteOwner(renderer, options = {}) {
176
193
  publish(lastValid);
177
194
  return;
178
195
  }
179
- publish(fallbackSnapshot(snapshot.availability === "pending" ? "pending" : "unavailable", renderer.themeMode ?? "dark", capabilities));
196
+ publish(fallbackSnapshot(snapshot.availability === "pending" ? "pending" : "unavailable", modeHint ?? renderer.themeMode ?? "dark", capabilities));
180
197
  };
181
198
  const onThemeNotification = (sequence) => {
182
199
  if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n")
183
200
  return false;
184
- if (isThemeModeUnlocked())
185
- scheduleFollowUps();
201
+ onThemeMode(sequence === "\x1b[?997;1n" ? "dark" : "light");
186
202
  return false;
187
203
  };
188
204
  renderer.on("theme_mode", onThemeMode);
@@ -198,7 +214,7 @@ export function createApplicationTerminalPaletteOwner(renderer, options = {}) {
198
214
  listeners.add(listener);
199
215
  return () => listeners.delete(listener);
200
216
  },
201
- refresh,
217
+ refresh: scheduleFollowUps,
202
218
  dispose() {
203
219
  if (disposed)
204
220
  return;
@@ -1,6 +1,6 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
2
  import { clampTerminalViewportOrigin, terminalLiveViewportOrigin, reflowTerminalPosition, } from "../terminal-viewport.js";
3
- import { For, Show, createMemo, createRenderEffect, createSignal, onCleanup, untrack, } from "solid-js";
3
+ import { For, Show, createMemo, createEffect, createRenderEffect, createSignal, onCleanup, untrack, } from "solid-js";
4
4
  import { PaneScopedTerminalSurface } from "./pane-scoped-terminal-surface.jsx";
5
5
  import { nativePaneGeometries, projectOpenTuiPaneFrames, } from "./terminal-layout-projection.js";
6
6
  import { TerminalWindowStrip, } from "../workspace/terminal-window-strip.jsx";
@@ -228,6 +228,28 @@ export function ApplicationTerminalWorkspace(props) {
228
228
  };
229
229
  let forwardedPointer = null;
230
230
  let drag = null;
231
+ const cancelResize = () => {
232
+ if (!drag)
233
+ return;
234
+ drag = null;
235
+ setResizePreview(null);
236
+ setHoveredSeparator(null);
237
+ props.onCancelResize?.();
238
+ };
239
+ createEffect(() => {
240
+ const current = layout().current;
241
+ const epoch = props.rendererEpoch;
242
+ const adapter = props.adapter;
243
+ const interactive = props.interactive;
244
+ if (drag &&
245
+ (interactive === false ||
246
+ epoch !== drag.rendererEpoch ||
247
+ adapter !== drag.adapter ||
248
+ (current?.semanticWindowId ?? current?.windowName) !== drag.windowId ||
249
+ !current?.panes.some((pane) => pane.pane === drag?.preview.semanticPaneId)))
250
+ cancelResize();
251
+ });
252
+ onCleanup(cancelResize);
231
253
  const terminalPoint = (event) => ({
232
254
  x: event.x - (props.originX ?? 0),
233
255
  y: event.y - (props.originY ?? 0) - topOffset(),
@@ -688,7 +710,14 @@ export function ApplicationTerminalWorkspace(props) {
688
710
  });
689
711
  return true;
690
712
  };
691
- props.onSelectionKeyOwner?.(handlePaneMenuKey, () => props.interactive !== false && (paneMenu.ownsInput() || keyboardCopy() !== null), () => {
713
+ props.onSelectionKeyOwner?.((name, event) => {
714
+ if (drag && name === "escape") {
715
+ cancelResize();
716
+ return true;
717
+ }
718
+ return handlePaneMenuKey(name, event);
719
+ }, () => props.interactive !== false &&
720
+ (drag !== null || paneMenu.ownsInput() || keyboardCopy() !== null), () => {
692
721
  // Called only after global shortcuts, copy, and local navigation decline
693
722
  // the event, immediately before terminal key or paste delivery.
694
723
  const paneId = retainedSelectionPane() ?? props.focusedPane;
@@ -702,6 +731,8 @@ export function ApplicationTerminalWorkspace(props) {
702
731
  props.onSelectionCopyOwner?.(null);
703
732
  props.onSelectionKeyOwner?.(null);
704
733
  });
734
+ // Register this router only as onMouse. OpenTUI invokes both the general
735
+ // listener and the typed listener on a target, even after stopPropagation.
705
736
  const routePointer = (event) => {
706
737
  if (event.type === "down")
707
738
  wheelGesture.reset();
@@ -775,15 +806,13 @@ export function ApplicationTerminalWorkspace(props) {
775
806
  }
776
807
  if (isRelease) {
777
808
  const completed = drag.preview;
778
- const changed = completed.cells !== drag.separator.initialCells;
779
809
  drag = null;
780
810
  setResizePreview(null);
781
811
  setHoveredSeparator(null);
782
- if (changed)
783
- props.onResizePane?.(globalPreview(Object.freeze({
784
- ...completed,
785
- ...(ingress ? { pointerIngress: ingress } : {}),
786
- })));
812
+ props.onResizePane?.(globalPreview(Object.freeze({
813
+ ...completed,
814
+ ...(ingress ? { pointerIngress: ingress } : {}),
815
+ })));
787
816
  }
788
817
  }
789
818
  return;
@@ -918,7 +947,16 @@ export function ApplicationTerminalWorkspace(props) {
918
947
  const ingress = resizeIngress();
919
948
  const origin = separator.axis === "x" ? point.x : point.y;
920
949
  const preview = terminalPaneResizePreview(separator, origin, origin);
921
- drag = { separator, origin, preview, gestureId: ingress?.gestureId ?? null };
950
+ drag = {
951
+ separator,
952
+ origin,
953
+ preview,
954
+ gestureId: ingress?.gestureId ?? null,
955
+ rendererEpoch: props.rendererEpoch,
956
+ adapter: props.adapter,
957
+ windowId: layout().current?.semanticWindowId ?? layout().current?.windowName,
958
+ };
959
+ props.onResizePreview?.(globalPreview(preview));
922
960
  setHoveredSeparator(null);
923
961
  setResizePreview(preview);
924
962
  return;
@@ -1136,7 +1174,7 @@ export function ApplicationTerminalWorkspace(props) {
1136
1174
  return Object.freeze(cells);
1137
1175
  });
1138
1176
  return (<>
1139
- <box position="absolute" left={0} top={topOffset()} width={props.width} height={props.height} onMouse={routePointer} onMouseDown={routePointer} onMouseUp={routePointer}/>
1177
+ <box position="absolute" left={0} top={topOffset()} width={props.width} height={props.height} onMouse={routePointer}/>
1140
1178
  <box position="absolute" left={0} top={topOffset() - 1} width={props.width} height={1} backgroundColor={props.theme.roles.surfaces.panel} flexDirection="row" onMouse={routePointer}>
1141
1179
  <Show when={layout().windows.length > 0} fallback={<text fg={props.theme.roles.text.muted}> no terminal windows </text>}>
1142
1180
  <TerminalWindowStrip theme={props.theme} width={props.width} tabs={terminalWindowTabs} hoveredIndex={null} onActivate={(index) => {
@@ -1200,7 +1238,7 @@ export function ApplicationTerminalWorkspace(props) {
1200
1238
  }}
1201
1239
  </For>
1202
1240
  <For each={terminalPaneSeparators(visibleFrames(), layout().current?.paneBorderStatus ?? "off")}>
1203
- {(separator) => (<box position="absolute" left={separator.axis === "x" ? separator.position : separator.start} top={(separator.axis === "x" ? separator.start : separator.position) + topOffset()} width={1} height={separator.axis === "x" ? Math.max(1, separator.end - separator.start) : 1} backgroundColor={props.theme.colors.accentMuted} onMouse={routePointer} onMouseDown={routePointer} onMouseUp={routePointer}>
1241
+ {(separator) => (<box position="absolute" left={separator.axis === "x" ? separator.position : separator.start} top={(separator.axis === "x" ? separator.start : separator.position) + topOffset()} width={1} height={separator.axis === "x" ? Math.max(1, separator.end - separator.start) : 1} backgroundColor={props.theme.colors.accentMuted} onMouse={routePointer}>
1204
1242
  <Show when={separator.axis === "y"}>
1205
1243
  <text fg={props.theme.roles.text.primary}>↕</text>
1206
1244
  </Show>