Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8895,6 +8895,7 @@ export default function ChatView(props: ChatViewProps) {
{rightPanelPresent && !shouldUseRightPanelSheet && activeThreadRef ? (
<RightPanelTabs
mode="inline"
widthStorageKey={`t3code:preview-panel-width:${activeThreadKey}`}
open={rightPanelOpen}
maximized={rightPanelMaximized}
surfaces={renderedRightPanelSurfaces}
Expand Down
59 changes: 55 additions & 4 deletions apps/web/src/hooks/useResizableWidth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const style = {
if (property === "user-select") this.userSelect = "";
},
};
const setItem = vi.fn();
const savedWidths = new Map<string, string>();
const setItem = vi.fn((key: string, value: string) => savedWidths.set(key, value));
const cancelAnimationFrame = vi.fn();
let events: EventTarget;
let frame: FrameRequestCallback | undefined;
Expand All @@ -40,9 +41,9 @@ function pointer(clientX = 100) {
} as unknown as PointerEvent<HTMLElement>;
}

function Panel() {
function Panel({ storageKey = "test-panel-width" }: { storageKey?: string }) {
const resize = useResizableWidth({
storageKey: "test-panel-width",
storageKey,
defaultWidth: 400,
minWidth: 200,
maxWidth: 800,
Expand All @@ -55,6 +56,7 @@ function Panel() {
}

beforeEach(async () => {
savedWidths.clear();
captured = false;
frame = undefined;
style.cursor = "";
Expand All @@ -64,7 +66,7 @@ beforeEach(async () => {
vi.stubGlobal("window", {
addEventListener: events.addEventListener.bind(events),
removeEventListener: events.removeEventListener.bind(events),
localStorage: { getItem: () => null, setItem },
localStorage: { getItem: (key: string) => savedWidths.get(key) ?? null, setItem },
});
vi.stubGlobal("document", { body: { style } });
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
Expand Down Expand Up @@ -125,3 +127,52 @@ describe("panel resize cleanup", () => {
expect(captured).toBe(false);
});
});

describe("panel width storage changes", () => {
it("restores separate thread widths without remounting and retains them after reload", async () => {
await act(() => {
result.handlers.onPointerDown(pointer());
result.handlers.onPointerMove(pointer(50));
result.handlers.onPointerUp(pointer(50));
});
expect(result.width).toBe(450);
await act(() => renderer.update(<Panel storageKey="thread-b" />));
expect(result.width).toBe(400);
await act(() => {
result.handlers.onPointerDown(pointer());
result.handlers.onPointerMove(pointer(-100));
result.handlers.onPointerUp(pointer(-100));
});
expect(result.width).toBe(600);
await act(() => renderer.update(<Panel />));
expect(result.width).toBe(450);
await act(() => renderer.unmount());
await act(() => {
renderer = create(<Panel storageKey="thread-b" />);
});
expect(result.width).toBe(600);
});

it("cancels an unfinished drag on a thread switch without saving it to either thread", async () => {
savedWidths.set("thread-b", "650");
await act(() => {
result.handlers.onPointerDown(pointer());
result.handlers.onPointerMove(pointer(50));
});
await act(() => frame?.(0));
expect(result.width).toBe(450);
await act(() => result.handlers.onPointerMove(pointer(25)));
await act(() => renderer.update(<Panel storageKey="thread-b" />));
expect(result.width).toBe(650);
expect(captured).toBe(false);
expect(style.cursor).toBe("");
await act(() => {
frame?.(0);
result.handlers.onPointerUp(pointer(25));
});
expect(result.width).toBe(650);
expect(setItem).not.toHaveBeenCalled();
await act(() => renderer.update(<Panel />));
expect(result.width).toBe(400);
});
});
24 changes: 15 additions & 9 deletions apps/web/src/hooks/useResizableWidth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface ResizableWidthHandlers {

/**
* Width state for a side-anchored panel resized via a drag handle on the
* specified edge. Width is read from localStorage on mount and persisted on
* specified edge. Width is read on mount or storage-key changes and persisted on
* drag-end (not on every rAF tick — would otherwise be ~60 writes/sec).
*
* The hook updates an internal `width` state during drag (so the panel
Expand All @@ -57,7 +57,7 @@ export function useResizableWidth(options: UseResizableWidthOptions): {
);

// No cross-tab subscription: panel width is per-window state.
const [width, setWidth] = useState<number>(() => {
const readWidth = () => {
if (typeof window === "undefined") return defaultWidth;
try {
const stored = getLocalStorageItem(storageKey, WidthSchema);
Expand All @@ -66,9 +66,15 @@ export function useResizableWidth(options: UseResizableWidthOptions): {
console.error("Could not read persisted panel width.", error);
return defaultWidth;
}
});
};
const [widthState, setWidthState] = useState(() => ({ storageKey, width: readWidth() }));
// The panel stays mounted across threads. Load the new thread's width before
// committing its first render, without remounting its preview or terminal.
if (widthState.storageKey !== storageKey) {
setWidthState({ storageKey, width: readWidth() });
}

const clampedWidth = clamp(width);
const clampedWidth = clamp(widthState.width);

const dragStateRef = useRef<{
pointerId: number;
Expand Down Expand Up @@ -102,8 +108,8 @@ export function useResizableWidth(options: UseResizableWidthOptions): {
const state = dragStateRef.current;
if (!state) return;
releasePointer(state.pointerId);
setWidth(state.startWidth);
}, [releasePointer]);
setWidthState({ storageKey, width: state.startWidth });
}, [releasePointer, storageKey]);

useEffect(() => {
window.addEventListener("blur", cancelDrag);
Expand Down Expand Up @@ -151,10 +157,10 @@ export function useResizableWidth(options: UseResizableWidthOptions): {
const active = dragStateRef.current;
if (!active) return;
active.rafId = null;
setWidth(active.pending);
setWidthState({ storageKey, width: active.pending });
});
},
[clamp, edge],
[clamp, edge, storageKey],
);

const onPointerUp = useCallback(
Expand All @@ -169,7 +175,7 @@ export function useResizableWidth(options: UseResizableWidthOptions): {
} catch (error) {
console.error("Could not persist panel width.", error);
}
setWidth(finalWidth);
setWidthState({ storageKey, width: finalWidth });
},
[clamp, releasePointer, storageKey],
);
Expand Down
Loading