Fix initial terminal resize for nvim

This commit is contained in:
Mohamed Boudra
2026-04-24 15:57:13 +07:00
parent 83689ef0c5
commit 2b37276577
3 changed files with 135 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
import type { Page } from "@playwright/test";
import { test, expect } from "./fixtures";
import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl";
import { getTabTestIds } from "./helpers/launcher";
import {
installTerminalRenderProbe,
readTerminalRenderProbe,
@@ -10,6 +11,54 @@ import {
} from "./helpers/terminal-probes";
import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf";
interface TerminalLayoutMetrics {
visibleSurfaceCount: number;
surfaceHeight: number;
rowCount: number;
rows: number | null;
cols: number | null;
renderedRowsHeight: number;
tabIds: string[];
}
async function readTerminalLayoutMetrics(page: Page): Promise<TerminalLayoutMetrics> {
const tabIds = await getTabTestIds(page);
return page.evaluate((currentTabIds) => {
const visibleSurfaces = Array.from(
document.querySelectorAll<HTMLElement>('[data-testid="terminal-surface"]'),
).filter((candidate) => {
const rect = candidate.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
const surface = visibleSurfaces[0] ?? null;
const renderedRows = Array.from(document.querySelectorAll<HTMLElement>(".xterm-rows > div"));
const firstRow = renderedRows[0] ?? null;
const lastRow = renderedRows.at(-1) ?? null;
const surfaceRect = surface?.getBoundingClientRect() ?? null;
const firstRowRect = firstRow?.getBoundingClientRect() ?? null;
const lastRowRect = lastRow?.getBoundingClientRect() ?? null;
const term = (
window as Window & {
__paseoTerminal?: {
rows?: number;
cols?: number;
};
}
).__paseoTerminal;
return {
visibleSurfaceCount: visibleSurfaces.length,
surfaceHeight: surfaceRect?.height ?? 0,
rowCount: renderedRows.length,
rows: typeof term?.rows === "number" ? term.rows : null,
cols: typeof term?.cols === "number" ? term.cols : null,
renderedRowsHeight:
firstRowRect && lastRowRect ? Math.max(0, lastRowRect.bottom - firstRowRect.top) : 0,
tabIds: currentTabIds,
};
}, tabIds);
}
async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout: number) {
let lastBufferText = "";
let lastProbe = await readTerminalRenderProbe(page);
@@ -53,6 +102,8 @@ async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout:
}
test.describe("Terminal alternate-screen transitions", () => {
test.describe.configure({ timeout: 120_000 });
let harness: TerminalE2EHarness;
test.beforeAll(async () => {
@@ -138,4 +189,62 @@ test.describe("Terminal alternate-screen transitions", () => {
).toEqual([]);
});
});
test("opening vim in a new terminal fills the terminal surface", async ({ page }, testInfo) => {
test.setTimeout(60_000);
await page.setViewportSize({ width: 1280, height: 900 });
await installTerminalRenderProbe(page);
await withTerminalInApp(page, harness, { name: "vim-layout" }, async () => {
await harness.setupPrompt(page);
const terminalSurface = harness.terminalSurface(page).first();
await expect(terminalSurface).toBeVisible({ timeout: 15_000 });
await terminalSurface.click();
const beforeVim = await readTerminalLayoutMetrics(page);
await startTerminalFrameSampling(page, 2_000);
await terminalSurface.pressSequentially("vim -Nu NONE -n\n", { delay: 0 });
await page.waitForTimeout(1_500);
const probe = await readTerminalRenderProbe(page);
const samples: TerminalLayoutMetrics[] = [];
for (let index = 0; index < 12; index += 1) {
samples.push(await readTerminalLayoutMetrics(page));
await page.waitForTimeout(50);
}
await testInfo.attach("reused-terminal-layout-metrics", {
body: JSON.stringify(
{
beforeVim,
probe: summarizeTerminalRenderProbe(probe),
samples,
},
null,
2,
),
contentType: "application/json",
});
const firstSample = samples[0];
const finalSample = samples.at(-1);
expect(firstSample, "expected an initial layout sample").toBeTruthy();
expect(finalSample, "expected a final layout sample").toBeTruthy();
expect(
finalSample?.visibleSurfaceCount ?? 0,
"opening vim should leave exactly one visible terminal surface",
).toBe(1);
expect(
finalSample?.renderedRowsHeight ?? 0,
"vim should render rows that fill most of the terminal surface",
).toBeGreaterThan((finalSample?.surfaceHeight ?? 0) * 0.75);
expect(
finalSample?.rowCount ?? 0,
"vim should leave a substantial number of rendered rows",
).toBeGreaterThan(Math.max(10, Math.floor((beforeVim.rowCount || 0) * 0.75)));
});
});
});

View File

@@ -164,7 +164,10 @@ export function TerminalPane({
const isConnected = useHostRuntimeIsConnected(serverId);
const scopeKey = useMemo(() => terminalScopeKey({ serverId, cwd }), [serverId, cwd]);
const lastReportedSizeRef = useRef<{ rows: number; cols: number } | null>(null);
// Keep the latest measured size for whichever client currently owns the pane,
// but only dedupe resizes that this specific client has already pushed.
const measuredTerminalSizeRef = useRef<{ rows: number; cols: number } | null>(null);
const lastSentTerminalSizeRef = useRef<{ rows: number; cols: number } | null>(null);
const streamControllerRef = useRef<TerminalStreamController | null>(null);
const workspaceTerminalSession = useMemo(
() => getWorkspaceTerminalSession({ scopeKey }),
@@ -210,7 +213,7 @@ export function TerminalPane({
useEffect(() => {
if (isPaneFocused && isWorkspaceFocused && isAppVisible && terminalId) {
lastReportedSizeRef.current = null;
lastSentTerminalSizeRef.current = null;
requestTerminalReflow();
}
}, [isAppVisible, isPaneFocused, isWorkspaceFocused, requestTerminalReflow, terminalId]);
@@ -296,7 +299,8 @@ export function TerminalPane({
}, [client, isConnected, isWorkspaceFocused, workspaceTerminalSession.snapshots]);
useEffect(() => {
lastReportedSizeRef.current = null;
measuredTerminalSizeRef.current = null;
lastSentTerminalSizeRef.current = null;
}, [scopeKey]);
const handleStreamControllerStatus = useCallback((status: TerminalStreamControllerStatus) => {
@@ -316,7 +320,7 @@ export function TerminalPane({
const controller = new TerminalStreamController({
client,
getPreferredSize: () => lastReportedSizeRef.current,
getPreferredSize: () => measuredTerminalSizeRef.current,
onOutput: ({ terminalId: outputTerminalId, text }) => {
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
return;
@@ -532,24 +536,25 @@ export function TerminalPane({
const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => {
const { rows, cols } = input;
if (
!client ||
!terminalId ||
!isPaneFocused ||
!isWorkspaceFocused ||
!isAppVisible ||
rows <= 0 ||
cols <= 0
) {
if (rows <= 0 || cols <= 0) {
return;
}
const normalizedRows = Math.floor(rows);
const normalizedCols = Math.floor(cols);
const previous = lastReportedSizeRef.current;
if (previous && previous.rows === normalizedRows && previous.cols === normalizedCols) {
const nextSize = { rows: normalizedRows, cols: normalizedCols };
measuredTerminalSizeRef.current = nextSize;
if (!client || !terminalId || !isPaneFocused || !isWorkspaceFocused || !isAppVisible) {
return;
}
lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols };
const previousSent = lastSentTerminalSizeRef.current;
if (
previousSent &&
previousSent.rows === normalizedRows &&
previousSent.cols === normalizedCols
) {
return;
}
lastSentTerminalSizeRef.current = nextSize;
client.sendTerminalInput(terminalId, {
type: "resize",
rows: normalizedRows,

View File

@@ -43,7 +43,11 @@ app.setName("Paseo");
// In dev mode, detect git worktrees and isolate each instance so multiple
// Electron windows can run side-by-side (separate userData = separate lock).
let devWorktreeName: string | null = null;
if (!app.isPackaged) {
const forcedUserDataDir = process.env.PASEO_ELECTRON_USER_DATA_DIR?.trim();
if (forcedUserDataDir) {
app.setPath("userData", forcedUserDataDir);
log.info("[dev-user-data] forced userData dir:", forcedUserDataDir);
} else if (!app.isPackaged) {
try {
const topLevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf-8",