[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
240
components/terminalPaneVisibility.ts
Normal file
240
components/terminalPaneVisibility.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { activeTabStore } from "../application/state/activeTabStore";
|
||||
import type { Workspace } from "../types";
|
||||
|
||||
export const HIDDEN_TERMINAL_PANE_SNAPSHOT = "hidden";
|
||||
|
||||
export type TerminalPaneSnapshot =
|
||||
| typeof HIDDEN_TERMINAL_PANE_SNAPSHOT
|
||||
| `solo|${string}`
|
||||
| `workspace|split|${string}`
|
||||
| `workspace|focus|${string}|${string}`;
|
||||
|
||||
export type TerminalPaneFocusSnapshot = "na" | "focused" | "unfocused";
|
||||
|
||||
export type TerminalPaneHiddenSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type TerminalPaneStyle = {
|
||||
left?: string | number;
|
||||
top?: string | number;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
visibility?: string;
|
||||
transform?: string;
|
||||
pointerEvents?: string;
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export function shouldUseTerminalPaneSplitLayout({
|
||||
workspace,
|
||||
}: {
|
||||
workspace: Workspace | undefined;
|
||||
sessionId: string;
|
||||
isVisible: boolean;
|
||||
hibernateHiddenTabs: boolean;
|
||||
}): boolean {
|
||||
if (!workspace) return false;
|
||||
// Default viewMode is tiled split (including undefined from createWorkspaceFromSessions).
|
||||
// Focus mode never uses split geometry: every pane is viewed full-size, so hidden
|
||||
// panes must keep full-size geometry too. Laying continuously rendered background
|
||||
// panes out at their split rects shrank the live xterm and the remote PTY to a
|
||||
// 1/N-width fragment, so \r-refresh progress output (e.g. rsync --info=progress2)
|
||||
// soft-wrapped and left one scrollback line per refresh (#3046).
|
||||
return workspace.viewMode !== "focus";
|
||||
}
|
||||
|
||||
export function shouldMeasureTerminalLayerLayout({
|
||||
isTerminalLayerVisible,
|
||||
keepHiddenLayoutActive,
|
||||
workspaceArea,
|
||||
}: {
|
||||
isTerminalLayerVisible: boolean;
|
||||
keepHiddenLayoutActive: boolean;
|
||||
workspaceArea: TerminalPaneHiddenSize;
|
||||
}): boolean {
|
||||
return isTerminalLayerVisible
|
||||
|| (keepHiddenLayoutActive && (workspaceArea.width <= 0 || workspaceArea.height <= 0));
|
||||
}
|
||||
|
||||
export function resolveInactiveTerminalPaneStyle<T extends TerminalPaneStyle>(
|
||||
layoutStyle: T,
|
||||
lastVisibleSize: TerminalPaneHiddenSize | null,
|
||||
hibernateHiddenTabs: boolean,
|
||||
preserveLastVisibleSize = false,
|
||||
): T {
|
||||
return {
|
||||
...layoutStyle,
|
||||
// Keep the measured box and live parser, but move the screen outside the
|
||||
// viewport so xterm's IntersectionObserver pauses rendering. Occlusion and
|
||||
// visibility:hidden alone do not stop xterm's WebGL refreshes.
|
||||
// Reset cached split offsets before translating: they can exceed the current
|
||||
// viewport after a window shrinks. Translate by both pane and viewport width,
|
||||
// including pinned panes wider than a window resized in the background.
|
||||
left: 0,
|
||||
transform: "translateX(calc(-100vw - 100%))",
|
||||
visibility: hibernateHiddenTabs ? "hidden" : "visible",
|
||||
pointerEvents: "none",
|
||||
zIndex: 0,
|
||||
...((hibernateHiddenTabs || preserveLastVisibleSize) && lastVisibleSize
|
||||
? {
|
||||
width: `${lastVisibleSize.width}px`,
|
||||
height: `${lastVisibleSize.height}px`,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTerminalLayerSurfaceStyle(
|
||||
isActive: boolean,
|
||||
hibernateHiddenTabs: boolean,
|
||||
): { visibility: "visible" | "hidden"; pointerEvents: "auto" | "none"; zIndex: number } {
|
||||
return {
|
||||
visibility: isActive || !hibernateHiddenTabs ? "visible" : "hidden",
|
||||
pointerEvents: isActive ? "auto" : "none",
|
||||
zIndex: isActive ? 10 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
interface GetTerminalPaneSnapshotOptions {
|
||||
activeTabId: string | null;
|
||||
sessionId: string;
|
||||
sessionWorkspaceId?: string;
|
||||
workspaceById: Map<string, Workspace>;
|
||||
isTerminalLayerVisible: boolean;
|
||||
}
|
||||
|
||||
export function getTerminalPaneSnapshot({
|
||||
activeTabId,
|
||||
sessionId,
|
||||
sessionWorkspaceId,
|
||||
workspaceById,
|
||||
isTerminalLayerVisible,
|
||||
}: GetTerminalPaneSnapshotOptions): TerminalPaneSnapshot {
|
||||
if (!isTerminalLayerVisible || !activeTabId) {
|
||||
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
||||
}
|
||||
|
||||
const activeWorkspace = workspaceById.get(activeTabId);
|
||||
if (activeWorkspace) {
|
||||
if (sessionWorkspaceId !== activeWorkspace.id) {
|
||||
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
||||
}
|
||||
|
||||
const focusedSessionId = activeWorkspace.focusedSessionId ?? "";
|
||||
if (activeWorkspace.viewMode === "focus") {
|
||||
return sessionId === focusedSessionId
|
||||
? `workspace|focus|${activeWorkspace.id}|${focusedSessionId}`
|
||||
: HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
||||
}
|
||||
|
||||
return `workspace|split|${activeWorkspace.id}`;
|
||||
}
|
||||
|
||||
return activeTabId === sessionId
|
||||
? `solo|${sessionId}`
|
||||
: HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
||||
}
|
||||
|
||||
export function parseTerminalPaneSnapshot(snapshot: TerminalPaneSnapshot): {
|
||||
isVisible: boolean;
|
||||
mode: "hidden" | "solo" | "split" | "focus";
|
||||
workspaceId: string | null;
|
||||
focusedSessionId: string | null;
|
||||
} {
|
||||
if (snapshot === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
||||
return {
|
||||
isVisible: false,
|
||||
mode: "hidden",
|
||||
workspaceId: null,
|
||||
focusedSessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
const parts = snapshot.split("|");
|
||||
if (parts[0] === "solo") {
|
||||
return {
|
||||
isVisible: true,
|
||||
mode: "solo",
|
||||
workspaceId: null,
|
||||
focusedSessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (parts[1] === "focus") {
|
||||
return {
|
||||
isVisible: true,
|
||||
mode: "focus",
|
||||
workspaceId: parts[2] || null,
|
||||
focusedSessionId: parts[3] || null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible: true,
|
||||
mode: "split",
|
||||
workspaceId: parts[2] || null,
|
||||
focusedSessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTerminalPaneFocusSnapshot({
|
||||
activeTabId: activeTabIdOverride,
|
||||
sessionId,
|
||||
sessionWorkspaceId,
|
||||
workspaceById,
|
||||
}: {
|
||||
activeTabId?: string | null;
|
||||
sessionId: string;
|
||||
sessionWorkspaceId?: string;
|
||||
workspaceById: Map<string, Workspace>;
|
||||
}): TerminalPaneFocusSnapshot {
|
||||
const activeTabId = activeTabIdOverride ?? activeTabStore.getActiveTabId();
|
||||
if (!activeTabId) return "na";
|
||||
|
||||
const activeWorkspace = workspaceById.get(activeTabId);
|
||||
if (!activeWorkspace || activeWorkspace.viewMode === "focus") return "na";
|
||||
if (sessionWorkspaceId !== activeWorkspace.id) return "na";
|
||||
|
||||
return activeWorkspace.focusedSessionId === sessionId ? "focused" : "unfocused";
|
||||
}
|
||||
|
||||
/** Combined visibility + focus snapshot for a single useSyncExternalStore subscription. */
|
||||
export function getTerminalPaneRenderSnapshot(
|
||||
options: GetTerminalPaneSnapshotOptions,
|
||||
): string {
|
||||
const pane = getTerminalPaneSnapshot(options);
|
||||
if (pane === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
||||
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
||||
}
|
||||
const focus = getTerminalPaneFocusSnapshot({
|
||||
activeTabId: options.activeTabId,
|
||||
sessionId: options.sessionId,
|
||||
sessionWorkspaceId: options.sessionWorkspaceId,
|
||||
workspaceById: options.workspaceById,
|
||||
});
|
||||
return `${pane}|${focus}`;
|
||||
}
|
||||
|
||||
export function parseTerminalPaneRenderSnapshot(snapshot: string): {
|
||||
paneState: ReturnType<typeof parseTerminalPaneSnapshot>;
|
||||
isFocusedPane: boolean;
|
||||
} {
|
||||
if (snapshot === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
||||
return {
|
||||
paneState: parseTerminalPaneSnapshot(HIDDEN_TERMINAL_PANE_SNAPSHOT),
|
||||
isFocusedPane: false,
|
||||
};
|
||||
}
|
||||
|
||||
const focusSep = snapshot.lastIndexOf("|");
|
||||
const focusToken = snapshot.slice(focusSep + 1);
|
||||
const paneSnapshot = snapshot.slice(0, focusSep) as TerminalPaneSnapshot;
|
||||
const paneState = parseTerminalPaneSnapshot(paneSnapshot);
|
||||
|
||||
return {
|
||||
paneState,
|
||||
isFocusedPane: focusToken === "focused",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user