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
|
@@ -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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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) => {
|