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
@@ -9,6 +9,7 @@ import {
9
9
  For,
10
10
  Show,
11
11
  createMemo,
12
+ createEffect,
12
13
  createRenderEffect,
13
14
  createSignal,
14
15
  onCleanup,
@@ -175,6 +176,7 @@ export interface ApplicationTerminalWorkspaceProps {
175
176
  ) => void;
176
177
  readonly onResizePreview?: (preview: ApplicationPaneResizePreview) => void;
177
178
  readonly onResizePane?: (preview: ApplicationPaneResizePreview) => void;
179
+ readonly onCancelResize?: () => void;
178
180
  readonly onResizePointerIngress?: (input: {
179
181
  readonly action: "down" | "drag" | "up";
180
182
  readonly x: number;
@@ -510,11 +512,38 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
510
512
  } | null = null;
511
513
  let drag: {
512
514
  readonly separator: ApplicationPaneSeparator;
515
+ readonly rendererEpoch: number;
516
+ readonly adapter: ApplicationTerminalWorkspaceProps["adapter"];
517
+ readonly windowId: string | null | undefined;
513
518
  readonly origin: number;
514
519
  preview: ApplicationPaneResizePreview;
515
520
  readonly gestureId: string | null;
516
521
  } | null = null;
517
522
 
523
+ const cancelResize = () => {
524
+ if (!drag) return;
525
+ drag = null;
526
+ setResizePreview(null);
527
+ setHoveredSeparator(null);
528
+ props.onCancelResize?.();
529
+ };
530
+ createEffect(() => {
531
+ const current = layout().current;
532
+ const epoch = props.rendererEpoch;
533
+ const adapter = props.adapter;
534
+ const interactive = props.interactive;
535
+ if (
536
+ drag &&
537
+ (interactive === false ||
538
+ epoch !== drag.rendererEpoch ||
539
+ adapter !== drag.adapter ||
540
+ (current?.semanticWindowId ?? current?.windowName) !== drag.windowId ||
541
+ !current?.panes.some((pane) => pane.pane === drag?.preview.semanticPaneId))
542
+ )
543
+ cancelResize();
544
+ });
545
+ onCleanup(cancelResize);
546
+
518
547
  const terminalPoint = (event: WorkspaceMouseEvent): { x: number; y: number } => ({
519
548
  x: event.x - (props.originX ?? 0),
520
549
  y: event.y - (props.originY ?? 0) - topOffset(),
@@ -1059,8 +1088,16 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1059
1088
  return true;
1060
1089
  };
1061
1090
  props.onSelectionKeyOwner?.(
1062
- handlePaneMenuKey,
1063
- () => props.interactive !== false && (paneMenu.ownsInput() || keyboardCopy() !== null),
1091
+ (name, event) => {
1092
+ if (drag && name === "escape") {
1093
+ cancelResize();
1094
+ return true;
1095
+ }
1096
+ return handlePaneMenuKey(name, event);
1097
+ },
1098
+ () =>
1099
+ props.interactive !== false &&
1100
+ (drag !== null || paneMenu.ownsInput() || keyboardCopy() !== null),
1064
1101
  () => {
1065
1102
  // Called only after global shortcuts, copy, and local navigation decline
1066
1103
  // the event, immediately before terminal key or paste delivery.
@@ -1075,6 +1112,8 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1075
1112
  props.onSelectionCopyOwner?.(null);
1076
1113
  props.onSelectionKeyOwner?.(null);
1077
1114
  });
1115
+ // Register this router only as onMouse. OpenTUI invokes both the general
1116
+ // listener and the typed listener on a target, even after stopPropagation.
1078
1117
  const routePointer = (event: WorkspaceMouseEvent): void => {
1079
1118
  if (event.type === "down") wheelGesture.reset();
1080
1119
  if (paneMenu.ownsInput()) {
@@ -1153,19 +1192,17 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1153
1192
  }
1154
1193
  if (isRelease) {
1155
1194
  const completed = drag.preview;
1156
- const changed = completed.cells !== drag.separator.initialCells;
1157
1195
  drag = null;
1158
1196
  setResizePreview(null);
1159
1197
  setHoveredSeparator(null);
1160
- if (changed)
1161
- props.onResizePane?.(
1162
- globalPreview(
1163
- Object.freeze({
1164
- ...completed,
1165
- ...(ingress ? { pointerIngress: ingress } : {}),
1166
- }),
1167
- ),
1168
- );
1198
+ props.onResizePane?.(
1199
+ globalPreview(
1200
+ Object.freeze({
1201
+ ...completed,
1202
+ ...(ingress ? { pointerIngress: ingress } : {}),
1203
+ }),
1204
+ ),
1205
+ );
1169
1206
  }
1170
1207
  }
1171
1208
  return;
@@ -1334,7 +1371,16 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1334
1371
  const ingress = resizeIngress();
1335
1372
  const origin = separator.axis === "x" ? point.x : point.y;
1336
1373
  const preview = terminalPaneResizePreview(separator, origin, origin);
1337
- drag = { separator, origin, preview, gestureId: ingress?.gestureId ?? null };
1374
+ drag = {
1375
+ separator,
1376
+ origin,
1377
+ preview,
1378
+ gestureId: ingress?.gestureId ?? null,
1379
+ rendererEpoch: props.rendererEpoch,
1380
+ adapter: props.adapter,
1381
+ windowId: layout().current?.semanticWindowId ?? layout().current?.windowName,
1382
+ };
1383
+ props.onResizePreview?.(globalPreview(preview));
1338
1384
  setHoveredSeparator(null);
1339
1385
  setResizePreview(preview);
1340
1386
  return;
@@ -1582,8 +1628,6 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1582
1628
  width={props.width}
1583
1629
  height={props.height}
1584
1630
  onMouse={routePointer}
1585
- onMouseDown={routePointer}
1586
- onMouseUp={routePointer}
1587
1631
  />
1588
1632
  <box
1589
1633
  position="absolute"
@@ -1754,8 +1798,6 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
1754
1798
  height={separator.axis === "x" ? Math.max(1, separator.end - separator.start) : 1}
1755
1799
  backgroundColor={props.theme.colors.accentMuted}
1756
1800
  onMouse={routePointer}
1757
- onMouseDown={routePointer}
1758
- onMouseUp={routePointer}
1759
1801
  >
1760
1802
  <Show when={separator.axis === "y"}>
1761
1803
  <text fg={props.theme.roles.text.primary}>↕</text>
@@ -3,6 +3,8 @@ import type { ApplicationShellProjectionV1 } from "@tmux-ide/contracts";
3
3
  import type { OpenTuiGenerationHostSnapshot } from "./open-tui-generation-host.ts";
4
4
  import { applicationShellViewport } from "./application-shell-viewport.ts";
5
5
 
6
+ import type { OpenTuiWorkspaceLayout } from "../open-tui-workspace-runtime-port.ts";
7
+
6
8
  type Dimensions = Readonly<{ width: number; height: number }>;
7
9
 
8
10
  /**
@@ -11,7 +13,11 @@ type Dimensions = Readonly<{ width: number; height: number }>;
11
13
  */
12
14
  export function createSemanticShellViewportResizeOwner(
13
15
  getLayout: () => {
14
- readonly current: { readonly paneBorderStatus: "off" | "top" | "bottom" } | null;
16
+ readonly windows?: readonly Pick<
17
+ OpenTuiWorkspaceLayout,
18
+ "semanticWindowId" | "paneBorderStatus"
19
+ >[];
20
+ readonly current: Pick<OpenTuiWorkspaceLayout, "paneBorderStatus"> | null;
15
21
  } = () => ({ current: { paneBorderStatus: "top" } }),
16
22
  ): Readonly<{
17
23
  adopt(
@@ -32,6 +38,60 @@ export function createSemanticShellViewportResizeOwner(
32
38
  }>;
33
39
  let applied: ResizeIdentity | null = null;
34
40
  let pending: ResizeIdentity | null = null;
41
+ type ScopedTarget = Readonly<{ cols: number; rows: number; semanticWindowId?: string }>;
42
+ let desired: Readonly<{ identity: ResizeIdentity; targets: readonly ScopedTarget[] }> | null =
43
+ null;
44
+ let scopedFlight = false;
45
+ let scopedEpoch = 0;
46
+ let authorityClient: OpenTuiGenerationHostSnapshot["authorityClient"] = null;
47
+ let stopAuthority: (() => void) | null = null;
48
+ let geometryOwner: string | null = null;
49
+ let authoritySuspended = false;
50
+ let retry: (() => void) | null = null;
51
+ const scopedApplied = new Map<string, string>();
52
+ const targetKey = (target: ScopedTarget) => target.semanticWindowId ?? "";
53
+ const targetSize = (target: ScopedTarget) => `${target.cols}x${target.rows}`;
54
+ const drain = async (): Promise<void> => {
55
+ if (scopedFlight || disposed || authoritySuspended) return;
56
+ scopedFlight = true;
57
+ try {
58
+ while (desired && !disposed && !authoritySuspended) {
59
+ const owner = desired;
60
+ const epoch = scopedEpoch;
61
+ const target = owner.targets.find(
62
+ (entry) => scopedApplied.get(targetKey(entry)) !== targetSize(entry),
63
+ );
64
+ if (!target) break;
65
+ // A global fit clears this control client's window overrides. Forget
66
+ // them before dispatch, including a reversal while the receipt waits.
67
+ if (target.semanticWindowId === undefined) scopedApplied.clear();
68
+ else scopedApplied.delete(targetKey(target));
69
+ const outcome = await owner.identity.lane.lane
70
+ .resize(target)
71
+ .catch(() => ({ status: "failed" as const }));
72
+ if (disposed || !desired) break;
73
+ if (epoch !== scopedEpoch) continue;
74
+ const stillDesired = desired.targets.some(
75
+ (entry) =>
76
+ targetKey(entry) === targetKey(target) && targetSize(entry) === targetSize(target),
77
+ );
78
+ if (outcome.status !== "applied") {
79
+ if (!stillDesired) continue;
80
+ break;
81
+ }
82
+ // Preserve the actual accepted size even after a reversal so the next
83
+ // pass compares current truth with the latest complete desired set.
84
+ if (desired.targets.some((entry) => targetKey(entry) === targetKey(target)))
85
+ scopedApplied.set(targetKey(target), targetSize(target));
86
+ }
87
+ } finally {
88
+ scopedFlight = false;
89
+ }
90
+ };
91
+ const sameAuthority = (left: ResizeIdentity, right: ResizeIdentity): boolean =>
92
+ left.lane === right.lane &&
93
+ left.daemonGeneration === right.daemonGeneration &&
94
+ left.rendererEpoch === right.rendererEpoch;
35
95
  const same = (left: ResizeIdentity | null, right: ResizeIdentity): boolean =>
36
96
  left?.lane === right.lane &&
37
97
  left.daemonGeneration === right.daemonGeneration &&
@@ -40,23 +100,59 @@ export function createSemanticShellViewportResizeOwner(
40
100
  left.rows === right.rows;
41
101
 
42
102
  return Object.freeze({
43
- adopt(
103
+ adopt: function adopt(
44
104
  dimensions,
45
105
  semantic,
46
106
  generation,
47
107
  paneBorderStatus = getLayout().current?.paneBorderStatus ?? "off",
48
108
  ) {
49
109
  if (disposed) return;
110
+ retry = () => adopt(dimensions, semantic, generation, paneBorderStatus);
50
111
  if (
51
112
  semantic === null ||
52
113
  generation?.status !== "live" ||
53
114
  generation.daemonGeneration === null ||
54
115
  generation.fastLane === null
55
116
  ) {
117
+ stopAuthority?.();
118
+ stopAuthority = null;
119
+ authorityClient = null;
120
+ authoritySuspended = false;
56
121
  applied = null;
57
122
  pending = null;
123
+ desired = null;
124
+ scopedEpoch += 1;
125
+ scopedApplied.clear();
58
126
  return;
59
127
  }
128
+ if (authorityClient !== generation.authorityClient) {
129
+ stopAuthority?.();
130
+ authorityClient = generation.authorityClient;
131
+ const observed = authorityClient;
132
+ geometryOwner = observed?.getAuthoritySnapshot()?.owners.geometry ?? null;
133
+ authoritySuspended =
134
+ geometryOwner !== null && geometryOwner !== observed?.authorityIdentity.clientId;
135
+ stopAuthority =
136
+ observed?.onAuthority((snapshot) => {
137
+ if (
138
+ disposed ||
139
+ authorityClient !== observed ||
140
+ snapshot.generation !== generation.daemonGeneration
141
+ )
142
+ return;
143
+ const next = snapshot.owners.geometry;
144
+ if (next === geometryOwner) return;
145
+ geometryOwner = next;
146
+ authoritySuspended = next !== null && next !== observed.authorityIdentity.clientId;
147
+ // The daemon clears scoped fits on handoff even when the host/runtime
148
+ // identity survives. Reacquisition must rebuild them from current truth.
149
+ applied = null;
150
+ pending = null;
151
+ scopedApplied.clear();
152
+ scopedEpoch += 1;
153
+ if (!authoritySuspended) retry?.();
154
+ }) ?? null;
155
+ }
60
156
  const viewport = applicationShellViewport(dimensions, true);
61
157
  const lane = generation.fastLane;
62
158
  const target = Object.freeze({
@@ -66,7 +162,53 @@ export function createSemanticShellViewportResizeOwner(
66
162
  cols: viewport.width,
67
163
  rows: Math.max(2, viewport.height - (paneBorderStatus === "off" ? 1 : 0)),
68
164
  });
69
- if (same(applied, target) || same(pending, target)) return;
165
+ const windows = getLayout().windows;
166
+ if (
167
+ windows &&
168
+ windows.every(
169
+ (window) =>
170
+ typeof window.semanticWindowId === "string" && window.semanticWindowId.length > 0,
171
+ )
172
+ ) {
173
+ applied = null;
174
+ pending = null;
175
+ if (!desired || !sameAuthority(desired.identity, target)) {
176
+ scopedEpoch += 1;
177
+ scopedApplied.clear();
178
+ }
179
+ const retainedWindows = new Set(["", ...windows.map((window) => window.semanticWindowId!)]);
180
+ for (const key of scopedApplied.keys())
181
+ if (!retainedWindows.has(key)) scopedApplied.delete(key);
182
+ desired = {
183
+ identity: target,
184
+ targets: [
185
+ // Unscoped/new windows inherit a stable size, independent of the
186
+ // selected window's border policy. Known windows then fit their own
187
+ // header reservation without resizing their neighbours on switch.
188
+ { cols: viewport.width, rows: Math.max(2, viewport.height - 1) },
189
+ ...windows.map((window) => ({
190
+ semanticWindowId: window.semanticWindowId!,
191
+ cols: viewport.width,
192
+ rows: Math.max(2, viewport.height - (window.paneBorderStatus === "off" ? 1 : 0)),
193
+ })),
194
+ ],
195
+ };
196
+ void drain();
197
+ return;
198
+ }
199
+ if (desired) {
200
+ desired = null;
201
+ scopedEpoch += 1;
202
+ scopedApplied.clear();
203
+ applied = null;
204
+ pending = null;
205
+ }
206
+ if (authoritySuspended || same(applied, target) || same(pending, target)) return;
207
+ // The accepted size stops being a dedupe target as soon as another
208
+ // resize can mutate tmux. Otherwise A -> B -> A drops the final A while
209
+ // B is still awaiting its receipt. The fast lane owns first/latest
210
+ // coalescing; it must see the reversal to retain the actual final size.
211
+ applied = null;
70
212
  pending = target;
71
213
  void lane.lane.resize({ cols: target.cols, rows: target.rows }).then(
72
214
  (outcome) => {
@@ -81,6 +223,12 @@ export function createSemanticShellViewportResizeOwner(
81
223
  },
82
224
  dispose() {
83
225
  disposed = true;
226
+ retry = null;
227
+ stopAuthority?.();
228
+ stopAuthority = null;
229
+ authorityClient = null;
230
+ desired = null;
231
+ scopedApplied.clear();
84
232
  applied = null;
85
233
  pending = null;
86
234
  },
@@ -103,7 +103,9 @@ export function createOpenTuiWorkspaceTerminalFastLane(
103
103
  if (client.getSnapshot().target?.daemon.instanceId !== address.generation) {
104
104
  return Promise.resolve("authority-lost");
105
105
  }
106
- return client.fitViewport(viewport.cols, viewport.rows);
106
+ return viewport.semanticWindowId === undefined
107
+ ? client.fitViewport(viewport.cols, viewport.rows)
108
+ : client.fitViewport(viewport.cols, viewport.rows, viewport.semanticWindowId);
107
109
  },
108
110
  },
109
111
  ...(performanceSink?.terminalTraceStage
@@ -758,7 +758,8 @@ export function blitSemanticRow(
758
758
  let background = resolveColor(cell.background, false, palette);
759
759
  let attributes = cell.attributes;
760
760
  if ((attributes & 32) !== 0) {
761
- [foreground, background] = [background, foreground];
761
+ // Resolve each side before reversing: a default retains its original role.
762
+ [foreground, background] = [background ?? defaultBg, foreground ?? defaultFg];
762
763
  attributes &= ~32;
763
764
  }
764
765
  // Native tmux clears a partial wide glyph, retaining its background.
@@ -37,6 +37,7 @@ import {
37
37
  type LegacyThemeOverrideProvenance,
38
38
  } from "../../lib/legacy-theme-compat.ts";
39
39
  import type { ResolvedThemeMode, ThemeModeSetting } from "../../lib/theme-mode.ts";
40
+ import { parseTerminalHostColor, terminalHostMode } from "../../lib/terminal-host-color.ts";
40
41
  import { XTERM_PALETTE } from "./ansi-palette.ts";
41
42
 
42
43
  /** Stable categorical host accents; palette authority stays with the theme. */
@@ -379,35 +380,6 @@ function rendererNeutralFromPacked(color: number): RendererNeutralColor {
379
380
  return rendererNeutral((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff);
380
381
  }
381
382
 
382
- function perceivedLuminance(color: RendererNeutralColor): number {
383
- return 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue;
384
- }
385
-
386
- function parseTerminalHostColor(value: string | null | undefined): RendererNeutralColor | null {
387
- if (!value) return null;
388
- const normalized = value.trim().toLowerCase();
389
- const hex = /^#([\da-f]{3}|[\da-f]{6})$/u.exec(normalized)?.[1];
390
- if (hex) {
391
- const expanded =
392
- hex.length === 3 ? `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}` : hex;
393
- return rendererNeutral(
394
- Number.parseInt(expanded.slice(0, 2), 16),
395
- Number.parseInt(expanded.slice(2, 4), 16),
396
- Number.parseInt(expanded.slice(4, 6), 16),
397
- );
398
- }
399
- // OSC palette replies may use X11's rgb:RR/GG/BB form with one to four
400
- // hexadecimal digits per channel. Scale each channel to a byte rather than
401
- // truncating high-fidelity replies.
402
- const x11 = /^rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})$/u.exec(normalized);
403
- if (!x11) return null;
404
- const channel = (part: string): number => {
405
- const maximum = 16 ** part.length - 1;
406
- return Math.round((Number.parseInt(part, 16) / maximum) * 255);
407
- };
408
- return rendererNeutral(channel(x11[1]!), channel(x11[2]!), channel(x11[3]!));
409
- }
410
-
411
383
  function mostReadable(
412
384
  background: RendererNeutralColor,
413
385
  candidates: readonly RendererNeutralColor[],
@@ -458,10 +430,8 @@ export function deriveSystemVisualHostDefaults(
458
430
  const reportedPalette = Array.from({ length: 16 }, (_, index) =>
459
431
  parseTerminalHostColor(input.palette[index]),
460
432
  );
461
- const measuredBackground =
462
- parseTerminalHostColor(input.defaultBackground) ?? reportedPalette[0] ?? null;
463
- const measuredForeground =
464
- parseTerminalHostColor(input.defaultForeground) ?? reportedPalette[7] ?? null;
433
+ const measuredBackground = parseTerminalHostColor(input.defaultBackground);
434
+ const measuredForeground = parseTerminalHostColor(input.defaultForeground);
465
435
  if (!measuredBackground && !measuredForeground && reportedPalette.every((value) => !value))
466
436
  return null;
467
437
 
@@ -469,12 +439,7 @@ export function deriveSystemVisualHostDefaults(
469
439
  const background = measuredBackground ?? BUILTIN_VISUAL_THEMES[fallbackMode].surfaces.canvas;
470
440
  const black = rendererNeutral(0, 0, 0);
471
441
  const white = rendererNeutral(255, 255, 255);
472
- const appearance =
473
- measuredBackground === null
474
- ? fallbackMode
475
- : perceivedLuminance(background) > 127.5
476
- ? "light"
477
- : "dark";
442
+ const appearance = terminalHostMode(input.defaultBackground) ?? fallbackMode;
478
443
  const builtIn = BUILTIN_VISUAL_THEMES[appearance];
479
444
  const structuralTarget = appearance === "dark" ? white : black;
480
445
  const panel = mixSrgbColors(background, structuralTarget, appearance === "dark" ? 0.055 : 0.04);
@@ -2,7 +2,7 @@
2
2
  import { paneInteractionPresence, type PaneInteractionProjection } from "@tmux-ide/core";
3
3
  import { Badge } from "../ui/badge.tsx";
4
4
  import type { AgentActivity } from "@tmux-ide/contracts";
5
- import { createSignal, Show } from "solid-js";
5
+ import { createMemo, createSignal, Show } from "solid-js";
6
6
 
7
7
  import type { SemanticThemeSnapshot } from "../theme.ts";
8
8
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.ts";
@@ -104,13 +104,6 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
104
104
  if (!presence() || available < presence()!.badge.length + 2) return 0;
105
105
  return Math.min(available, terminalDisplayWidth(activityLabel()) + 2);
106
106
  };
107
- // Capture one non-null presence for each badge lifetime. A retiring child's
108
- // queued style effect must not dereference the parent's now-expired receipt.
109
- const activityBadge = () => {
110
- const current = presence();
111
- const width = activityWidth();
112
- return current && width > 0 ? { presence: current, width, label: activityLabel() } : null;
113
- };
114
107
  // Keep the state glyph out of the first two inline cells. OpenTUI can repaint
115
108
  // those cells from the clipped parent during nested workspace composition.
116
109
  const markerGutterWidth = () => Math.min(2, safeWidth());
@@ -187,6 +180,26 @@ export function PaneTitleBar(props: PaneTitleBarProps) {
187
180
  else props.onSelectIntent();
188
181
  };
189
182
 
183
+ // Capture one non-null presence for each badge lifetime. A retiring child's
184
+ // queued style effect must not dereference the parent's now-expired receipt.
185
+ const activityBadge = createMemo(
186
+ () => {
187
+ const current = presence();
188
+ const width = activityWidth();
189
+ return current && width > 0 ? { presence: current, width, label: activityLabel() } : null;
190
+ },
191
+ undefined,
192
+ {
193
+ equals: (previous, next) =>
194
+ previous === next ||
195
+ (previous !== null &&
196
+ next !== null &&
197
+ previous.width === next.width &&
198
+ previous.label === next.label &&
199
+ previous.presence.tone === next.presence.tone),
200
+ },
201
+ );
202
+
190
203
  return (
191
204
  <box
192
205
  id={`pane-title-bar:${props.paneId}`}
@@ -648,6 +648,24 @@ describe("terminal fast lane", () => {
648
648
  });
649
649
  });
650
650
 
651
+ it("keeps equal-sized scoped targets distinct across an in-flight resize", async () => {
652
+ const { lane, control } = rig();
653
+ control.owned.add("geometry");
654
+ const gate = deferred<void>();
655
+ control.resizeGates.push(gate.promise, Promise.resolve());
656
+ const first = lane.resize({ semanticWindowId: "window.a", cols: 80, rows: 24 });
657
+ const second = lane.resize({ semanticWindowId: "window.b", cols: 80, rows: 24 });
658
+ gate.resolve();
659
+ await settle();
660
+ expect(await first).toEqual({ status: "applied" });
661
+ expect(await second).toEqual({ status: "applied" });
662
+ expect(control.resizes.map(({ viewport }) => viewport.semanticWindowId)).toEqual([
663
+ "window.a",
664
+ "window.b",
665
+ ]);
666
+ lane.dispose();
667
+ });
668
+
651
669
  it("preserves an exact geometry authority conflict without treating it as applied", async () => {
652
670
  const { lane, control } = rig();
653
671
  control.owned.add("geometry");
@@ -73,6 +73,7 @@ export interface TerminalFastLaneControlPort {
73
73
  }
74
74
 
75
75
  export interface TerminalFastLaneViewport {
76
+ readonly semanticWindowId?: string;
76
77
  readonly cols: number;
77
78
  readonly rows: number;
78
79
  }
@@ -225,7 +226,11 @@ function validViewport(viewport: TerminalFastLaneViewport): boolean {
225
226
  Number.isInteger(viewport.cols) &&
226
227
  Number.isInteger(viewport.rows) &&
227
228
  viewport.cols > 0 &&
228
- viewport.rows > 0
229
+ viewport.rows > 0 &&
230
+ (viewport.semanticWindowId === undefined ||
231
+ (typeof viewport.semanticWindowId === "string" &&
232
+ viewport.semanticWindowId.length > 0 &&
233
+ viewport.semanticWindowId.length <= 256))
229
234
  );
230
235
  }
231
236
 
@@ -744,7 +749,7 @@ export function createTerminalFastLane(options: TerminalFastLaneOptions): Termin
744
749
  return Promise.resolve({ status: "failed", error: new TypeError("invalid viewport") });
745
750
  }
746
751
  const retained = Object.freeze({ ...viewport });
747
- const key = `${retained.cols}x${retained.rows}`;
752
+ const key = JSON.stringify([retained.semanticWindowId ?? null, retained.cols, retained.rows]);
748
753
  const epoch = generationEpoch;
749
754
  mutableCounters.resizeAccepted += 1;
750
755
  return new Promise((resolve) => {