fix(workbench): Keep tool window resizing smooth with a long agent conversation (#133)

Dragging a tool window edge set an inherited custom property on the tool window
grid, so every pointer move restyled the whole workbench below it. With a long
agent conversation open each move cost 150-400 ms. The grid track variables are
now registered as non-inherited, and the side splitter sets flex-grow on its two
areas instead of inherited variables on the side.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-28 17:59:09 +08:00
1 джерело ecb3134460
коміт 08fd134bdb
3 files changed
+61 -23

No files matched your search

+5
Переглянути файл
@@ -279,8 +279,13 @@ describe("tool window split areas", () => {
loadToolWindowLayout("/split");
const { container, unmount } = render(<ToolWindowLayout editor={<div>editor</div>} />);
const splitter = container.querySelector('[data-split-handle="left"]')!;
const grow = (slot: string) => container.querySelector<HTMLElement>(`[data-slot="${slot}"]`)!.style.flexGrow;
expect(splitter).toHaveAttribute("hidden");
act(() => showToolWindow("project"));
// Alone, the area fills its side whatever the stored ratio.
expect(grow("left")).toBe("1");
act(() => showToolWindow("structure"));
expect(Number(grow("left")) + Number(grow("left-split"))).toBeCloseTo(1);
expect(screen.getByText("tree")).toBeVisible();
expect(screen.getByText("members")).toBeInTheDocument();
expect(splitter).not.toHaveAttribute("hidden");
+29 -7
Переглянути файл
@@ -12,6 +12,35 @@
.frameNoLeft {
padding-left: var(--island-gap);
}
/*
* Only the grid reads its track variables. Not inherited, so dragging a tool window
* edge restyles the grid alone instead of every node in the workbench below it.
*/
@property --tw-left {
syntax: "*";
inherits: false;
}
@property --tw-right {
syntax: "*";
inherits: false;
}
@property --tw-bottom {
syntax: "*";
inherits: false;
}
@property --gap-left {
syntax: "*";
inherits: false;
}
@property --gap-right {
syntax: "*";
inherits: false;
}
@property --gap-bottom {
syntax: "*";
inherits: false;
}
.grid {
flex: 1;
display: grid;
@@ -56,13 +85,6 @@
.sideRow {
flex-direction: row;
}
/* --split-a / --split-b: the stored ratio (SidePanel), moved directly while the splitter is dragged. */
.areaFirst {
flex: var(--split-a, 1) 1 0px;
}
.areaSecond {
flex: var(--split-b, 1) 1 0px;
}
.island[hidden] {
display: none;
}
+27 -16
Переглянути файл
@@ -696,24 +696,28 @@ const KEY_STEP = 10;
/**
* Splitter between the primary and split areas of a side: horizontal on the
* left and right (areas stacked), vertical at the bottom (side by side).
* Moves CSS variables while dragging; the ratio is stored on release.
* Moves the areas' flex-grow while dragging; the ratio is stored on release.
*/
function SplitHandle({ anchor, hidden }: { anchor: ToolWindowAnchor; hidden: boolean }) {
const stacked = anchor !== "bottom";
const measure = (el: HTMLElement) => {
const side = el.parentElement;
const first = el.previousElementSibling as HTMLElement | null;
const second = el.nextElementSibling as HTMLElement | null;
if (!side || !first || !second) return null;
if (!first || !second) return null;
const size = (e: HTMLElement) => (stacked ? e.offsetHeight : e.offsetWidth);
const total = size(first) + size(second);
if (total <= 0) return null;
const min = Math.min(stacked ? SPLIT_MIN.stacked : SPLIT_MIN.sideBySide, total / 2);
return { side, first: size(first), total, clamp: (px: number) => Math.min(total - min, Math.max(min, px)) };
return {
areas: [first, second] as const,
first: size(first),
total,
clamp: (px: number) => Math.min(total - min, Math.max(min, px)),
};
};
const apply = (side: HTMLElement, ratio: number) => {
side.style.setProperty("--split-a", String(ratio));
side.style.setProperty("--split-b", String(1 - ratio));
const apply = ([first, second]: readonly [HTMLElement, HTMLElement], ratio: number) => {
first.style.flexGrow = String(ratio);
second.style.flexGrow = String(1 - ratio);
};
const ratioOf = (px: number, total: number) => Math.min(MAX_SPLIT, Math.max(MIN_SPLIT, px / total));
@@ -729,7 +733,7 @@ function SplitHandle({ anchor, hidden }: { anchor: ToolWindowAnchor; hidden: boo
let ratio: number | null = null;
const move = (ev: globalThis.PointerEvent) => {
ratio = ratioOf(m.clamp(m.first + (stacked ? ev.clientY : ev.clientX) - origin), m.total);
apply(m.side, ratio);
apply(m.areas, ratio);
};
const end = () => {
el.removeEventListener("pointermove", move);
@@ -738,7 +742,7 @@ function SplitHandle({ anchor, hidden }: { anchor: ToolWindowAnchor; hidden: boo
document.body.style.cursor = cursor;
if (ratio !== null) setToolWindowSplit(anchor, ratio);
// Back to the store's value (unchanged when the ratio was clamped to it).
apply(m.side, useToolWindowLayout.getState().splits[anchor]);
apply(m.areas, useToolWindowLayout.getState().splits[anchor]);
};
el.addEventListener("pointermove", move);
el.addEventListener("pointerup", end);
@@ -782,25 +786,32 @@ function SidePanel({
visible: SlotRecord;
}) {
const mounted = useToolWindowLayout((s) => s.mounted);
const split = !!visible[anchor] && !!visible[`${anchor}-split`];
// A lone area takes the whole side: flex-grow below 1 would leave the rest empty.
const ratio = useToolWindowLayout((s) => s.splits[anchor]);
const area = (slot: ToolWindowSlot, className: string | undefined) => {
const area = (slot: ToolWindowSlot, grow: number) => {
const visibleId = visible[slot];
const list = windows.filter((w) => slotOf(w) === slot && (mounted.has(w.id) || w.id === visibleId));
if (!list.length) return null;
return (
<div className={clsx(styles.island, styles.anchor, className)} data-slot={slot} hidden={!visibleId}>
<div
className={clsx(styles.island, styles.anchor)}
// The stored ratio; SplitHandle moves it directly while dragging.
style={{ flexGrow: grow }}
data-slot={slot}
hidden={!visibleId}
>
{list.map((w) => (
<ToolWindowView key={w.id} tw={w} hidden={w.id !== visibleId} />
))}
</div>
);
};
const style = { "--split-a": ratio, "--split-b": 1 - ratio } as CSSProperties;
return (
<div className={clsx(styles.side, anchor === "bottom" && styles.sideRow)} data-anchor={anchor} style={style}>
{area(anchor, styles.areaFirst)}
<SplitHandle anchor={anchor} hidden={!visible[anchor] || !visible[`${anchor}-split`]} />
{area(`${anchor}-split`, styles.areaSecond)}
<div className={clsx(styles.side, anchor === "bottom" && styles.sideRow)} data-anchor={anchor}>
{area(anchor, split ? ratio : 1)}
<SplitHandle anchor={anchor} hidden={!split} />
{area(`${anchor}-split`, split ? 1 - ratio : 1)}
</div>
);
}