Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #5894: Make it possible to collapse the right-hand side of the openhands screen #5896

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
35 changes: 35 additions & 0 deletions frontend/__tests__/components/workspace-toggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { ToggleWorkspaceIconButton } from "#/components/shared/buttons/toggle-workspace-icon-button";

describe("Workspace Toggle", () => {
it("should render toggle button with correct icon and label", () => {
const onClickMock = vi.fn();

// Test initial state (workspace visible)
const { rerender } = render(
<ToggleWorkspaceIconButton onClick={onClickMock} isHidden={false} />
);

const button = screen.getByTestId("toggle");
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute("aria-label", "Close workspace");

// Test hidden state
rerender(
<ToggleWorkspaceIconButton onClick={onClickMock} isHidden={true} />
);
expect(button).toHaveAttribute("aria-label", "Open workspace");
});

it("should call onClick handler when clicked", () => {
const onClickMock = vi.fn();
render(
<ToggleWorkspaceIconButton onClick={onClickMock} isHidden={false} />
);

const button = screen.getByTestId("toggle");
fireEvent.click(button);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { ToggleWorkspaceIconButton } from "../toggle-workspace-icon-button";

describe("ToggleWorkspaceIconButton", () => {
it("renders with correct dimensions and styling", () => {
const mockOnClick = vi.fn();
render(
<ToggleWorkspaceIconButton onClick={mockOnClick} isHidden={false} />,
);

const button = screen.getByTestId("toggle");
expect(button).toBeInTheDocument();
expect(button).toHaveClass("h-[100px] w-[20px]");
expect(button).toHaveClass("bg-neutral-800");
expect(button).toHaveClass("hover:bg-neutral-700");
expect(button).toHaveClass("rounded-md");
});

it("displays the correct icon based on isHidden prop", () => {
const mockOnClick = vi.fn();

const { rerender } = render(
<ToggleWorkspaceIconButton onClick={mockOnClick} isHidden={false} />,
);
expect(screen.getByLabelText("Close workspace")).toBeInTheDocument();
expect(screen.getByTestId("toggle")).toContainElement(
screen.getByTestId("arrow-forward-icon"),
);

rerender(<ToggleWorkspaceIconButton onClick={mockOnClick} isHidden />);
expect(screen.getByLabelText("Open workspace")).toBeInTheDocument();
expect(screen.getByTestId("toggle")).toContainElement(
screen.getByTestId("arrow-back-icon"),
);
});

it("remains visible when workspace is collapsed", () => {
const mockOnClick = vi.fn();
render(<ToggleWorkspaceIconButton onClick={mockOnClick} isHidden />);

const button = screen.getByTestId("toggle");
expect(button).toBeVisible();
});
});
10 changes: 6 additions & 4 deletions frontend/src/components/shared/buttons/icon-button.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { Button } from "@nextui-org/react";
import React, { MouseEventHandler, ReactElement } from "react";
import React, { ReactElement } from "react";

export interface IconButtonProps {
icon: ReactElement;
onClick: MouseEventHandler<HTMLButtonElement>;
onClick: () => void;
ariaLabel: string;
testId?: string;
className?: string;
}

export function IconButton({
icon,
onClick,
ariaLabel,
testId = "",
className = "",
}: IconButtonProps): React.ReactElement {
return (
<Button
type="button"
variant="flat"
onClick={onClick}
className="cursor-pointer text-[12px] bg-transparent aspect-square px-0 min-w-[20px] h-[20px]"
onPress={onClick}
className={`cursor-pointer text-[12px] bg-transparent aspect-square px-0 min-w-[12px] h-[20px] ${className}`}
aria-label={ariaLabel}
data-testid={testId}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ export function ToggleWorkspaceIconButton({
<IconButton
icon={
isHidden ? (
<IoIosArrowForward
size={20}
<IoIosArrowBack
size={10}
className="text-neutral-400 hover:text-neutral-100 transition"
data-testid="arrow-back-icon"
/>
) : (
<IoIosArrowBack
size={20}
<IoIosArrowForward
size={10}
className="text-neutral-400 hover:text-neutral-100 transition"
data-testid="arrow-forward-icon"
/>
)
}
testId="toggle"
ariaLabel={isHidden ? "Open workspace" : "Close workspace"}
title={isHidden ? "Open workspace" : "Close workspace"}
onClick={onClick}
className="h-[80px] w-[8px] bg-neutral-800 hover:bg-neutral-700 rounded-md"
/>
);
}
37 changes: 30 additions & 7 deletions frontend/src/routes/_oh.app/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Container } from "#/components/layout/container";
import Security from "#/components/shared/modals/security/security";
import { CountBadge } from "#/components/layout/count-badge";
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
import { ToggleWorkspaceIconButton } from "#/components/shared/buttons/toggle-workspace-icon-button";

function AppContent() {
const { gitHubToken } = useAuth();
Expand Down Expand Up @@ -62,6 +63,12 @@ function AppContent() {
dispatch(clearJupyter());
});

const [isWorkspaceHidden, setIsWorkspaceHidden] = React.useState(false);

const toggleWorkspace = React.useCallback(() => {
setIsWorkspaceHidden((prev) => !prev);
}, []);

const {
isOpen: securityModalIsOpen,
onOpen: onSecurityModalOpen,
Expand All @@ -71,15 +78,31 @@ function AppContent() {
return (
<WsClientProvider ghToken={gitHubToken} conversationId={conversationId}>
<EventHandler>
<div className="flex flex-col h-full gap-3">
<div className="flex h-full overflow-auto gap-3">
<Container className="w-full md:w-[390px] max-h-full relative">
<ChatInterface />
</Container>
<div className="flex flex-col h-full">
<div className="flex flex-grow overflow-hidden">
<div
className={`relative flex-grow mr-5 ${isWorkspaceHidden ? "w-full" : "md:w-[390px]"}`}
>
<Container className="h-full">
<ChatInterface />
</Container>
<div
className={`absolute top-1/2 -translate-y-1/2 ${isWorkspaceHidden ? "-right-4" : "-right-4"} z-10`}
>
<ToggleWorkspaceIconButton
onClick={toggleWorkspace}
isHidden={isWorkspaceHidden}
/>
</div>
</div>

<div className="hidden md:flex flex-col grow gap-3">
<div
className={`hidden md:flex flex-col flex-grow transition-all duration-300 ${
isWorkspaceHidden ? "w-0 opacity-0 overflow-hidden" : ""
}`}
>
<Container
className="h-2/3"
className="flex-grow"
labels={[
{ label: "Workspace", to: "", icon: <CodeIcon /> },
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]


[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
Expand Down Expand Up @@ -129,6 +130,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"


[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"
Expand Down