fix(terminals): détecte la stabilité réelle du fit terminal au remount
L'ancien correctif (traîne de refits à nombre de frames fixe, MOUNT_SETTLE_REFIT_FRAMES) restait insuffisant : une cellule CLI pouvait rester mal dimensionnée après un changement de projet/layout si l'agent écrivait en arrière-plan pendant la fenêtre de stabilisation, nécessitant un resize manuel pour corriger l'affichage. TerminalView détecte désormais la stabilité réelle du fit (dimensions inchangées sur des mesures successives) au lieu de s'appuyer sur un nombre de frames fixe avant de rendre la main au ResizeObserver. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -455,10 +455,11 @@ describe("TerminalView — visible launch-failure surface (ticket #14 F3)", () =
|
||||
fitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps fitting across a keyed project remount until the new cell settles", async () => {
|
||||
it("keeps fitting across a keyed project remount until the new cell is actually stable", async () => {
|
||||
// ProjectsView keys LayoutGrid by project. A project switch therefore
|
||||
// unmounts/remounts TerminalView, so the mount path itself must keep a
|
||||
// short tail of fits alive while the new cell geometry settles.
|
||||
// unmounts/remounts TerminalView, so the mount path itself must fit until
|
||||
// the new cell's real geometry has stabilized, not for a guessed count of
|
||||
// frames.
|
||||
const fitSpy = vi.spyOn(FitAddon.prototype, "fit");
|
||||
const firstHandle = makeHandle({ sessionId: "project-1" });
|
||||
const secondHandle = makeHandle({ sessionId: "project-2" });
|
||||
@ -489,11 +490,11 @@ describe("TerminalView — visible launch-failure surface (ticket #14 F3)", () =
|
||||
await waitFor(() => expect(open).toHaveBeenCalledTimes(2));
|
||||
setTerminalBoxSize(520, 260);
|
||||
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
expect(fitSpy.mock.calls.length).toBeGreaterThan(5);
|
||||
expect(fitSpy.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
await waitFor(() => expect(secondHandle.resize).toHaveBeenCalled());
|
||||
expect(firstHandle.close).not.toHaveBeenCalled();
|
||||
expect(secondHandle.close).not.toHaveBeenCalled();
|
||||
@ -501,6 +502,70 @@ describe("TerminalView — visible launch-failure surface (ticket #14 F3)", () =
|
||||
fitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("refits a late remount after scrollback while the cell was absent", async () => {
|
||||
// Regression for project/layout switch: the agent can keep writing while
|
||||
// its view is unmounted. On reattach, scrollback is repainted immediately,
|
||||
// but the new container may stay 0x0 longer than the old fixed remount
|
||||
// frame tail. The view must keep retrying until a real size appears.
|
||||
const fitSpy = vi.spyOn(FitAddon.prototype, "fit");
|
||||
const handle = makeHandle({ sessionId: "late-remount-1" });
|
||||
const reattach = vi.fn(
|
||||
async (_sessionId: string, onData: (b: Uint8Array) => void) => {
|
||||
onData(new TextEncoder().encode("agent wrote while absent\r\n"));
|
||||
return {
|
||||
handle,
|
||||
scrollback: new TextEncoder().encode("retained scrollback\r\n"),
|
||||
} satisfies ReattachResult;
|
||||
},
|
||||
);
|
||||
const open = vi.fn(async () => makeHandle({ sessionId: "should-not-open" }));
|
||||
|
||||
renderView(new MockTerminalGateway(), "/cwd", {
|
||||
sessionId: "late-remount-1",
|
||||
open,
|
||||
reattach,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(reattach).toHaveBeenCalledTimes(1));
|
||||
expect(open).not.toHaveBeenCalled();
|
||||
fitSpy.mockClear();
|
||||
|
||||
// Stay zero-sized past the previous fixed remount tail.
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
expect(fitSpy).not.toHaveBeenCalled();
|
||||
expect(handle.resize).not.toHaveBeenCalled();
|
||||
|
||||
setTerminalBoxSize(560, 280);
|
||||
|
||||
await waitFor(() => expect(fitSpy).toHaveBeenCalled());
|
||||
await waitFor(() => expect(handle.resize).toHaveBeenCalled());
|
||||
expect(handle.close).not.toHaveBeenCalled();
|
||||
|
||||
fitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("continues refitting while remount geometry keeps changing, then stops after stability", async () => {
|
||||
const fitSpy = vi.spyOn(FitAddon.prototype, "fit");
|
||||
const handle = makeHandle({ sessionId: "moving-remount-1" });
|
||||
const open = vi.fn(async () => handle);
|
||||
|
||||
renderView(new MockTerminalGateway(), "/cwd", { open });
|
||||
await waitFor(() => expect(open).toHaveBeenCalledTimes(1));
|
||||
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
setTerminalBoxSize(420 + i, 220);
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
}
|
||||
|
||||
expect(fitSpy.mock.calls.length).toBeGreaterThan(12);
|
||||
setTerminalBoxSize(520, 260);
|
||||
await waitFor(() => expect(handle.resize).toHaveBeenCalled());
|
||||
|
||||
fitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not refit when refitSignal is left undefined (no-op for callers that don't pass it)", async () => {
|
||||
const fitSpy = vi.spyOn(FitAddon.prototype, "fit");
|
||||
const open = vi.fn(async () => makeHandle({ sessionId: "no-signal-1" }));
|
||||
|
||||
Reference in New Issue
Block a user