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
|
@@ -155,7 +155,11 @@ export interface OpenTuiWorkspaceRuntimePort extends WorkspaceClientRuntimePort<
|
|
|
155
155
|
getLayout(): OpenTuiWorkspaceLayout | null;
|
|
156
156
|
getLayoutSnapshot(): OpenTuiWorkspaceLayoutSnapshot;
|
|
157
157
|
onLayout(listener: (layout: OpenTuiWorkspaceLayoutSnapshot) => void): () => void;
|
|
158
|
-
fitViewport(
|
|
158
|
+
fitViewport(
|
|
159
|
+
cols: number,
|
|
160
|
+
rows: number,
|
|
161
|
+
semanticWindowId?: string,
|
|
162
|
+
): Promise<"ok" | "geometry-authority-conflict">;
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
export interface ConnectOpenTuiWorkspaceRuntimePortOptions {
|
|
@@ -1395,8 +1399,17 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1395
1399
|
}
|
|
1396
1400
|
}) ?? null;
|
|
1397
1401
|
type FitResult = "ok" | "geometry-authority-conflict";
|
|
1398
|
-
let pendingInitialFit: {
|
|
1399
|
-
|
|
1402
|
+
let pendingInitialFit: {
|
|
1403
|
+
cols: number;
|
|
1404
|
+
rows: number;
|
|
1405
|
+
semanticWindowId?: string;
|
|
1406
|
+
result: Promise<FitResult>;
|
|
1407
|
+
} | null = null;
|
|
1408
|
+
const fitViewport = (
|
|
1409
|
+
cols: number,
|
|
1410
|
+
rows: number,
|
|
1411
|
+
semanticWindowId?: string,
|
|
1412
|
+
): Promise<FitResult> => {
|
|
1400
1413
|
if (closed) return Promise.resolve("geometry-authority-conflict");
|
|
1401
1414
|
// Keep only the latest requested geometry while hidden seeds arrive. All
|
|
1402
1415
|
// callers share one wait; resize storms cannot accumulate deferred work.
|
|
@@ -1404,23 +1417,31 @@ export async function connectOpenTuiWorkspaceRuntimePort(
|
|
|
1404
1417
|
if (pendingInitialFit) {
|
|
1405
1418
|
pendingInitialFit.cols = cols;
|
|
1406
1419
|
pendingInitialFit.rows = rows;
|
|
1420
|
+
pendingInitialFit.semanticWindowId = semanticWindowId;
|
|
1407
1421
|
return pendingInitialFit.result;
|
|
1408
1422
|
}
|
|
1409
1423
|
const request = {
|
|
1410
1424
|
cols,
|
|
1411
1425
|
rows,
|
|
1426
|
+
semanticWindowId,
|
|
1412
1427
|
result: Promise.resolve<FitResult>("geometry-authority-conflict"),
|
|
1413
1428
|
};
|
|
1414
1429
|
pendingInitialFit = request;
|
|
1415
1430
|
request.result = allSeeds.then((ready) => {
|
|
1416
1431
|
pendingInitialFit = null;
|
|
1417
|
-
return ready
|
|
1432
|
+
return ready
|
|
1433
|
+
? fitViewport(request.cols, request.rows, request.semanticWindowId)
|
|
1434
|
+
: "geometry-authority-conflict";
|
|
1418
1435
|
});
|
|
1419
1436
|
return request.result;
|
|
1420
1437
|
}
|
|
1421
1438
|
if ([...endpoints.values()].some((endpoint) => !endpoint.inputReady))
|
|
1422
1439
|
return Promise.resolve("geometry-authority-conflict");
|
|
1423
|
-
return
|
|
1440
|
+
return (
|
|
1441
|
+
semanticWindowId === undefined
|
|
1442
|
+
? opened.fitViewport(cols, rows)
|
|
1443
|
+
: opened.fitViewport(cols, rows, semanticWindowId)
|
|
1444
|
+
).catch((error: unknown) => {
|
|
1424
1445
|
if (error instanceof PaneStreamOperationError && error.code === "authority-rejected")
|
|
1425
1446
|
return "geometry-authority-conflict" as const;
|
|
1426
1447
|
throw error;
|
|
@@ -164,9 +164,8 @@ export interface PaneSurfaceOptions extends RenderableOptions<FrameBufferRendera
|
|
|
164
164
|
/** Bumps when canonical presentation changed without terminal cell damage. */
|
|
165
165
|
presentationVersion?: number;
|
|
166
166
|
/**
|
|
167
|
-
* Renderer-owned
|
|
168
|
-
*
|
|
169
|
-
* presentation may have been invalidated by composition or visibility.
|
|
167
|
+
* Renderer-owned composition generation. Retained framebuffer cells survive
|
|
168
|
+
* visibility and position changes; the hardware cursor must be re-presented.
|
|
170
169
|
*/
|
|
171
170
|
presentationGeneration?: string;
|
|
172
171
|
/** Retained-source generation; forces a full blit even when content version restarts equal. */
|
|
@@ -641,11 +640,11 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
641
640
|
set presentationGeneration(v: string) {
|
|
642
641
|
if (v === this._presentationGeneration) return;
|
|
643
642
|
this._presentationGeneration = v;
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
this.
|
|
648
|
-
this.
|
|
643
|
+
// Composition reuses the retained framebuffer. Geometry reallocation,
|
|
644
|
+
// source replacement and palette changes independently force a full walk.
|
|
645
|
+
// Reclaim the hardware cursor at the new absolute position on visibility.
|
|
646
|
+
this._needsCursorPresentation = true;
|
|
647
|
+
this.requestRender();
|
|
649
648
|
}
|
|
650
649
|
set sourceEpoch(v: number) {
|
|
651
650
|
if (v === this._sourceEpoch) return;
|
|
@@ -668,6 +667,8 @@ class PaneSurfaceRenderable extends FrameBufferRenderable {
|
|
|
668
667
|
// Detailed focus correlation never owns renderer replacement.
|
|
669
668
|
}
|
|
670
669
|
this._rendererEpoch = v;
|
|
670
|
+
this._forceFull = true;
|
|
671
|
+
this.invalidate();
|
|
671
672
|
}
|
|
672
673
|
set hostFocusTransitionOwner(v: PaneSurfaceHostFocusTransitionOwner | null | undefined) {
|
|
673
674
|
if (v !== this._hostFocusTransitionOwner) this.cancelPendingFocusTransition();
|
|
@@ -104,6 +104,7 @@ function sameTarget(
|
|
|
104
104
|
): boolean {
|
|
105
105
|
return (
|
|
106
106
|
state.operationId === observation.operationId &&
|
|
107
|
+
state.authorityGeneration === observation.authorityGeneration &&
|
|
107
108
|
state.workspaceName === observation.workspaceName &&
|
|
108
109
|
state.semanticPaneId === observation.semanticPaneId &&
|
|
109
110
|
state.axis === observation.axis
|
|
@@ -113,9 +114,9 @@ function sameTarget(
|
|
|
113
114
|
/**
|
|
114
115
|
* Renderer-local pane-resize transaction.
|
|
115
116
|
*
|
|
116
|
-
* Pointer motion
|
|
117
|
-
*
|
|
118
|
-
* operation
|
|
117
|
+
* Pointer motion submits the first target immediately, then retains only the
|
|
118
|
+
* latest desired size while receipt and canonical layout settle the pending
|
|
119
|
+
* operation. Release closes the gesture and flushes that latest target. The controller owns no timers, transport, or
|
|
119
120
|
* rendering effects beyond the functions supplied by its caller.
|
|
120
121
|
*/
|
|
121
122
|
export class ResizeTransactionController {
|
|
@@ -129,6 +130,8 @@ export class ResizeTransactionController {
|
|
|
129
130
|
#projection: OptimisticProjectionState<number, number> | null = null;
|
|
130
131
|
#revision = 0;
|
|
131
132
|
#disposed = false;
|
|
133
|
+
#dragOpen = false;
|
|
134
|
+
#submittedCells: number | null = null;
|
|
132
135
|
|
|
133
136
|
constructor(options: ResizeTransactionControllerOptions) {
|
|
134
137
|
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
@@ -150,6 +153,8 @@ export class ResizeTransactionController {
|
|
|
150
153
|
const workspaceName = nonEmpty(input.workspaceName, "workspaceName");
|
|
151
154
|
const semanticPaneId = nonEmpty(input.semanticPaneId, "semanticPaneId");
|
|
152
155
|
const canonicalCells = positiveCells(input.canonicalCells, "canonicalCells");
|
|
156
|
+
this.#dragOpen = true;
|
|
157
|
+
this.#submittedCells = null;
|
|
153
158
|
this.#projection = createOptimisticProjection<number, number>({
|
|
154
159
|
generation: authorityGeneration,
|
|
155
160
|
revision: this.#revision,
|
|
@@ -168,28 +173,31 @@ export class ResizeTransactionController {
|
|
|
168
173
|
return true;
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
/**
|
|
176
|
+
/** First/latest coalescing: at most one mutation awaits canonical settlement. */
|
|
172
177
|
move(previewCells: number): boolean {
|
|
173
|
-
if (this.#state.phase
|
|
178
|
+
if (!this.#dragOpen || this.#state.phase === "idle") return false;
|
|
174
179
|
const next = positiveCells(previewCells, "previewCells");
|
|
175
180
|
if (next === this.#state.previewCells) return false;
|
|
176
181
|
this.#emit({ ...this.#state, previewCells: next });
|
|
182
|
+
if (this.#state.phase === "dragging") this.#submit();
|
|
177
183
|
return true;
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
/**
|
|
181
|
-
* Submit once. Duplicate physical release/up/drop events return the existing
|
|
182
|
-
* operation id without authoring another mutation.
|
|
183
|
-
*/
|
|
186
|
+
/** Duplicate releases never author another mutation. */
|
|
184
187
|
release(): string | null {
|
|
188
|
+
this.#dragOpen = false;
|
|
185
189
|
if (this.#state.phase === "pending") return this.#state.operationId;
|
|
186
190
|
if (this.#state.phase !== "dragging") return null;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
191
|
+
const canonicalCells = this.#state.canonicalCells;
|
|
192
|
+
this.#emit({ phase: "idle", canonicalCells, outcome: null });
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
192
195
|
|
|
196
|
+
#submit(): string | null {
|
|
197
|
+
if (this.#state.phase !== "dragging") return null;
|
|
198
|
+
const dragging = this.#state;
|
|
199
|
+
if (dragging.previewCells === dragging.canonicalCells) return null;
|
|
200
|
+
this.#submittedCells = dragging.previewCells;
|
|
193
201
|
const operationId = nonEmpty(this.#options.operationId(), "operationId");
|
|
194
202
|
const submittedAt = this.#options.now();
|
|
195
203
|
if (!this.#projection) throw new Error("resize projection is unavailable");
|
|
@@ -245,9 +253,14 @@ export class ResizeTransactionController {
|
|
|
245
253
|
});
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
/**
|
|
256
|
+
/** Discard queued motion; an already dispatched mutation cannot be undone. */
|
|
249
257
|
cancelDrag(): boolean {
|
|
250
|
-
if (this.#state.phase
|
|
258
|
+
if (!this.#dragOpen || this.#state.phase === "idle") return false;
|
|
259
|
+
this.#dragOpen = false;
|
|
260
|
+
if (this.#state.phase === "pending") {
|
|
261
|
+
this.#emit({ ...this.#state, previewCells: this.#submittedCells! });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
251
264
|
const canonicalCells = this.#state.canonicalCells;
|
|
252
265
|
this.#emit({ phase: "idle", canonicalCells, outcome: null });
|
|
253
266
|
return true;
|
|
@@ -260,6 +273,8 @@ export class ResizeTransactionController {
|
|
|
260
273
|
|
|
261
274
|
/** Retire one runtime lane without permanently disposing the controller. */
|
|
262
275
|
retire(): void {
|
|
276
|
+
this.#dragOpen = false;
|
|
277
|
+
this.#submittedCells = null;
|
|
263
278
|
this.#clearTimeout();
|
|
264
279
|
this.#projection = null;
|
|
265
280
|
if (this.#state.phase !== "idle" || this.#state.canonicalCells !== null) {
|
|
@@ -277,7 +292,8 @@ export class ResizeTransactionController {
|
|
|
277
292
|
)
|
|
278
293
|
return false;
|
|
279
294
|
const cells = positiveCells(observation.cells, "observed cells");
|
|
280
|
-
const
|
|
295
|
+
const pending = this.#state;
|
|
296
|
+
const operationId = pending.operationId;
|
|
281
297
|
if (!this.#projection) return false;
|
|
282
298
|
this.#revision += 1;
|
|
283
299
|
this.#projection = replaceCommittedProjection(
|
|
@@ -290,11 +306,18 @@ export class ResizeTransactionController {
|
|
|
290
306
|
{ observedOperationIds: [operationId], nowMs: this.#options.now() },
|
|
291
307
|
);
|
|
292
308
|
this.#clearTimeout();
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
309
|
+
const hasLatest =
|
|
310
|
+
pending.previewCells !== this.#submittedCells && pending.previewCells !== cells;
|
|
311
|
+
if (this.#dragOpen || hasLatest) {
|
|
312
|
+
this.#emit({ ...pending, phase: "dragging", canonicalCells: cells });
|
|
313
|
+
if (hasLatest) this.#submit();
|
|
314
|
+
} else {
|
|
315
|
+
this.#emit({
|
|
316
|
+
phase: "idle",
|
|
317
|
+
canonicalCells: cells,
|
|
318
|
+
outcome: { kind: "settled", operationId, source, cells },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
298
321
|
return true;
|
|
299
322
|
}
|
|
300
323
|
|
|
@@ -321,6 +344,7 @@ export class ResizeTransactionController {
|
|
|
321
344
|
);
|
|
322
345
|
}
|
|
323
346
|
const canonicalCells = this.#state.canonicalCells;
|
|
347
|
+
this.#dragOpen = false;
|
|
324
348
|
this.#clearTimeout();
|
|
325
349
|
this.#emit({
|
|
326
350
|
phase: "idle",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
import { createAutomaticContrastPass } from "../automatic-contrast.ts";
|
|
1
3
|
import { VISUAL_THEME_PRESETS, findVisualThemePreset } from "@tmux-ide/contracts";
|
|
2
4
|
import { batch, createSignal, type Accessor } from "solid-js";
|
|
3
5
|
|
|
@@ -26,6 +28,8 @@ export interface ApplicationAppearanceOwner {
|
|
|
26
28
|
readonly palette: Accessor<TerminalPaletteProjection>;
|
|
27
29
|
readonly hostPalette: Accessor<ApplicationTerminalPaletteSnapshot>;
|
|
28
30
|
readonly setTransientNote: (note: string | null) => void;
|
|
31
|
+
readonly automaticContrast: Accessor<boolean>;
|
|
32
|
+
readonly toggleAutomaticContrast: () => void;
|
|
29
33
|
readonly pickerOpen: Accessor<boolean>;
|
|
30
34
|
readonly pickerError: Accessor<string | null>;
|
|
31
35
|
readonly openPicker: () => void;
|
|
@@ -54,15 +58,36 @@ export interface ApplicationAppearanceSnapshot {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
function hostDefaults(snapshot: ApplicationTerminalPaletteSnapshot) {
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
return (
|
|
62
|
+
deriveSystemVisualHostDefaults(snapshot) ?? { appearance: snapshot.detectedMode, overrides: {} }
|
|
63
|
+
);
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export function createAppearanceOwner(
|
|
62
67
|
config: AppConfig,
|
|
63
|
-
renderer: ThemeModeSource
|
|
68
|
+
renderer: ThemeModeSource &
|
|
69
|
+
Partial<Pick<CliRenderer, "addPostProcessFn" | "removePostProcessFn" | "requestRender">>,
|
|
64
70
|
terminalPaletteOwner: ApplicationTerminalPaletteOwner,
|
|
65
71
|
): ApplicationAppearanceOwner {
|
|
72
|
+
const [automaticContrast, setAutomaticContrast] = createSignal(
|
|
73
|
+
config.theme.automaticContrast ?? true,
|
|
74
|
+
);
|
|
75
|
+
const correct = createAutomaticContrastPass();
|
|
76
|
+
const postProcess: Parameters<CliRenderer["addPostProcessFn"]>[0] = (buffer) => {
|
|
77
|
+
if (automaticContrast()) correct(buffer);
|
|
78
|
+
};
|
|
79
|
+
renderer.addPostProcessFn?.(postProcess);
|
|
80
|
+
const setContrast = (enabled: boolean) => {
|
|
81
|
+
if (enabled === automaticContrast()) return;
|
|
82
|
+
setAutomaticContrast(enabled);
|
|
83
|
+
renderer.requestRender?.();
|
|
84
|
+
};
|
|
85
|
+
const toggleAutomaticContrast = () => {
|
|
86
|
+
if (pickerOpen()) {
|
|
87
|
+
setContrast(!automaticContrast());
|
|
88
|
+
setPickerError(null);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
66
91
|
const initialHostPalette = terminalPaletteOwner.getSnapshot();
|
|
67
92
|
const store = createSemanticThemeStore(config.theme, {
|
|
68
93
|
rendererMode: renderer.themeMode,
|
|
@@ -128,10 +153,17 @@ export function createAppearanceOwner(
|
|
|
128
153
|
const apply = (id: string) => {
|
|
129
154
|
const preset = findVisualThemePreset(id);
|
|
130
155
|
const mode = preset?.appearance ?? (id as ThemeModeSetting);
|
|
131
|
-
|
|
156
|
+
const wasSystem = theme().setting === "system";
|
|
157
|
+
store.configure({
|
|
158
|
+
...config.theme,
|
|
159
|
+
mode,
|
|
160
|
+
preset: preset?.id,
|
|
161
|
+
});
|
|
162
|
+
if (mode === "system" && !wasSystem) void terminalPaletteOwner.refresh();
|
|
132
163
|
setPickerSelection(id);
|
|
133
164
|
};
|
|
134
165
|
let originalSelection = pickerSelection();
|
|
166
|
+
let originalContrast = automaticContrast();
|
|
135
167
|
const preview = (id: string): void => {
|
|
136
168
|
if (!pickerOpen()) return;
|
|
137
169
|
setPickerError(null);
|
|
@@ -139,6 +171,7 @@ export function createAppearanceOwner(
|
|
|
139
171
|
};
|
|
140
172
|
const cancelPicker = (): void => {
|
|
141
173
|
if (!pickerOpen()) return;
|
|
174
|
+
setContrast(originalContrast);
|
|
142
175
|
apply(originalSelection);
|
|
143
176
|
setPickerOpen(false);
|
|
144
177
|
setPickerError(null);
|
|
@@ -148,6 +181,7 @@ export function createAppearanceOwner(
|
|
|
148
181
|
try {
|
|
149
182
|
updateAppConfig({
|
|
150
183
|
theme: {
|
|
184
|
+
automaticContrast: automaticContrast(),
|
|
151
185
|
mode: theme().setting,
|
|
152
186
|
preset: findVisualThemePreset(pickerSelection())?.id ?? "",
|
|
153
187
|
},
|
|
@@ -165,7 +199,7 @@ export function createAppearanceOwner(
|
|
|
165
199
|
const order: readonly ThemeModeSetting[] = ["dark", "light", "system"];
|
|
166
200
|
const next = order[(order.indexOf(theme().setting) + 1) % order.length]!;
|
|
167
201
|
apply(next);
|
|
168
|
-
updateAppConfig({ theme: { mode: next, preset: "" } });
|
|
202
|
+
updateAppConfig({ theme: { mode: next, preset: "", automaticContrast: automaticContrast() } });
|
|
169
203
|
notice.publish(`theme → ${next}`);
|
|
170
204
|
};
|
|
171
205
|
return {
|
|
@@ -176,6 +210,8 @@ export function createAppearanceOwner(
|
|
|
176
210
|
palette,
|
|
177
211
|
hostPalette,
|
|
178
212
|
setTransientNote: notice.publish,
|
|
213
|
+
automaticContrast,
|
|
214
|
+
toggleAutomaticContrast,
|
|
179
215
|
pickerOpen,
|
|
180
216
|
pickerError,
|
|
181
217
|
pickerQuery,
|
|
@@ -184,6 +220,7 @@ export function createAppearanceOwner(
|
|
|
184
220
|
openPicker() {
|
|
185
221
|
if (pickerOpen()) return;
|
|
186
222
|
originalSelection = pickerSelection();
|
|
223
|
+
originalContrast = automaticContrast();
|
|
187
224
|
setPickerQuery("");
|
|
188
225
|
setPickerError(null);
|
|
189
226
|
setPickerOpen(true);
|
|
@@ -196,7 +233,8 @@ export function createAppearanceOwner(
|
|
|
196
233
|
if (event.eventType === "release" || event.repeated) return true;
|
|
197
234
|
const name = event.name.toLowerCase();
|
|
198
235
|
const choices = pickerOptions();
|
|
199
|
-
if (name === "
|
|
236
|
+
if (event.ctrl && name === "a") toggleAutomaticContrast();
|
|
237
|
+
else if (name === "escape") cancelPicker();
|
|
200
238
|
else if (name === "return" || name === "enter") savePicker();
|
|
201
239
|
else if (name === "up" || name === "down" || name === "tab") {
|
|
202
240
|
if (choices.length) {
|
|
@@ -219,6 +257,7 @@ export function createAppearanceOwner(
|
|
|
219
257
|
},
|
|
220
258
|
cycleTheme,
|
|
221
259
|
dispose() {
|
|
260
|
+
renderer.removePostProcessFn?.(postProcess);
|
|
222
261
|
stopTheme();
|
|
223
262
|
stopRendererTheme();
|
|
224
263
|
stopHostPalette();
|
|
@@ -175,6 +175,7 @@ export function ApplicationMachineSidebar(props: {
|
|
|
175
175
|
: []),
|
|
176
176
|
]),
|
|
177
177
|
);
|
|
178
|
+
const rowsByKey = createMemo(() => new Map(rows().map((row) => [row.key, row])));
|
|
178
179
|
const index = () =>
|
|
179
180
|
Math.max(
|
|
180
181
|
0,
|
|
@@ -437,83 +438,106 @@ export function ApplicationMachineSidebar(props: {
|
|
|
437
438
|
scrollY={true}
|
|
438
439
|
horizontalScrollbarOptions={{ visible: false }}
|
|
439
440
|
>
|
|
440
|
-
<For each={rows()}>
|
|
441
|
-
{(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
441
|
+
<For each={rows().map((row) => row.key)}>
|
|
442
|
+
{(key) => {
|
|
443
|
+
// The semantic key owns the native row; fresh snapshots update its
|
|
444
|
+
// props instead of destroying and recreating identical renderables.
|
|
445
|
+
const current = createMemo<Row>(
|
|
446
|
+
(previous) => rowsByKey().get(key) ?? previous,
|
|
447
|
+
rowsByKey().get(key)!,
|
|
448
|
+
);
|
|
449
|
+
const row: Row = {
|
|
450
|
+
key,
|
|
451
|
+
get group() {
|
|
452
|
+
return current().group;
|
|
453
|
+
},
|
|
454
|
+
get session() {
|
|
455
|
+
return current().session;
|
|
456
|
+
},
|
|
457
|
+
get agent() {
|
|
458
|
+
return current().agent;
|
|
459
|
+
},
|
|
460
|
+
get agentHeading() {
|
|
461
|
+
return current().agentHeading;
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
return (
|
|
465
|
+
<box
|
|
457
466
|
width={Math.max(1, props.width - 1)}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
?
|
|
478
|
-
:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
:
|
|
490
|
-
|
|
491
|
-
|
|
467
|
+
height={row.agent ? (row.agentHeading ? 3 : 2) : 1}
|
|
468
|
+
flexShrink={0}
|
|
469
|
+
flexDirection="column"
|
|
470
|
+
>
|
|
471
|
+
<Show when={row.agentHeading}>
|
|
472
|
+
<text height={1} fg={props.theme.roles.text.secondary}>
|
|
473
|
+
{" "}
|
|
474
|
+
Agents
|
|
475
|
+
</text>
|
|
476
|
+
</Show>
|
|
477
|
+
<NavigationRow
|
|
478
|
+
theme={props.theme}
|
|
479
|
+
id={`machine:${row.key}`}
|
|
480
|
+
width={Math.max(1, props.width - 1)}
|
|
481
|
+
labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined}
|
|
482
|
+
label={
|
|
483
|
+
row.agent
|
|
484
|
+
? row.agent.name
|
|
485
|
+
: row.session
|
|
486
|
+
? friendlySessionLabel(row.session.name)
|
|
487
|
+
: row.group.label
|
|
488
|
+
}
|
|
489
|
+
marker={
|
|
490
|
+
row.agent
|
|
491
|
+
? active(row)
|
|
492
|
+
? row.agent.attention
|
|
493
|
+
? "›!"
|
|
494
|
+
: "›"
|
|
495
|
+
: row.agent.attention
|
|
496
|
+
? "!"
|
|
497
|
+
: "•"
|
|
498
|
+
: row.session
|
|
499
|
+
? props.model.favorites?.().includes(row.session.id)
|
|
500
|
+
? " ★"
|
|
501
|
+
: active(row)
|
|
502
|
+
? " ›"
|
|
503
|
+
: " "
|
|
504
|
+
: collapsed().has(preferenceKey(row.group))
|
|
505
|
+
? "▸"
|
|
506
|
+
: "▾"
|
|
507
|
+
}
|
|
508
|
+
detail={
|
|
509
|
+
row.agent
|
|
510
|
+
? row.group.state !== "ready" || row.agent.disabled
|
|
492
511
|
? "unavailable"
|
|
493
|
-
:
|
|
494
|
-
: row.
|
|
495
|
-
?
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
512
|
+
: `[${terminalAgentStatusLabel(row.agent.activity)}]`
|
|
513
|
+
: row.session
|
|
514
|
+
? row.group.state !== "ready" || row.session.disabled
|
|
515
|
+
? "unavailable"
|
|
516
|
+
: `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
|
|
517
|
+
: row.group.agents?.length && row.group.state === "ready"
|
|
518
|
+
? activity(row).label
|
|
519
|
+
: `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
|
|
520
|
+
}
|
|
521
|
+
selected={Boolean((row.session || row.agent) && active(row))}
|
|
522
|
+
focused={Boolean(focused() && row.key === selectedKey())}
|
|
523
|
+
attention={row.agent?.attention ?? activity(row).kind === "attention"}
|
|
524
|
+
onActivate={(source) => activate(row, source)}
|
|
525
|
+
/>
|
|
526
|
+
<Show when={row.agent}>
|
|
527
|
+
{(agent) => (
|
|
528
|
+
<text
|
|
529
|
+
height={1}
|
|
530
|
+
fg={props.theme.roles.text.muted}
|
|
531
|
+
content={clipTerminal(
|
|
532
|
+
` ${friendlySessionLabel(agent().sessionName)}`,
|
|
533
|
+
Math.max(1, props.width - 1),
|
|
534
|
+
)}
|
|
535
|
+
/>
|
|
536
|
+
)}
|
|
537
|
+
</Show>
|
|
538
|
+
</box>
|
|
539
|
+
);
|
|
540
|
+
}}
|
|
517
541
|
</For>
|
|
518
542
|
</scrollbox>
|
|
519
543
|
<For each={agentHeight() > 0 ? [true] : []}>
|
|
@@ -622,6 +622,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
|
|
|
622
622
|
})}
|
|
623
623
|
onResizePreview={recoverHostFocus(interaction.previewPaneResize)}
|
|
624
624
|
onResizePane={recoverHostFocus(interaction.resizePane)}
|
|
625
|
+
onCancelResize={interaction.cancelPaneResize}
|
|
625
626
|
onResizePointerIngress={recoverHostFocus.optional(resizeIngress)}
|
|
626
627
|
onWheelObservation={tuiPerfWheelObservation}
|
|
627
628
|
onTerminalInput={recoverHostFocus((paneId, input) =>
|
|
@@ -493,7 +493,7 @@ export function AppearanceDialog(props: {
|
|
|
493
493
|
viewportWidth={props.width}
|
|
494
494
|
viewportHeight={props.height}
|
|
495
495
|
width={width()}
|
|
496
|
-
height={Math.min(
|
|
496
|
+
height={Math.min(19, props.height)}
|
|
497
497
|
title="Appearance"
|
|
498
498
|
footer={width() < 40 ? "Enter save · Esc back" : "↑↓ preview · Enter save · Esc cancel"}
|
|
499
499
|
active={props.active}
|
|
@@ -505,10 +505,17 @@ export function AppearanceDialog(props: {
|
|
|
505
505
|
fg={props.owner.theme().roles.text.primary}
|
|
506
506
|
content={`Search: ${props.owner.pickerQuery() || "type to filter"}`}
|
|
507
507
|
/>
|
|
508
|
+
<TuiButton
|
|
509
|
+
theme={props.owner.theme()}
|
|
510
|
+
label={`^A Contrast: ${props.owner.automaticContrast() ? "On" : "Off"}`}
|
|
511
|
+
size="compact"
|
|
512
|
+
disabled={props.active === false}
|
|
513
|
+
onPress={props.owner.toggleAutomaticContrast}
|
|
514
|
+
/>
|
|
508
515
|
<For
|
|
509
516
|
each={(() => {
|
|
510
517
|
const options = props.owner.pickerOptions();
|
|
511
|
-
const count = Math.max(1, Math.min(10, props.height -
|
|
518
|
+
const count = Math.max(1, Math.min(10, props.height - 9));
|
|
512
519
|
const index = options.findIndex((p) => p.id === props.owner.pickerSelection());
|
|
513
520
|
const start = Math.max(
|
|
514
521
|
0,
|
|
@@ -125,6 +125,7 @@ export interface ApplicationShellViewProps {
|
|
|
125
125
|
readonly onSelectPane: TerminalWorkspaceProps["onSelectPane"];
|
|
126
126
|
readonly onResizePreview: TerminalWorkspaceProps["onResizePreview"];
|
|
127
127
|
readonly onResizePane: TerminalWorkspaceProps["onResizePane"];
|
|
128
|
+
readonly onCancelResize?: TerminalWorkspaceProps["onCancelResize"];
|
|
128
129
|
readonly onResizePointerIngress?: TerminalWorkspaceProps["onResizePointerIngress"];
|
|
129
130
|
readonly onWheelObservation?: TerminalWorkspaceProps["onWheelObservation"];
|
|
130
131
|
readonly onTerminalInput?: TerminalWorkspaceProps["onTerminalInput"];
|
|
@@ -517,6 +518,7 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
|
|
|
517
518
|
}}
|
|
518
519
|
onResizePreview={props.onResizePreview}
|
|
519
520
|
onResizePane={props.onResizePane}
|
|
521
|
+
onCancelResize={props.onCancelResize}
|
|
520
522
|
onResizePointerIngress={props.onResizePointerIngress}
|
|
521
523
|
onWheelObservation={props.onWheelObservation}
|
|
522
524
|
onTerminalInput={props.onTerminalInput}
|