From 9e56fb7b02bffea2ef909d560b2c66c558decc37 Mon Sep 17 00:00:00 2001 From: Yuan Tang Date: Fri, 7 Aug 2026 22:25:02 -0400 Subject: [PATCH 1/5] fix(web): hide fullscreen button when workspace has no changes The maximize/minimize toggle was shown even when the workspace panel displayed "No workspace changes yet," offering no useful action. Gate the button on having changed files, an open file tab, or an open terminal tab. Signed-off-by: Yuan Tang --- web/src/shell/WorkspacePanel.tsx | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/web/src/shell/WorkspacePanel.tsx b/web/src/shell/WorkspacePanel.tsx index faefe320e1..87b0d8dac8 100644 --- a/web/src/shell/WorkspacePanel.tsx +++ b/web/src/shell/WorkspacePanel.tsx @@ -865,21 +865,23 @@ 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. */} - - - + + + )} {/* Tab content — single slot. An open shell tab holds its xterm; a file tab holds FileViewer; the Files tab shows FilesPanel; the From 3804acb98c7391192d2236bfd4c419a0722d539e Mon Sep 17 00:00:00 2001 From: Yuan Tang Date: Fri, 7 Aug 2026 22:43:46 -0400 Subject: [PATCH 2/5] style(web): fix Prettier formatting in WorkspacePanel Signed-off-by: Yuan Tang --- web/src/shell/WorkspacePanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/shell/WorkspacePanel.tsx b/web/src/shell/WorkspacePanel.tsx index 87b0d8dac8..4b6db8f2f0 100644 --- a/web/src/shell/WorkspacePanel.tsx +++ b/web/src/shell/WorkspacePanel.tsx @@ -878,7 +878,11 @@ export function WorkspacePanel({ 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 ? : } + {maximized ? ( + + ) : ( + + )} )} From f8eb584ce1b691b3b1e79a7caaadcf52a883a886 Mon Sep 17 00:00:00 2001 From: Yuan Tang Date: Fri, 7 Aug 2026 22:51:50 -0400 Subject: [PATCH 3/5] test(web): update WorkspacePanel tests for conditional fullscreen button Pass changedCount: 1 in tests that assert the fullscreen button is present, and add a test verifying the button is hidden when the workspace has no changes and no open tabs. Signed-off-by: Yuan Tang --- web/src/shell/WorkspacePanel.test.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/web/src/shell/WorkspacePanel.test.tsx b/web/src/shell/WorkspacePanel.test.tsx index 8676b417ee..af9019226e 100644 --- a/web/src/shell/WorkspacePanel.test.tsx +++ b/web/src/shell/WorkspacePanel.test.tsx @@ -87,6 +87,7 @@ function renderWorkspace( selectedTerminalKey?: string | null; maximized?: boolean; liveness?: SessionLiveness; + changedCount?: number; } = {}, ) { const openFileViewer = vi.fn(); @@ -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} @@ -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" }); @@ -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). @@ -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. @@ -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"); }); From 51df6208d510af12b7bedc05de565ead3ca31339 Mon Sep 17 00:00:00 2001 From: Yuan Tang Date: Fri, 7 Aug 2026 23:01:48 -0400 Subject: [PATCH 4/5] test(web): mock changed files in AppShell maximize tests The fullscreen button is now hidden when changedCount is 0, so the maximize tests need at least one changed file to render the button. Signed-off-by: Yuan Tang --- web/src/shell/AppShell.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/src/shell/AppShell.test.tsx b/web/src/shell/AppShell.test.tsx index 116d696cd2..d5f2049c6d 100644 --- a/web/src/shell/AppShell.test.tsx +++ b/web/src/shell/AppShell.test.tsx @@ -1161,6 +1161,9 @@ describe("Workspace rail maximize", () => { data: { available: true, root: null }, isLoading: false, } as unknown as ReturnType); + useChangedFilesMock.mockReturnValue({ + data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + } as unknown as ReturnType); mockConversations([{ id: "conv_abc", permission_level: null }]); renderShell("/c/conv_abc"); @@ -1195,6 +1198,9 @@ describe("Workspace rail maximize", () => { data: { available: true, root: null }, isLoading: false, } as unknown as ReturnType); + useChangedFilesMock.mockReturnValue({ + data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + } as unknown as ReturnType); mockConversations([{ id: "conv_abc", permission_level: null }]); renderShell("/c/conv_abc"); @@ -1218,6 +1224,9 @@ describe("Workspace rail maximize", () => { data: { available: true, root: null }, isLoading: false, } as unknown as ReturnType); + useChangedFilesMock.mockReturnValue({ + data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + } as unknown as ReturnType); mockConversations([ { id: "conv_abc", permission_level: null }, { id: "conv_xyz", permission_level: null }, From ffff389b4b7dbcd33008ab1e4c1c67e37061c6f9 Mon Sep 17 00:00:00 2001 From: Yuan Tang Date: Fri, 7 Aug 2026 23:05:21 -0400 Subject: [PATCH 5/5] style(web): fix Prettier formatting in AppShell test Signed-off-by: Yuan Tang --- web/src/shell/AppShell.test.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/web/src/shell/AppShell.test.tsx b/web/src/shell/AppShell.test.tsx index d5f2049c6d..f07f857162 100644 --- a/web/src/shell/AppShell.test.tsx +++ b/web/src/shell/AppShell.test.tsx @@ -1162,7 +1162,10 @@ describe("Workspace rail maximize", () => { isLoading: false, } as unknown as ReturnType); useChangedFilesMock.mockReturnValue({ - data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + data: { + available: true, + data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }], + }, } as unknown as ReturnType); mockConversations([{ id: "conv_abc", permission_level: null }]); @@ -1199,7 +1202,10 @@ describe("Workspace rail maximize", () => { isLoading: false, } as unknown as ReturnType); useChangedFilesMock.mockReturnValue({ - data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + data: { + available: true, + data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }], + }, } as unknown as ReturnType); mockConversations([{ id: "conv_abc", permission_level: null }]); @@ -1225,7 +1231,10 @@ describe("Workspace rail maximize", () => { isLoading: false, } as unknown as ReturnType); useChangedFilesMock.mockReturnValue({ - data: { available: true, data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }] }, + data: { + available: true, + data: [{ path: "a.ts", name: "a.ts", status: "modified" as const, bytes: 1 }], + }, } as unknown as ReturnType); mockConversations([ { id: "conv_abc", permission_level: null },