mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-30 01:46:01 +00:00
fix(frontend): keep panel open after reversed drag (#4566)
* fix(frontend): keep panel open after reversed drag * test(frontend): model reversed panel drag cumulatively
This commit is contained in:
parent
4582a41347
commit
3c5e3d9de5
@ -116,7 +116,7 @@ Edit-and-rerun is deliberately latest-turn-only. `core/messages/utils.ts::getLat
|
||||
- `src/components/workspace/messages/message-list.tsx` owns human-input card answered/latest/pending gating; entry pages only translate a submitted card response into `sendMessage` calls.
|
||||
- `src/components/workspace/browser-view/browser-view-panel.tsx` forwards each physical pointer click as one `click` input; do not also emit `down`/`up` for the same gesture because the remote Playwright click would run twice.
|
||||
- `src/core/threads/hooks.ts` owns pre-submit upload state and thread submission.
|
||||
- `src/components/workspace/chats/chat-box.tsx` owns the desktop right-panel layout, and **all three** right panels (artifacts, sidecar, browser) share one `ResizablePanelGroup` — do not fork a non-resizable branch per panel kind, which is how the artifacts divider silently lost its drag handle (#4465). Open/close is `collapse()` / `resize()` on the side panel's imperative handle, not conditional rendering, so the width can animate. Three constraints hold that together: the size transition is applied from the group as `[&>[data-panel]]:transition-[flex-grow]` because the sized flex item is the library's own `[data-panel]` element rather than the child `className` lands on; it is applied only while an open/close is in flight, so a drag is not interpolated frame by frame; and during the animation the panel content is held at its final width in `cqw` and clipped, because a reflowing message list re-runs its scroll-to-bottom (pinned by `tests/e2e/sidecar-chat.spec.ts`'s no-animated-scroll test) and a re-wrapping composer changes which responsive labels it shows. Because the panel is `collapsible`, the library can also collapse it to `0%` on its own when a drag crosses `minSize`, without going through the state that owns it — so `onResize` must mirror that back into `sidecar` / `browserView` / `artifactsOpen`, otherwise the state still reads open and the trigger needs two clicks to bring the panel back.
|
||||
- `src/components/workspace/chats/chat-box.tsx` owns the desktop right-panel layout, and **all three** right panels (artifacts, sidecar, browser) share one `ResizablePanelGroup` — do not fork a non-resizable branch per panel kind, which is how the artifacts divider silently lost its drag handle (#4465). Open/close is `collapse()` / `resize()` on the side panel's imperative handle, not conditional rendering, so the width can animate. Three constraints hold that together: the size transition is applied from the group as `[&>[data-panel]]:transition-[flex-grow]` because the sized flex item is the library's own `[data-panel]` element rather than the child `className` lands on; it is applied only while an open/close is in flight, so a drag is not interpolated frame by frame; and during the animation the panel content is held at its final width in `cqw` and clipped, because a reflowing message list re-runs its scroll-to-bottom (pinned by `tests/e2e/sidecar-chat.spec.ts`'s no-animated-scroll test) and a re-wrapping composer changes which responsive labels it shows. Because the panel is `collapsible`, the library can also collapse it to `0%` on its own when a drag crosses `minSize`, without going through the state that owns it. `onResize` records the last positive size while the pointer moves, but the owning `sidecar` / `browserView` / `artifactsOpen` state must only mirror a final `0%` layout from `onLayoutChanged`, after pointer release; closing on the first `0%` resize frame breaks a continuous drag that reaches the edge and then reverses before release.
|
||||
|
||||
## Code Style
|
||||
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { FilesIcon, XIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type PanelSize, usePanelRef } from "react-resizable-panels";
|
||||
import {
|
||||
type Layout,
|
||||
type PanelSize,
|
||||
usePanelRef,
|
||||
} from "react-resizable-panels";
|
||||
|
||||
import { ConversationEmptyState } from "@/components/ai-elements/conversation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -150,19 +154,25 @@ const ChatBox: React.FC<{
|
||||
rightPanelOpen ? RIGHT_PANEL_DEFAULT_SIZE : "0%",
|
||||
);
|
||||
|
||||
const handleSidePanelResize = useCallback(
|
||||
(size: PanelSize) => {
|
||||
if (!rightPanelOpenRef.current) {
|
||||
return;
|
||||
}
|
||||
if (size.asPercentage > 0) {
|
||||
openSizeRef.current = `${size.asPercentage}%`;
|
||||
const handleSidePanelResize = useCallback((size: PanelSize) => {
|
||||
if (!rightPanelOpenRef.current || size.asPercentage <= 0) {
|
||||
return;
|
||||
}
|
||||
openSizeRef.current = `${size.asPercentage}%`;
|
||||
}, []);
|
||||
|
||||
const handlePanelGroupLayoutChanged = useCallback(
|
||||
(layout: Layout) => {
|
||||
if (
|
||||
!rightPanelOpenRef.current ||
|
||||
layout[`${resizableIdBase}-side`] !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dragging a collapsible panel below its minimum snaps it to 0% without
|
||||
// updating the state that owns the panel. Treat that as a normal close so
|
||||
// its trigger can reopen it at the last non-zero size.
|
||||
// Finalize a drag-collapse only after the pointer is released. Closing
|
||||
// from onResize at the first 0% frame would break a continuous gesture
|
||||
// that reaches the edge and then reverses before release.
|
||||
if (activeRightPanel === "sidecar") {
|
||||
sidecar?.close();
|
||||
} else if (activeRightPanel === "browser") {
|
||||
@ -171,7 +181,7 @@ const ChatBox: React.FC<{
|
||||
setArtifactsOpen(false);
|
||||
}
|
||||
},
|
||||
[activeRightPanel, browserView, setArtifactsOpen, sidecar],
|
||||
[activeRightPanel, browserView, resizableIdBase, setArtifactsOpen, sidecar],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -350,6 +360,7 @@ const ChatBox: React.FC<{
|
||||
<ResizablePanelGroup
|
||||
id={`${resizableIdBase}-group`}
|
||||
orientation="horizontal"
|
||||
onLayoutChanged={handlePanelGroupLayoutChanged}
|
||||
className={cn(
|
||||
"[container-type:inline-size] size-full min-h-0",
|
||||
// The sized flex item is the library's own `[data-panel]` element, not
|
||||
|
||||
@ -44,7 +44,7 @@ async function panelWidth(panel: Locator): Promise<number> {
|
||||
return box?.width ?? 0;
|
||||
}
|
||||
|
||||
async function dragPanel(handle: Locator, distance: number): Promise<void> {
|
||||
async function dragPanel(handle: Locator, ...deltas: number[]): Promise<void> {
|
||||
// hover() waits for a stable bounding box: the open animation moves the
|
||||
// 1px-wide divider, so coordinates read any earlier miss it entirely.
|
||||
await handle.hover();
|
||||
@ -58,7 +58,11 @@ async function dragPanel(handle: Locator, distance: number): Promise<void> {
|
||||
const mouse = handle.page().mouse;
|
||||
await mouse.down();
|
||||
await expect(handle).toHaveAttribute("data-separator", "active");
|
||||
await mouse.move(x + distance, y, { steps: 10 });
|
||||
let currentX = x;
|
||||
for (const delta of deltas) {
|
||||
currentX += delta;
|
||||
await mouse.move(currentX, y, { steps: 10 });
|
||||
}
|
||||
await mouse.up();
|
||||
}
|
||||
|
||||
@ -135,6 +139,24 @@ test.describe("Artifacts panel resize", () => {
|
||||
.toBeGreaterThan(groupWidth * 0.19);
|
||||
});
|
||||
|
||||
test("reversing a collapse drag before release keeps the panel open", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openArtifact(page);
|
||||
|
||||
const artifactsPanel = page.locator("#artifacts");
|
||||
const handle = page.locator('[data-slot="resizable-handle"]');
|
||||
await expect(artifactsPanel.getByText("report.html")).toBeVisible();
|
||||
|
||||
// Cross the collapse threshold, then reverse the same drag before the
|
||||
// pointer is released. The final non-zero layout should remain open.
|
||||
await dragPanel(handle, 500, -500);
|
||||
|
||||
await expect(artifactsPanel).toHaveAttribute("aria-hidden", "false");
|
||||
await expect(artifactsPanel.getByText("report.html")).toBeVisible();
|
||||
await expect(handle).not.toHaveAttribute("data-separator", "disabled");
|
||||
});
|
||||
|
||||
test("a dragged width is kept when the panel is reopened", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user