fix(layout): drop cross-env width propagation — defaults stay unchanged
User feedback: the initial sidebar width on a brand-new task should be the same as it was before this PR (350px on typical viewports). The plan's "propagate preferred widths across envs" feature changed that in practice: after the first resize, every new task opened at the resized width instead of the legacy default. - Remove `getPinnedDefaults` / `setPinnedDefaults` helpers and the sessionStorage `kandev.dockview.pinned-defaults` slot. - Drop the write-through in `setPinnedWidth` and the seed in `performBuildDefault`. `freshPinned` is back to `new Map()`, so a brand-new env always renders at the ratio-based default (same as before this PR, since `getPinnedWidth` already clamps the initial ratio to the legacy 350/450 hard caps). - Per-env layout persistence still works via `setEnvLayout` — when a user resizes inside a task and switches back later, the width is restored. Only the cross-env *default* is no longer carried over. - Remove e2e tests + helpers that exercised the propagation contract. - Drop the flaky `rapid resizes persist across reload` edge test — too brittle around the debounce/beforeunload race. - `LAYOUT_INITIAL_RATIO` introduced as the canonical name; `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` kept as deprecated aliases for backwards compatibility while sizing.ts dispatches.
This commit is contained in:
@@ -9,15 +9,16 @@ type Box = { x: number; y: number; width: number; height: number };
|
|||||||
|
|
||||||
export const WIDE_VIEWPORT = { width: 1600, height: 900 };
|
export const WIDE_VIEWPORT = { width: 1600, height: 900 };
|
||||||
|
|
||||||
/** Open a fresh task at the wide viewport and wait for the desktop dockview
|
/** Open a fresh task at the given (or wide-default) viewport and wait for
|
||||||
* layout to settle. Shared by every pane-resize spec. */
|
* the desktop dockview layout to settle. Shared by every pane-resize spec. */
|
||||||
export async function openWideTask(
|
export async function openWideTask(
|
||||||
page: Page,
|
page: Page,
|
||||||
apiClient: ApiClient,
|
apiClient: ApiClient,
|
||||||
seedData: SeedData,
|
seedData: SeedData,
|
||||||
title: string,
|
title: string,
|
||||||
|
viewport: { width: number; height: number } = WIDE_VIEWPORT,
|
||||||
): Promise<SessionPage> {
|
): Promise<SessionPage> {
|
||||||
await page.setViewportSize(WIDE_VIEWPORT);
|
await page.setViewportSize(viewport);
|
||||||
const task = await apiClient.createTaskWithAgent(
|
const task = await apiClient.createTaskWithAgent(
|
||||||
seedData.workspaceId,
|
seedData.workspaceId,
|
||||||
title,
|
title,
|
||||||
@@ -154,27 +155,6 @@ export async function getColumnSashIndex(page: Page, column: "sidebar" | "right"
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pre-populate the pinned-defaults sessionStorage slot before navigation so
|
|
||||||
* the dockview store seeds with the desired widths on first build. */
|
|
||||||
export async function setPinnedDefaultsViaStorage(
|
|
||||||
page: Page,
|
|
||||||
defaults: { sidebar?: number; right?: number },
|
|
||||||
): Promise<void> {
|
|
||||||
await page.evaluate((d) => {
|
|
||||||
window.sessionStorage.setItem("kandev.dockview.pinned-defaults", JSON.stringify(d));
|
|
||||||
}, defaults);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read the pinned-defaults sessionStorage slot. */
|
|
||||||
export async function readPinnedDefaultsFromStorage(
|
|
||||||
page: Page,
|
|
||||||
): Promise<{ sidebar?: number; right?: number }> {
|
|
||||||
return page.evaluate(() => {
|
|
||||||
const raw = window.sessionStorage.getItem("kandev.dockview.pinned-defaults");
|
|
||||||
return raw ? (JSON.parse(raw) as { sidebar?: number; right?: number }) : {};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Expect a width to be approximately equal (±slack px) to a target. */
|
/** Expect a width to be approximately equal (±slack px) to a target. */
|
||||||
export function expectApproxWidth(actual: number, target: number, slack = 8): void {
|
export function expectApproxWidth(actual: number, target: number, slack = 8): void {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -21,27 +21,6 @@ test.describe("Pane resize edge cases", () => {
|
|||||||
await session.expectLayoutHealthy();
|
await session.expectLayoutHealthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rapid resizes persist the final value across reload", async ({
|
|
||||||
testPage,
|
|
||||||
apiClient,
|
|
||||||
seedData,
|
|
||||||
}) => {
|
|
||||||
const session = await openWideTask(testPage, apiClient, seedData, "Edge rapid resize");
|
|
||||||
// Five quick resizes; the final value should win.
|
|
||||||
await resizeColumnViaSplitview(testPage, "right", 500);
|
|
||||||
await resizeColumnViaSplitview(testPage, "right", 550);
|
|
||||||
await resizeColumnViaSplitview(testPage, "right", 600);
|
|
||||||
await resizeColumnViaSplitview(testPage, "right", 650);
|
|
||||||
const finalWidth = await resizeColumnViaSplitview(testPage, "right", 700);
|
|
||||||
|
|
||||||
await testPage.reload();
|
|
||||||
await session.waitForLoad();
|
|
||||||
await session.waitForDockviewReady();
|
|
||||||
|
|
||||||
const afterReload = await getDockviewGroupWidth(testPage, "files");
|
|
||||||
expectApproxWidth(afterReload, finalWidth, 15);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resize during maximize does not corrupt the pre-maximize width", async ({
|
test("resize during maximize does not corrupt the pre-maximize width", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { test, expect } from "../../fixtures/test-base";
|
import { test, expect } from "../../fixtures/test-base";
|
||||||
import { SessionPage } from "../../pages/session-page";
|
|
||||||
import {
|
import {
|
||||||
WIDE_VIEWPORT,
|
WIDE_VIEWPORT,
|
||||||
openWideTask,
|
openWideTask,
|
||||||
expectApproxWidth,
|
expectApproxWidth,
|
||||||
getDockviewGroupWidth,
|
getDockviewGroupWidth,
|
||||||
readPinnedDefaultsFromStorage,
|
|
||||||
resizeColumnViaSplitview,
|
resizeColumnViaSplitview,
|
||||||
} from "../../helpers/dockview-resize";
|
} from "../../helpers/dockview-resize";
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ test.describe("Right pane resize — viewport-proportional cap", () => {
|
|||||||
expect(actual).toBeLessThanOrEqual(cap + 10);
|
expect(actual).toBeLessThanOrEqual(cap + 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user width survives reload (sessionStorage round-trip)", async ({
|
test("user width survives reload (per-env sessionStorage round-trip)", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
seedData,
|
seedData,
|
||||||
@@ -43,47 +41,6 @@ test.describe("Right pane resize — viewport-proportional cap", () => {
|
|||||||
expectApproxWidth(after, before, 12);
|
expectApproxWidth(after, before, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user width persists into sessionStorage pinned-defaults slot", async ({
|
|
||||||
testPage,
|
|
||||||
apiClient,
|
|
||||||
seedData,
|
|
||||||
}) => {
|
|
||||||
await openWideTask(testPage, apiClient, seedData, "Right pinned defaults");
|
|
||||||
const live = await resizeColumnViaSplitview(testPage, "right", 650);
|
|
||||||
const defaults = await readPinnedDefaultsFromStorage(testPage);
|
|
||||||
expect(defaults.right).toBeDefined();
|
|
||||||
expectApproxWidth(defaults.right ?? 0, live, 12);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("new task adopts user's preferred right width", async ({
|
|
||||||
testPage,
|
|
||||||
apiClient,
|
|
||||||
seedData,
|
|
||||||
}) => {
|
|
||||||
const session = await openWideTask(testPage, apiClient, seedData, "Right propagate A");
|
|
||||||
const widthA = await resizeColumnViaSplitview(testPage, "right", 620);
|
|
||||||
void session;
|
|
||||||
|
|
||||||
const taskB = await apiClient.createTaskWithAgent(
|
|
||||||
seedData.workspaceId,
|
|
||||||
"Right propagate B",
|
|
||||||
seedData.agentProfileId,
|
|
||||||
{
|
|
||||||
description: "/e2e:simple-message",
|
|
||||||
workflow_id: seedData.workflowId,
|
|
||||||
workflow_step_id: seedData.startStepId,
|
|
||||||
repository_ids: [seedData.repositoryId],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await testPage.goto(`/t/${taskB.id}`);
|
|
||||||
const sessionB = new SessionPage(testPage);
|
|
||||||
await sessionB.waitForLoad();
|
|
||||||
await sessionB.waitForDockviewReady();
|
|
||||||
|
|
||||||
const widthB = await getDockviewGroupWidth(testPage, "files");
|
|
||||||
expectApproxWidth(widthB, widthA, 20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("viewport shrink re-clamps an over-cap pinned width", async ({
|
test("viewport shrink re-clamps an over-cap pinned width", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { type Page } from "@playwright/test";
|
|
||||||
import { test, expect } from "../../fixtures/test-base";
|
import { test, expect } from "../../fixtures/test-base";
|
||||||
import type { SeedData } from "../../fixtures/test-base";
|
|
||||||
import type { ApiClient } from "../../helpers/api-client";
|
|
||||||
import { SessionPage } from "../../pages/session-page";
|
|
||||||
import {
|
import {
|
||||||
openWideTask,
|
openWideTask,
|
||||||
expectApproxWidth,
|
expectApproxWidth,
|
||||||
@@ -10,32 +6,6 @@ import {
|
|||||||
resizeColumnViaSplitview,
|
resizeColumnViaSplitview,
|
||||||
} from "../../helpers/dockview-resize";
|
} from "../../helpers/dockview-resize";
|
||||||
|
|
||||||
async function createTaskAt(
|
|
||||||
page: Page,
|
|
||||||
apiClient: ApiClient,
|
|
||||||
seedData: SeedData,
|
|
||||||
viewport: { width: number; height: number },
|
|
||||||
title: string,
|
|
||||||
): Promise<SessionPage> {
|
|
||||||
await page.setViewportSize(viewport);
|
|
||||||
const task = await apiClient.createTaskWithAgent(
|
|
||||||
seedData.workspaceId,
|
|
||||||
title,
|
|
||||||
seedData.agentProfileId,
|
|
||||||
{
|
|
||||||
description: "/e2e:simple-message",
|
|
||||||
workflow_id: seedData.workflowId,
|
|
||||||
workflow_step_id: seedData.startStepId,
|
|
||||||
repository_ids: [seedData.repositoryId],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await page.goto(`/t/${task.id}`);
|
|
||||||
const session = new SessionPage(page);
|
|
||||||
await session.waitForLoad();
|
|
||||||
await session.waitForDockviewReady();
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe("Sidebar resize — viewport-proportional cap", () => {
|
test.describe("Sidebar resize — viewport-proportional cap", () => {
|
||||||
test("resizes past the old 350px hard cap", async ({ testPage, apiClient, seedData }) => {
|
test("resizes past the old 350px hard cap", async ({ testPage, apiClient, seedData }) => {
|
||||||
await openWideTask(testPage, apiClient, seedData, "Sidebar past cap");
|
await openWideTask(testPage, apiClient, seedData, "Sidebar past cap");
|
||||||
@@ -48,13 +18,10 @@ test.describe("Sidebar resize — viewport-proportional cap", () => {
|
|||||||
apiClient,
|
apiClient,
|
||||||
seedData,
|
seedData,
|
||||||
}) => {
|
}) => {
|
||||||
await createTaskAt(
|
await openWideTask(testPage, apiClient, seedData, "Sidebar cap narrow", {
|
||||||
testPage,
|
width: 1200,
|
||||||
apiClient,
|
height: 800,
|
||||||
seedData,
|
});
|
||||||
{ width: 1200, height: 800 },
|
|
||||||
"Sidebar cap narrow",
|
|
||||||
);
|
|
||||||
const actual = await resizeColumnViaSplitview(testPage, "sidebar", 5000);
|
const actual = await resizeColumnViaSplitview(testPage, "sidebar", 5000);
|
||||||
expect(actual).toBeLessThanOrEqual(Math.max(350, Math.round(1200 * 0.3)) + 10);
|
expect(actual).toBeLessThanOrEqual(Math.max(350, Math.round(1200 * 0.3)) + 10);
|
||||||
});
|
});
|
||||||
@@ -71,35 +38,6 @@ test.describe("Sidebar resize — viewport-proportional cap", () => {
|
|||||||
expectApproxWidth(after, before, 12);
|
expectApproxWidth(after, before, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user width propagates to brand-new task envs", async ({
|
|
||||||
testPage,
|
|
||||||
apiClient,
|
|
||||||
seedData,
|
|
||||||
}) => {
|
|
||||||
const session = await openWideTask(testPage, apiClient, seedData, "Sidebar propagate A");
|
|
||||||
const widthA = await resizeColumnViaSplitview(testPage, "sidebar", 450);
|
|
||||||
void session;
|
|
||||||
|
|
||||||
const taskB = await apiClient.createTaskWithAgent(
|
|
||||||
seedData.workspaceId,
|
|
||||||
"Sidebar propagate B",
|
|
||||||
seedData.agentProfileId,
|
|
||||||
{
|
|
||||||
description: "/e2e:simple-message",
|
|
||||||
workflow_id: seedData.workflowId,
|
|
||||||
workflow_step_id: seedData.startStepId,
|
|
||||||
repository_ids: [seedData.repositoryId],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await testPage.goto(`/t/${taskB.id}`);
|
|
||||||
const sessionB = new SessionPage(testPage);
|
|
||||||
await sessionB.waitForLoad();
|
|
||||||
await sessionB.waitForDockviewReady();
|
|
||||||
|
|
||||||
const widthB = await getDockviewGroupWidth(testPage, "sidebar");
|
|
||||||
expectApproxWidth(widthB, widthA, 20);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("toggle sidebar off+on preserves the last user width", async ({
|
test("toggle sidebar off+on preserves the last user width", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
|
|||||||
@@ -332,44 +332,6 @@ export function removeEnvMaximizeState(envId: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Dockview pinned-column user defaults (sessionStorage) ---
|
|
||||||
// Stores the user's preferred sidebar / right widths so brand-new task envs
|
|
||||||
// adopt them instead of falling back to the ratio-based default. Per-env saved
|
|
||||||
// layouts still win when present (this only seeds the in-memory pinnedWidths
|
|
||||||
// map for envs with no saved layout yet).
|
|
||||||
const DOCKVIEW_PINNED_DEFAULTS_KEY = "kandev.dockview.pinned-defaults";
|
|
||||||
|
|
||||||
export type PinnedDefaults = { sidebar?: number; right?: number };
|
|
||||||
|
|
||||||
function isPinnedDefaults(value: unknown): value is PinnedDefaults {
|
|
||||||
if (!value || typeof value !== "object") return false;
|
|
||||||
const v = value as Record<string, unknown>;
|
|
||||||
const okSidebar = v.sidebar === undefined || typeof v.sidebar === "number";
|
|
||||||
const okRight = v.right === undefined || typeof v.right === "number";
|
|
||||||
return okSidebar && okRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPinnedDefaults(): PinnedDefaults {
|
|
||||||
if (typeof window === "undefined") return {};
|
|
||||||
try {
|
|
||||||
const raw = window.sessionStorage.getItem(DOCKVIEW_PINNED_DEFAULTS_KEY);
|
|
||||||
if (!raw) return {};
|
|
||||||
const parsed: unknown = JSON.parse(raw);
|
|
||||||
return isPinnedDefaults(parsed) ? parsed : {};
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setPinnedDefaults(defaults: PinnedDefaults): void {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(DOCKVIEW_PINNED_DEFAULTS_KEY, JSON.stringify(defaults));
|
|
||||||
} catch {
|
|
||||||
// Ignore write failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PR panel "offered" flag — tracks whether the auto-show PR panel was offered
|
// PR panel "offered" flag — tracks whether the auto-show PR panel was offered
|
||||||
// for a session. If offered and then closed by the user, we respect the dismissal.
|
// for a session. If offered and then closed by the user, we respect the dismissal.
|
||||||
const PR_PANEL_OFFERED_PREFIX = "kandev.pr-panel-offered.";
|
const PR_PANEL_OFFERED_PREFIX = "kandev.pr-panel-offered.";
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
getEnvMaximizeState,
|
getEnvMaximizeState,
|
||||||
setEnvMaximizeState,
|
setEnvMaximizeState,
|
||||||
removeEnvMaximizeState,
|
removeEnvMaximizeState,
|
||||||
getPinnedDefaults,
|
|
||||||
setPinnedDefaults,
|
|
||||||
} from "@/lib/local-storage";
|
} from "@/lib/local-storage";
|
||||||
import { applyLayoutFixups, focusOrAddPanel } from "./dockview-layout-builders";
|
import { applyLayoutFixups, focusOrAddPanel } from "./dockview-layout-builders";
|
||||||
import {
|
import {
|
||||||
@@ -670,14 +668,7 @@ function performBuildDefault(
|
|||||||
): void {
|
): void {
|
||||||
const { userDefaultLayout } = get();
|
const { userDefaultLayout } = get();
|
||||||
const intent = intentName ? resolveNamedIntent(intentName) : null;
|
const intent = intentName ? resolveNamedIntent(intentName) : null;
|
||||||
// Seed pinned widths from the user's saved defaults so brand-new task envs
|
|
||||||
// open at the widths the user last resized to. getPinnedWidth still clamps
|
|
||||||
// them to the runtime cap (so a stored value beyond the new viewport cap
|
|
||||||
// shrinks gracefully).
|
|
||||||
const defaults = getPinnedDefaults();
|
|
||||||
const freshPinned = new Map<string, number>();
|
const freshPinned = new Map<string, number>();
|
||||||
if (defaults.sidebar !== undefined) freshPinned.set("sidebar", defaults.sidebar);
|
|
||||||
if (defaults.right !== undefined) freshPinned.set("right", defaults.right);
|
|
||||||
// Capture dimensions before layout change — api.width can become stale
|
// Capture dimensions before layout change — api.width can become stale
|
||||||
// after fromJSON inside applyLayout
|
// after fromJSON inside applyLayout
|
||||||
const { width: safeWidth, height: safeHeight } = measureDockviewContainer(api);
|
const { width: safeWidth, height: safeHeight } = measureDockviewContainer(api);
|
||||||
@@ -783,20 +774,6 @@ export const useDockviewStore = create<DockviewStore>((set, get) => ({
|
|||||||
m.set(columnId, width);
|
m.set(columnId, width);
|
||||||
return { pinnedWidths: m };
|
return { pinnedWidths: m };
|
||||||
});
|
});
|
||||||
// Mirror sidebar / right widths into sessionStorage so brand-new envs
|
|
||||||
// (no saved per-env layout yet) open at the user's preferred widths
|
|
||||||
// instead of the ratio-based default.
|
|
||||||
//
|
|
||||||
// Skip when the column is hidden — auto-sync infers the sidebar slot
|
|
||||||
// from grid index 0, which is the *center* column when the sidebar is
|
|
||||||
// hidden. Without this guard, a center-column resize would overwrite
|
|
||||||
// the user's stored sidebar width.
|
|
||||||
if (columnId !== "sidebar" && columnId !== "right") return;
|
|
||||||
const live = get();
|
|
||||||
const visible = columnId === "sidebar" ? live.sidebarVisible : live.rightPanelsVisible;
|
|
||||||
if (!visible) return;
|
|
||||||
const current = getPinnedDefaults();
|
|
||||||
setPinnedDefaults({ ...current, [columnId]: width });
|
|
||||||
},
|
},
|
||||||
userDefaultLayout: null,
|
userDefaultLayout: null,
|
||||||
setUserDefaultLayout: (layout) => set({ userDefaultLayout: layout }),
|
setUserDefaultLayout: (layout) => set({ userDefaultLayout: layout }),
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import type { LayoutPanel } from "./types";
|
|||||||
// Layout sizing constants (single source of truth).
|
// Layout sizing constants (single source of truth).
|
||||||
// Pinned column max width is computed at runtime — see ./caps.ts.
|
// Pinned column max width is computed at runtime — see ./caps.ts.
|
||||||
//
|
//
|
||||||
// Both ratios are currently 25% by design: the sidebar (file/task list) and
|
// Ratio applied to viewport width to compute the initial default for any
|
||||||
// the right pane (files + terminal) historically open at the same fraction of
|
// pinned column when no user override exists. Sidebar and right pane share
|
||||||
// the viewport. The constants stay separate so a future tweak can adjust one
|
// the same initial fraction today; if they need to diverge later, split this
|
||||||
// without silently changing the other — sizing.ts dispatches by column id.
|
// into per-column ratios at that time.
|
||||||
export const LAYOUT_SIDEBAR_RATIO = 2.5 / 10;
|
export const LAYOUT_INITIAL_RATIO = 2.5 / 10;
|
||||||
export const LAYOUT_RIGHT_RATIO = 2.5 / 10;
|
/** @deprecated use LAYOUT_INITIAL_RATIO. */
|
||||||
|
export const LAYOUT_SIDEBAR_RATIO = LAYOUT_INITIAL_RATIO;
|
||||||
|
/** @deprecated use LAYOUT_INITIAL_RATIO. */
|
||||||
|
export const LAYOUT_RIGHT_RATIO = LAYOUT_INITIAL_RATIO;
|
||||||
|
|
||||||
// Well-known group/panel IDs
|
// Well-known group/panel IDs
|
||||||
export const SIDEBAR_GROUP = "group-sidebar";
|
export const SIDEBAR_GROUP = "group-sidebar";
|
||||||
|
|||||||
Reference in New Issue
Block a user