fix(web): cap clarification overlay height and add mock agent ask commands (#956)

This commit is contained in:
Carlos Florêncio
2026-05-19 22:27:24 +01:00
committed by GitHub
parent e32c6a252f
commit a74409af3c
9 changed files with 513 additions and 35 deletions
+6
View File
@@ -74,6 +74,12 @@ func handlePrompt(e *emitter, prompt, model string) {
rest := strings.TrimPrefix(cmd, "/e2e:")
scenarioName, _, _ := strings.Cut(strings.TrimSpace(rest), " ")
emitPredefinedScenario(e, scenarioName)
// Friendly aliases for the two clarification e2e scenarios so the slash menu
// exposes them without forcing users to type /e2e:clarification(-multi).
case strings.EqualFold(cmd, "/ask-single"):
emitPredefinedScenario(e, "clarification")
case strings.EqualFold(cmd, "/ask-multiple"):
emitPredefinedScenario(e, "clarification-multi")
case strings.EqualFold(cmd, "/crash"):
emitCrash(e, model)
case strings.HasPrefix(cmd, "/todo"):
+2
View File
@@ -266,6 +266,8 @@ func mockAvailableCommands() []acp.AvailableCommand {
{Name: "tool:search", Description: "Emit a search tool call"},
{Name: "markdown", Description: "Render all markdown/text types"},
{Name: "sleep", Description: "Sleep without output (default 10s)", Input: hint("seconds (e.g. 30)")},
{Name: "ask-single", Description: "Ask a single clarification question"},
{Name: "ask-multiple", Description: "Ask multiple clarification questions"},
}
}
@@ -12,6 +12,8 @@ import {
useChatPanelHandlers,
} from "@/components/task/chat/chat-input-area";
import { ClarificationInputOverlay } from "@/components/task/chat/clarification-input-overlay";
import { ResizeHandle } from "@/components/task/chat/resize-handle";
import { useResizableClarificationOverlay } from "@/hooks/use-resizable-clarification-overlay";
type QuickChatContentProps = {
sessionId: string;
@@ -73,6 +75,18 @@ export const QuickChatContent = memo(function QuickChatContent({
}, [initialPrompt, taskId, handleSubmit, onInitialPromptSent]);
const handleClarificationResolved = useCallback(() => setClarificationKey((k) => k + 1), []);
const {
height: clarificationHeight,
containerRef: clarificationContainerRef,
resetHeight: clarificationResetHeight,
resizeHandleProps: clarificationResizeProps,
} = useResizableClarificationOverlay();
// Reset the dragged height when the overlay closes so a fresh
// clarification starts auto-sized instead of inheriting a stale value.
useEffect(() => {
if (!pendingClarification) clarificationResetHeight();
}, [pendingClarification, clarificationResetHeight]);
return (
<div className="flex flex-col flex-1 min-h-0">
@@ -93,11 +107,19 @@ export const QuickChatContent = memo(function QuickChatContent({
/>
</div>
{pendingClarification && (
<div className="flex-shrink-0 border-t border-sky-400/30 bg-card px-1">
<ClarificationInputOverlay
messages={pendingClarificationGroup}
onResolved={handleClarificationResolved}
/>
<div className="relative flex-shrink-0 border-t border-sky-400/30 bg-card">
<ResizeHandle {...clarificationResizeProps} />
<div
ref={clarificationContainerRef}
data-testid="clarification-overlay-container"
className="px-1 overflow-y-auto overscroll-contain max-h-[50vh]"
style={clarificationHeight === null ? undefined : { height: clarificationHeight }}
>
<ClarificationInputOverlay
messages={pendingClarificationGroup}
onResolved={handleClarificationResolved}
/>
</div>
</div>
)}
<QueuedGhostList sessionId={panelState.resolvedSessionId} isArchived={false} />
@@ -90,22 +90,23 @@ function ClarificationCard(props: CardProps) {
data-testid="clarification-question-card"
data-question-id={meta.questionId}
data-question-index={String(index)}
className="px-4 pt-3 pb-4"
className="px-4 pt-1 pb-4"
>
<div className="flex items-center gap-2 mb-2 text-xs text-muted-foreground">
<IconMessageQuestion className="h-4 w-4 text-blue-500" />
{total > 1 && (
<span data-testid="clarification-progress-chip">
Question {index + 1} of {total}
</span>
)}
{metadata.question.title && (
<span className="text-muted-foreground/70">
{total > 1 ? "· " : ""}
{metadata.question.title}
</span>
)}
</div>
{(total > 1 || metadata.question.title) && (
<div className="flex items-center gap-2 mb-2 text-xs text-muted-foreground">
{total > 1 && (
<span data-testid="clarification-progress-chip">
Question {index + 1} of {total}
</span>
)}
{metadata.question.title && (
<span className="text-muted-foreground/70">
{total > 1 ? "· " : ""}
{metadata.question.title}
</span>
)}
</div>
)}
<div className="markdown-body max-w-none text-sm font-medium [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 mb-3">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{question.prompt}
@@ -453,8 +454,9 @@ export function ClarificationInputOverlay({
return (
<div className="relative" data-testid="clarification-overlay">
<div className="flex items-center justify-between gap-3 px-4 pt-3 pb-2">
<div className="flex items-center gap-3">
<div className="flex items-center justify-between gap-3 px-4 pt-2 pb-1">
<div className="flex items-center gap-3 min-w-0">
<IconMessageQuestion className="h-4 w-4 text-blue-500 flex-shrink-0" />
{total > 1 && (
<ClarificationStepper
total={total}
@@ -11,11 +11,13 @@ type ResizeHandleProps = {
};
function getHandleColor(planModeEnabled?: boolean, isAgentBusy?: boolean, isStarting?: boolean) {
if (isStarting) return "bg-primary/40 hover:bg-primary/70";
if (isAgentBusy && planModeEnabled) return "bg-violet-400/60 hover:bg-violet-400";
if (isAgentBusy) return "bg-primary/40 hover:bg-primary/70";
if (planModeEnabled) return "bg-violet-400/60 hover:bg-violet-400";
return "bg-border hover:bg-muted-foreground";
if (isStarting) return "bg-primary/70 hover:bg-primary";
if (isAgentBusy && planModeEnabled) return "bg-violet-400/80 hover:bg-violet-400";
if (isAgentBusy) return "bg-primary/70 hover:bg-primary";
if (planModeEnabled) return "bg-violet-400/80 hover:bg-violet-400";
// Resting state: muted-foreground (light gray on dark bg) at high opacity so
// the handle reads as a discoverable affordance, not invisible chrome.
return "bg-muted-foreground/60 hover:bg-muted-foreground";
}
export function ResizeHandle({
@@ -28,10 +30,11 @@ export function ResizeHandle({
return (
<button
type="button"
aria-label="Resize"
className={cn(
"absolute left-1/2 top-[-1px] -translate-x-1/2 -translate-y-1/2 z-10",
"w-12 h-2 cursor-ns-resize",
"flex items-center justify-center",
"w-16 h-3 cursor-ns-resize",
"flex items-center justify-center group",
)}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
@@ -39,7 +42,7 @@ export function ResizeHandle({
>
<div
className={cn(
"w-8 h-0.5 rounded-full transition-colors",
"w-12 h-1 rounded-full transition-all group-hover:h-1.5 group-hover:w-14",
getHandleColor(planModeEnabled, isAgentBusy, isStarting),
)}
/>
+27 -5
View File
@@ -10,6 +10,8 @@ import { useIsTaskArchived } from "./task-archived-context";
import { useChatPanelState } from "./chat/use-chat-panel-state";
import { ChatInputArea, useSubmitHandler, useChatPanelHandlers } from "./chat/chat-input-area";
import { ClarificationInputOverlay } from "./chat/clarification-input-overlay";
import { ResizeHandle } from "./chat/resize-handle";
import { useResizableClarificationOverlay } from "@/hooks/use-resizable-clarification-overlay";
import { PanelSearchBar } from "@/components/search/panel-search-bar";
import { SessionSearchHits } from "@/components/task/chat/session-search-hits";
import { usePanelSearch } from "@/hooks/use-panel-search";
@@ -157,6 +159,18 @@ export const TaskChatPanel = memo(function TaskChatPanel({
} = panelState;
const { handleCancelTurn } = useChatPanelHandlers(resolvedSessionId, clearQueue, chatInputRef);
const { clarificationKey, handleClarificationResolved } = useClarificationKey(agentMessageCount);
const {
height: clarificationHeight,
containerRef: clarificationContainerRef,
resetHeight: clarificationResetHeight,
resizeHandleProps: clarificationResizeProps,
} = useResizableClarificationOverlay();
// Reset the dragged height when the overlay closes so a fresh
// clarification starts auto-sized instead of inheriting a stale value.
useEffect(() => {
if (!pendingClarification) clarificationResetHeight();
}, [pendingClarification, clarificationResetHeight]);
const panelRef = useRef<HTMLDivElement>(null);
const { loadMore } = useLazyLoadMessages(resolvedSessionId);
@@ -216,11 +230,19 @@ export const TaskChatPanel = memo(function TaskChatPanel({
<SessionSearchOverlay search={search} agentLabel={agentLabel} agentName={agentName} />
</PanelBody>
{pendingClarification && !isArchived && (
<div className="flex-shrink-0 border-t border-sky-400/30 bg-card px-1">
<ClarificationInputOverlay
messages={pendingClarificationGroup}
onResolved={handleClarificationResolved}
/>
<div className="relative flex-shrink-0 border-t border-sky-400/30 bg-card">
<ResizeHandle {...clarificationResizeProps} />
<div
ref={clarificationContainerRef}
data-testid="clarification-overlay-container"
className="px-1 overflow-y-auto overscroll-contain max-h-[50vh]"
style={clarificationHeight === null ? undefined : { height: clarificationHeight }}
>
<ClarificationInputOverlay
messages={pendingClarificationGroup}
onResolved={handleClarificationResolved}
/>
</div>
</div>
)}
<QueuedGhostList sessionId={resolvedSessionId} isArchived={isArchived} />
@@ -0,0 +1,164 @@
import { type Page } from "@playwright/test";
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";
/**
* Helper to create a regular task, navigate to it, and wait for idle.
* Mirrors clarification.spec.ts's `seedClarificationTask`, but uses a non-
* blocking scenario so we reach the idle input and can drive the slash
* commands `/ask-single` and `/ask-multiple` from inside the session.
*/
async function seedTaskAndWaitForIdle(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<SessionPage> {
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],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle();
return session;
}
test.describe("Mock agent clarification slash commands", () => {
test.describe.configure({ retries: 1 });
// Smoke test that the /ask-single alias routes to the clarification scenario.
// The underlying clarification behaviour (option click, multi-question carousel,
// submit, etc.) is exhaustively covered by clarification.spec.ts — this only
// proves the alias is wired up.
test("/ask-single alias triggers the clarification overlay", async ({
testPage,
apiClient,
seedData,
}) => {
const session = await seedTaskAndWaitForIdle(testPage, apiClient, seedData, "Ask Single Alias");
await session.sendMessage("/ask-single");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
await session.clarificationOption("PostgreSQL").click();
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
});
test("/ask-multiple alias triggers the multi-question carousel", async ({
testPage,
apiClient,
seedData,
}) => {
const session = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Ask Multiple Alias",
);
await session.sendMessage("/ask-multiple");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
await expect(session.clarificationSteps()).toHaveCount(3);
await expect(session.clarificationOverlay()).toContainText("Which database");
await session.clarificationOption("PostgreSQL").click();
await session.clarificationOption("Go").click();
await session.clarificationOption("Docker").click();
await session.clarificationSubmit().click();
await expect(session.clarificationOverlay()).not.toBeVisible({ timeout: 30_000 });
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
});
});
test.describe("Clarification overlay resizable layout", () => {
test("starts content-sized, drag grows the overlay, double-click resets", async ({
testPage,
apiClient,
seedData,
}) => {
// Tall viewport so the content-sized overlay has plenty of room to grow
// without bumping into the 50vh safety cap on the first drag.
await testPage.setViewportSize({ width: 1280, height: 1000 });
const session = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Clarification Resize",
);
await session.sendMessage("/ask-multiple");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
const container = testPage.getByTestId("clarification-overlay-container");
await expect(container).toBeVisible();
const initial = await container.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
overflowY: computed.overflowY,
height: el.getBoundingClientRect().height,
// Inline style is what disambiguates "auto-sized" from "user-dragged".
inlineHeight: (el as HTMLElement).style.height,
};
});
expect(initial.overflowY).toBe("auto");
// Default state: no inline height → container sizes to its content.
expect(initial.inlineHeight).toBe("");
// Sanity check: content-sized overlay is at least tall enough for the
// question card but well under the 50vh cap.
expect(initial.height).toBeGreaterThan(200);
expect(initial.height).toBeLessThan(1000 * 0.5);
const handle = container.locator("xpath=..").locator("button[aria-label='Resize']");
await expect(handle).toBeVisible();
// Drag the handle upward by 120px → overlay should grow by ~120px.
const handleBox = await handle.boundingBox();
expect(handleBox).not.toBeNull();
const dragDistance = 120;
const startX = handleBox!.x + handleBox!.width / 2;
const startY = handleBox!.y + handleBox!.height / 2;
await testPage.mouse.move(startX, startY);
await testPage.mouse.down();
await testPage.mouse.move(startX, startY - dragDistance, { steps: 10 });
await testPage.mouse.up();
const afterDrag = await container.evaluate((el) => ({
height: el.getBoundingClientRect().height,
inlineHeight: (el as HTMLElement).style.height,
}));
// Drag flipped the container to an explicit pixel height.
expect(afterDrag.inlineHeight).toMatch(/^\d+(\.\d+)?px$/);
// Allow ±10px tolerance for rounding / mouse event coalescing.
expect(afterDrag.height).toBeGreaterThan(initial.height + dragDistance - 10);
// Double-click the handle → back to auto-sized.
await handle.dblclick();
const afterReset = await container.evaluate((el) => ({
height: el.getBoundingClientRect().height,
inlineHeight: (el as HTMLElement).style.height,
}));
expect(afterReset.inlineHeight).toBe("");
expect(Math.abs(afterReset.height - initial.height)).toBeLessThanOrEqual(2);
});
});
@@ -0,0 +1,172 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useResizableClarificationOverlay } from "./use-resizable-clarification-overlay";
const MIN_HEIGHT = 120;
function setInnerHeight(h: number) {
Object.defineProperty(window, "innerHeight", { configurable: true, value: h, writable: true });
}
// Stub getBoundingClientRect on the (detached) container ref so the hook's
// "measure current rendered height on drag start" branch has a value to read.
function attachContainerWithHeight(ref: React.RefObject<HTMLDivElement | null>, height: number) {
const el = document.createElement("div");
el.getBoundingClientRect = () => ({
height,
width: 800,
x: 0,
y: 0,
top: 0,
left: 0,
right: 800,
bottom: height,
toJSON: () => ({}),
});
(ref as { current: HTMLDivElement }).current = el;
}
function dispatchMouseMove(clientY: number) {
document.dispatchEvent(new MouseEvent("mousemove", { clientY, bubbles: true }));
}
function dispatchMouseUp() {
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
}
// Fake MouseEvent for handleMouseDown — only clientY and preventDefault are read.
function fakeReactMouseEvent(clientY: number) {
return { clientY, preventDefault: () => {} } as unknown as React.MouseEvent;
}
describe("useResizableClarificationOverlay", () => {
beforeEach(() => {
setInnerHeight(1000);
document.body.style.cursor = "";
document.body.style.userSelect = "";
});
afterEach(() => {
document.body.style.cursor = "";
document.body.style.userSelect = "";
});
it("starts with height=null (auto-sized) and exposes a containerRef + handle props", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
expect(result.current.height).toBeNull();
expect(result.current.containerRef.current).toBeNull();
expect(typeof result.current.resizeHandleProps.onMouseDown).toBe("function");
expect(typeof result.current.resizeHandleProps.onDoubleClick).toBe("function");
});
it("drag UP from a measured 300px grows height proportionally", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 300);
// Mouse down at y=500, drag up to y=400 (delta=100), expect height ≈ 400.
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(400));
expect(result.current.height).toBe(400);
});
it("clamps below MIN_HEIGHT when dragging down past it", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 200);
// Drag down (clientY grows) 500px → would target -300, must clamp to MIN_HEIGHT.
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(200)));
act(() => dispatchMouseMove(700));
expect(result.current.height).toBe(MIN_HEIGHT);
});
it("clamps above 50% of viewport when dragging up past it", () => {
setInnerHeight(800); // 50% = 400 cap
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 300);
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(0)); // delta=500 → 800 requested, must clamp to 400
expect(result.current.height).toBe(400);
});
it("does not update height after mouseup", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 250);
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(450)); // delta=50 → 300
expect(result.current.height).toBe(300);
act(() => dispatchMouseUp());
act(() => dispatchMouseMove(200)); // would target 550 if still dragging
expect(result.current.height).toBe(300);
});
it("sets document.body cursor/userSelect during drag and restores on mouseup", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 200);
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(400)));
expect(document.body.style.cursor).toBe("ns-resize");
expect(document.body.style.userSelect).toBe("none");
act(() => dispatchMouseUp());
expect(document.body.style.cursor).toBe("");
expect(document.body.style.userSelect).toBe("");
});
it("falls back to MIN_HEIGHT as the drag origin when the container ref is empty", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
// No containerRef attached → fallback path.
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(400)); // delta=100 → MIN_HEIGHT + 100 = 220
expect(result.current.height).toBe(MIN_HEIGHT + 100);
});
it("resetHeight and the handle's onDoubleClick both revert to null (auto)", () => {
const { result } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 250);
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(400));
act(() => dispatchMouseUp());
// startHeight=250 + delta=100 (500→400) = 350
expect(result.current.height).toBe(350);
act(() => result.current.resetHeight());
expect(result.current.height).toBeNull();
// Drag again, then double-click resets back to auto-sized.
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
act(() => dispatchMouseMove(380));
act(() => dispatchMouseUp());
expect(result.current.height).not.toBeNull();
act(() => result.current.resizeHandleProps.onDoubleClick());
expect(result.current.height).toBeNull();
});
it("cleans up body cursor and stops listening on unmount mid-drag", () => {
const { result, unmount } = renderHook(() => useResizableClarificationOverlay());
attachContainerWithHeight(result.current.containerRef, 250);
act(() => result.current.resizeHandleProps.onMouseDown(fakeReactMouseEvent(500)));
expect(document.body.style.cursor).toBe("ns-resize");
unmount();
// Unmount mid-drag must restore body styles.
expect(document.body.style.cursor).toBe("");
expect(document.body.style.userSelect).toBe("");
// Listeners removed: a synthetic mousemove must not throw / update anything.
expect(() => dispatchMouseMove(200)).not.toThrow();
expect(result.current.height).toBeNull();
});
});
@@ -0,0 +1,85 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
const MIN_HEIGHT = 120;
const MAX_VIEWPORT_RATIO = 0.5;
function viewportHeight(): number {
return typeof window === "undefined" ? 800 : window.innerHeight;
}
function clampHeight(value: number): number {
const max = Math.round(viewportHeight() * MAX_VIEWPORT_RATIO);
return Math.min(Math.max(value, MIN_HEIGHT), max);
}
/**
* Drives the user-resizable clarification overlay.
*
* Default behaviour: the container sizes to its content (`height === null` →
* caller omits the inline height style). Once the user drags the resize
* handle, the height switches to an explicit pixel value clamped between
* MIN_HEIGHT and 50% of the viewport. Double-clicking the handle returns to
* the auto-sized default.
*
* Callers MUST invoke `resetHeight()` when the overlay closes so a fresh
* clarification starts with auto-sized height instead of inheriting a
* dragged value.
*/
export function useResizableClarificationOverlay() {
// null = auto-size to content; number = user-driven pixel height.
const [height, setHeight] = useState<number | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const isDragging = useRef(false);
const startY = useRef(0);
const startHeight = useRef(0);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
// Capture the *currently rendered* height so the drag continues from
// wherever the overlay sits — whether it's auto-sized or already pinned.
const measured = containerRef.current?.getBoundingClientRect().height ?? MIN_HEIGHT;
isDragging.current = true;
startY.current = e.clientY;
startHeight.current = measured;
document.body.style.cursor = "ns-resize";
document.body.style.userSelect = "none";
}, []);
const resetHeight = useCallback(() => setHeight(null), []);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.current) return;
// Dragging the handle UP grows the overlay (it expands toward the top
// of the screen), so a smaller clientY → larger height.
const delta = startY.current - e.clientY;
setHeight(clampHeight(startHeight.current + delta));
};
const handleMouseUp = () => {
if (!isDragging.current) return;
isDragging.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (isDragging.current) {
isDragging.current = false;
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
};
}, []);
return {
height,
containerRef,
resetHeight,
resizeHandleProps: { onMouseDown: handleMouseDown, onDoubleClick: resetHeight },
};
}