Skip to content
Closed
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
18 changes: 18 additions & 0 deletions web/src/shell/AppShell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,12 @@ describe("Workspace rail maximize", () => {
data: { available: true, root: null },
isLoading: false,
} as unknown as ReturnType<typeof useWorkspaceEnvironment>);
useChangedFilesMock.mockReturnValue({
data: {
available: true,
data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }],
},
} as unknown as ReturnType<typeof useWorkspaceChangedFiles>);
mockConversations([{ id: "conv_abc", permission_level: null }]);

renderShell("/c/conv_abc");
Expand Down Expand Up @@ -1195,6 +1201,12 @@ describe("Workspace rail maximize", () => {
data: { available: true, root: null },
isLoading: false,
} as unknown as ReturnType<typeof useWorkspaceEnvironment>);
useChangedFilesMock.mockReturnValue({
data: {
available: true,
data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }],
},
} as unknown as ReturnType<typeof useWorkspaceChangedFiles>);
mockConversations([{ id: "conv_abc", permission_level: null }]);

renderShell("/c/conv_abc");
Expand All @@ -1218,6 +1230,12 @@ describe("Workspace rail maximize", () => {
data: { available: true, root: null },
isLoading: false,
} as unknown as ReturnType<typeof useWorkspaceEnvironment>);
useChangedFilesMock.mockReturnValue({
data: {
available: true,
data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }],
},
} as unknown as ReturnType<typeof useWorkspaceChangedFiles>);
mockConversations([
{ id: "conv_abc", permission_level: null },
{ id: "conv_xyz", permission_level: null },
Expand Down
19 changes: 13 additions & 6 deletions web/src/shell/WorkspacePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function renderWorkspace(
selectedTerminalKey?: string | null;
maximized?: boolean;
liveness?: SessionLiveness;
changedCount?: number;
} = {},
) {
const openFileViewer = vi.fn();
Expand All @@ -105,7 +106,7 @@ function renderWorkspace(
onRightRailTabChange={onRightRailTabChange}
showFilesPanel
showBrowserTab={overrides.showBrowserTab ?? false}
changedCount={0}
changedCount={overrides.changedCount ?? 0}
showShellsTab={overrides.showShellsTab ?? false}
terminalsLength={0}
subagentsWorking={0}
Expand Down Expand Up @@ -537,15 +538,21 @@ describe('WorkspacePanel "+" new-tab menu', () => {

describe("WorkspacePanel maximize", () => {
it("shows a full-screen toggle pinned to the right and fires onToggleMaximized", () => {
const { onToggleMaximized } = renderWorkspace();
const { onToggleMaximized } = renderWorkspace({ changedCount: 1 });

fireEvent.click(screen.getByRole("button", { name: "Full screen" }));

expect(onToggleMaximized).toHaveBeenCalledTimes(1);
});

it("hides the full-screen button when workspace has no changes and no open tabs", () => {
renderWorkspace({ changedCount: 0, openFiles: [] });

expect(screen.queryByRole("button", { name: "Full screen" })).toBeNull();
});

it("swaps to the exit-full-screen affordance and covers the content region when maximized", () => {
renderWorkspace({ maximized: true });
renderWorkspace({ maximized: true, changedCount: 1 });

// Label + pressed state flip so the icon reads as a minimize/exit control.
const toggle = screen.getByRole("button", { name: "Exit full screen" });
Expand All @@ -572,7 +579,7 @@ describe("WorkspacePanel tab-strip layout (regression)", () => {
// Two ml-auto siblings split the free space and strand the nav group
// mid-strip. With no open tabs the single ml-auto lives on the maximize
// button; the nav group must NOT also carry one.
renderWorkspace({ openFiles: [], showBrowserTab: true });
renderWorkspace({ openFiles: [], showBrowserTab: true, changedCount: 1 });

expect(strip().querySelectorAll(".ml-auto")).toHaveLength(1);
// It's the maximize button's wrapper (pins the button right).
Expand All @@ -586,7 +593,7 @@ describe("WorkspacePanel tab-strip layout (regression)", () => {
// The nav tabs stay anchored on the LEFT with open tabs — the open-tabs
// region renders to their right and the maximize button keeps the row's
// single ml-auto. Two ml-auto siblings would split the free space.
renderWorkspace({ openFiles: ["src/App.tsx"], showBrowserTab: true });
renderWorkspace({ openFiles: ["src/App.tsx"], showBrowserTab: true, changedCount: 1 });

expect(strip().querySelectorAll(".ml-auto")).toHaveLength(1);
// The nav tablist stays left — no ml-auto of its own.
Expand Down Expand Up @@ -620,7 +627,7 @@ describe("WorkspacePanel tab-strip layout (regression)", () => {
it("gives the full-screen button no left padding", () => {
// The maximize button must not carry a pl-* gap — it sits flush against the
// preceding nav icon like the rest of the strip.
renderWorkspace({ openFiles: [] });
renderWorkspace({ openFiles: [], changedCount: 1 });
const fullScreen = screen.getByRole("button", { name: "Full screen" });
expect(fullScreen.parentElement).not.toHaveClass("pl-0.5");
});
Expand Down
34 changes: 20 additions & 14 deletions web/src/shell/WorkspacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -865,21 +865,27 @@ export function WorkspacePanel({
{/* Maximize/minimize toggle, pinned to the rightmost edge via ml-auto,
which absorbs the free space before it. When open tabs exist their
≥500px flex-1 region absorbs the space instead, so the button still
hugs the right. */}
<WorkspaceTabTooltip
label={maximized ? "Exit full screen" : "Full screen"}
className="ml-auto"
>
<button
type="button"
aria-label={maximized ? "Exit full screen" : "Full screen"}
aria-pressed={maximized}
onClick={onToggleMaximized}
className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
hugs the right. Hidden when the panel has no meaningful content. */}
{(changedCount > 0 || selectedFilePath !== null || selectedTerminalKey !== null) && (
<WorkspaceTabTooltip
label={maximized ? "Exit full screen" : "Full screen"}
className="ml-auto"
>
{maximized ? <MinimizeIcon className="size-4" /> : <MaximizeIcon className="size-4" />}
</button>
</WorkspaceTabTooltip>
<button
type="button"
aria-label={maximized ? "Exit full screen" : "Full screen"}
aria-pressed={maximized}
onClick={onToggleMaximized}
className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{maximized ? (
<MinimizeIcon className="size-4" />
) : (
<MaximizeIcon className="size-4" />
)}
</button>
</WorkspaceTabTooltip>
)}
</div>
{/* Tab content — single slot. An open shell tab holds its xterm; a
file tab holds FileViewer; the Files tab shows FilesPanel; the
Expand Down
Loading