diff --git a/apps/web/app/dockview-theme.css b/apps/web/app/dockview-theme.css index 03d7bb0ff4..0d840ec189 100644 --- a/apps/web/app/dockview-theme.css +++ b/apps/web/app/dockview-theme.css @@ -137,13 +137,6 @@ box-shadow: none !important; } -/* Per-group minimize: hide the content area while keeping the tab strip visible. - The splitview size is shrunk programmatically by useMinimizedGroupSync; this - rule ensures the body is invisible during the transition and at rest. */ -.dockview-theme-kandev .dv-groupview[data-kandev-minimized="true"] .dv-content-container { - display: none; -} - .dockview-theme-kandev .dv-content-container .session-panel-wrapper { border: none !important; border-radius: 0 !important; diff --git a/apps/web/components/task/dockview-desktop-layout.tsx b/apps/web/components/task/dockview-desktop-layout.tsx index d7252a4691..ed0765bce2 100644 --- a/apps/web/components/task/dockview-desktop-layout.tsx +++ b/apps/web/components/task/dockview-desktop-layout.tsx @@ -56,7 +56,6 @@ import { useCompactDockviewDefault, useDockviewUnmountCleanup, } from "./dockview-desktop-layout-hooks"; -import { useMinimizedGroupSync } from "./dockview-minimize-sync"; import { TerminalPanel } from "./terminal-panel"; import { BrowserPanel } from "./browser-panel"; import { VscodePanel } from "./vscode-panel"; @@ -499,8 +498,6 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({ useEditorKeybinds(); usePlanPanelAutoOpen(); useCompactDockviewDefault(compact); - const apiForMinimize = useDockviewStore((s) => s.api); - useMinimizedGroupSync(apiForMinimize); useEffect(() => { envIdRef.current = effectiveEnvId; diff --git a/apps/web/components/task/dockview-group-actions.test.tsx b/apps/web/components/task/dockview-group-actions.test.tsx index 66864238f5..9189497f2e 100644 --- a/apps/web/components/task/dockview-group-actions.test.tsx +++ b/apps/web/components/task/dockview-group-actions.test.tsx @@ -7,7 +7,6 @@ afterEach(() => cleanup()); const TID = { maximize: "dockview-maximize-btn", - minimize: "dockview-minimize-btn", splitRight: "dockview-split-right-btn", splitDown: "dockview-split-down-btn", close: "dockview-close-group-btn", @@ -18,8 +17,6 @@ const baseProps = { isChatGroup: false, isMaximized: false, onMaximize: () => {}, - isMinimized: false, - onMinimize: () => {}, onSplitRight: () => {}, onSplitDown: () => {}, onCloseGroup: () => {}, @@ -34,47 +31,44 @@ function renderView(width: number, override: Partial = {}) { } describe("GroupSplitCloseActionsView", () => { - it("wide width: renders 5 inline buttons and no overflow dropdown", () => { + it("wide width: renders 4 inline buttons and no overflow dropdown", () => { renderView(600); expect(screen.queryByTestId(TID.maximize)).not.toBeNull(); - expect(screen.queryByTestId(TID.minimize)).not.toBeNull(); expect(screen.queryByTestId(TID.splitRight)).not.toBeNull(); expect(screen.queryByTestId(TID.splitDown)).not.toBeNull(); expect(screen.queryByTestId(TID.close)).not.toBeNull(); expect(screen.queryByTestId(TID.menu)).toBeNull(); }); - it("narrow width: keeps Maximize inline, folds Minimize/split/close into dropdown", () => { + it("narrow width: keeps Maximize inline, collapses split/close into dropdown", () => { renderView(200); expect(screen.queryByTestId(TID.maximize)).not.toBeNull(); expect(screen.queryByTestId(TID.menu)).not.toBeNull(); - expect(screen.queryByTestId(TID.minimize)).toBeNull(); expect(screen.queryByTestId(TID.splitRight)).toBeNull(); expect(screen.queryByTestId(TID.splitDown)).toBeNull(); expect(screen.queryByTestId(TID.close)).toBeNull(); }); - it("narrow + chat group: no Close, no Minimize, dropdown still present for splits", () => { + it("narrow + chat group: no Close anywhere, dropdown still present for splits", () => { renderView(200, { isChatGroup: true }); expect(screen.queryByTestId(TID.maximize)).not.toBeNull(); expect(screen.queryByTestId(TID.menu)).not.toBeNull(); expect(screen.queryByTestId(TID.close)).toBeNull(); - expect(screen.queryByTestId(TID.minimize)).toBeNull(); }); - it("wide + chat group: Maximize + splits inline, no Close, no Minimize", () => { + it("wide + chat group: 3 inline buttons, no Close button", () => { renderView(600, { isChatGroup: true }); expect(screen.queryByTestId(TID.maximize)).not.toBeNull(); expect(screen.queryByTestId(TID.splitRight)).not.toBeNull(); expect(screen.queryByTestId(TID.splitDown)).not.toBeNull(); expect(screen.queryByTestId(TID.close)).toBeNull(); - expect(screen.queryByTestId(TID.minimize)).toBeNull(); expect(screen.queryByTestId(TID.menu)).toBeNull(); }); it("hysteresis: stays collapsed between 320 and 340 once collapsed", () => { const { rerender } = renderView(200); expect(screen.queryByTestId(TID.menu)).not.toBeNull(); + // Resize up to 330 — still inside hysteresis band, should remain collapsed rerender( @@ -82,6 +76,7 @@ describe("GroupSplitCloseActionsView", () => { ); expect(screen.queryByTestId(TID.menu)).not.toBeNull(); expect(screen.queryByTestId(TID.splitRight)).toBeNull(); + // Resize up to 360 — past expand threshold, switches back to inline rerender( @@ -108,37 +103,4 @@ describe("GroupSplitCloseActionsView", () => { screen.getByTestId(TID.maximize).click(); expect(onMaximize).toHaveBeenCalledTimes(2); }); - - it("Minimize button fires onMinimize in wide mode", () => { - const onMinimize = vi.fn(); - render( - - - , - ); - screen.getByTestId(TID.minimize).click(); - expect(onMinimize).toHaveBeenCalledTimes(1); - }); - - it("narrow mode renders Minimize as a dropdown menu item (not inline)", () => { - renderView(200); - expect(screen.queryByTestId(TID.minimize)).toBeNull(); - expect(screen.queryByTestId(TID.menu)).not.toBeNull(); - }); -}); - -describe("GroupSplitCloseActionsView — minimize icon swap", () => { - function minimizeBtnIconClass(): string | null { - return screen.getByTestId(TID.minimize).querySelector("svg")?.getAttribute("class") ?? null; - } - - it("renders IconMinus when not minimized", () => { - renderView(600, { isMinimized: false }); - expect(minimizeBtnIconClass()).toContain("tabler-icon-minus"); - }); - - it("renders IconWindowMaximize when minimized", () => { - renderView(600, { isMinimized: true }); - expect(minimizeBtnIconClass()).toContain("tabler-icon-window-maximize"); - }); }); diff --git a/apps/web/components/task/dockview-group-actions.tsx b/apps/web/components/task/dockview-group-actions.tsx index 578fc21fb1..cc54c8e525 100644 --- a/apps/web/components/task/dockview-group-actions.tsx +++ b/apps/web/components/task/dockview-group-actions.tsx @@ -8,8 +8,6 @@ import { IconDots, IconLayoutColumns, IconLayoutRows, - IconMinus, - IconWindowMaximize, IconX, } from "@tabler/icons-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; @@ -64,40 +62,6 @@ type SplitCloseHandlers = { onCloseGroup: () => void; }; -type MinimizeHandlers = { - isChatGroup: boolean; - isMinimized: boolean; - onMinimize: () => void; -}; - -function MinimizeButton({ - isMinimized, - onMinimize, -}: { - isMinimized: boolean; - onMinimize: () => void; -}) { - return ( - - - - - {isMinimized ? "Restore" : "Minimize"} - - ); -} - function MaximizeButton({ isMaximized, onMaximize, @@ -181,12 +145,10 @@ function InlineSplitClose({ function SplitCloseDropdown({ isChatGroup, - isMinimized, - onMinimize, onSplitRight, onSplitDown, onCloseGroup, -}: SplitCloseHandlers & MinimizeHandlers) { +}: SplitCloseHandlers) { return ( @@ -195,20 +157,6 @@ function SplitCloseDropdown({ - {!isChatGroup && ( - - {isMinimized ? ( - - ) : ( - - )} - {isMinimized ? "Restore" : "Minimize"} - - )} Split right @@ -232,38 +180,24 @@ type GroupSplitCloseActionsViewProps = SplitCloseHandlers & { width: number; isMaximized: boolean; onMaximize: () => void; - isMinimized: boolean; - onMinimize: () => void; }; -/** Presentational view: maximize stays inline; split/close collapse into a dropdown when narrow. - * Minimize sits between Maximize and the split/close cluster in wide mode and folds into the - * dropdown as its first item in narrow mode. Chat group never sees Minimize (same exclusion as Close). */ +/** Presentational view: maximize stays inline; split/close collapse into a dropdown when narrow. */ export function GroupSplitCloseActionsView({ width, isChatGroup, isMaximized, onMaximize, - isMinimized, - onMinimize, onSplitRight, onSplitDown, onCloseGroup, }: GroupSplitCloseActionsViewProps) { const collapsed = useCollapsedWithHysteresis(width); - const splitCloseHandlers = { isChatGroup, onSplitRight, onSplitDown, onCloseGroup }; - const minimizeHandlers = { isChatGroup, isMinimized, onMinimize }; + const handlers = { isChatGroup, onSplitRight, onSplitDown, onCloseGroup }; return ( <> - {!isChatGroup && !collapsed && ( - - )} - {collapsed ? ( - - ) : ( - - )} + {collapsed ? : } ); } diff --git a/apps/web/components/task/dockview-header-actions.tsx b/apps/web/components/task/dockview-header-actions.tsx index 33e151557e..3831c56710 100644 --- a/apps/web/components/task/dockview-header-actions.tsx +++ b/apps/web/components/task/dockview-header-actions.tsx @@ -168,16 +168,13 @@ export function LeftHeaderActions(props: IDockviewHeaderActionsProps) { ); } -/** Faded maximize, minimize, split, and close buttons for any non-sidebar group. */ +/** Faded maximize, split, and close buttons for any non-sidebar group. */ function GroupSplitCloseActions({ group, containerApi }: IDockviewHeaderActionsProps) { const centerGroupId = useDockviewStore((s) => s.centerGroupId); const isChatGroup = group.id === centerGroupId; const isMaximized = useDockviewStore((s) => s.preMaximizeLayout !== null); - const isMinimized = useDockviewStore((s) => s.minimizedGroupIds.has(group.id)); - const preMaximizeGroupId = useDockviewStore((s) => s.maximizedGroupId); const storeMaximize = useDockviewStore((s) => s.maximizeGroup); const storeExitMaximize = useDockviewStore((s) => s.exitMaximizedLayout); - const toggleGroupMinimized = useDockviewStore((s) => s.toggleGroupMinimized); const width = useDockviewGroupWidth(group); const handleMaximize = useCallback(() => { @@ -188,38 +185,6 @@ function GroupSplitCloseActions({ group, containerApi }: IDockviewHeaderActionsP } }, [group.id, isMaximized, storeMaximize, storeExitMaximize]); - const handleMinimize = useCallback(() => { - // Minimize and maximize are orthogonal — maximize is a global single-group - // overlay, minimize is per-group in-place collapse. Clicking Minimize while - // maximized would minimize the lone visible group and leave the user with a - // header-only screen; exit maximize first so they see the regular layout. - // - // During maximize, `group.id` is the ephemeral overlay group's ID — once - // exitMaximizedLayout rebuilds the original layout, that ID no longer - // exists in api.groups, and toggling minimize on it would silently no-op. - // Use the pre-maximize group ID (stored at maximize time) instead. - // - // exitMaximizedLayout schedules `api.layout(w, h)` in a requestAnimationFrame - // after rebuilding the layout; calling toggleGroupMinimized synchronously - // would race with that layout pass and the resulting setSize(0,0) could be - // overridden. Defer the toggle to the next frame so the maximize-exit - // settles first. - if (isMaximized) { - const target = preMaximizeGroupId ?? group.id; - storeExitMaximize(); - requestAnimationFrame(() => { - // The world may have changed across the frame (env switch, another - // maximize, the target group removed). Only toggle if the target still - // exists as a real group in the current dockview api. - const live = useDockviewStore.getState(); - const stillThere = live.api?.groups.some((g) => g.id === target) ?? false; - if (stillThere) live.toggleGroupMinimized(target); - }); - return; - } - toggleGroupMinimized(group.id); - }, [group.id, isMaximized, preMaximizeGroupId, storeExitMaximize, toggleGroupMinimized]); - const handleSplitRight = useCallback(() => { containerApi.addGroup({ referenceGroup: group, direction: "right" }); }, [group, containerApi]); @@ -253,8 +218,6 @@ function GroupSplitCloseActions({ group, containerApi }: IDockviewHeaderActionsP isChatGroup={isChatGroup} isMaximized={isMaximized} onMaximize={handleMaximize} - isMinimized={isMinimized} - onMinimize={handleMinimize} onSplitRight={handleSplitRight} onSplitDown={handleSplitDown} onCloseGroup={handleCloseGroup} diff --git a/apps/web/components/task/dockview-minimize-sync.test.ts b/apps/web/components/task/dockview-minimize-sync.test.ts deleted file mode 100644 index 953c1db578..0000000000 --- a/apps/web/components/task/dockview-minimize-sync.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { renderHook, act } from "@testing-library/react"; -import type { DockviewApi } from "dockview-react"; -import { useDockviewStore } from "@/lib/state/dockview-store"; -import { useMinimizedGroupSync } from "./dockview-minimize-sync"; - -const MINIMIZED_ATTR = "data-kandev-minimized"; - -type FakeListener = (event: T) => void; - -type FakeGroup = { - id: string; - element: HTMLElement; - api: { - width: number; - height: number; - setSize: ReturnType; - }; -}; - -type FakePanel = { group: FakeGroup }; - -type FakeApi = { - groups: FakeGroup[]; - emit: { - addGroup: (g: FakeGroup) => void; - removeGroup: (g: FakeGroup) => void; - addPanel: (panel: FakePanel) => void; - }; -}; - -function makeFakeApi(initialGroups: FakeGroup[] = []): { api: DockviewApi; fake: FakeApi } { - const addGroupListeners: Array> = []; - const removeGroupListeners: Array> = []; - const addPanelListeners: Array> = []; - - const api = { - groups: [...initialGroups], - onDidAddGroup: (cb: FakeListener) => { - addGroupListeners.push(cb); - return { dispose: () => addGroupListeners.splice(addGroupListeners.indexOf(cb), 1) }; - }, - onDidRemoveGroup: (cb: FakeListener) => { - removeGroupListeners.push(cb); - return { dispose: () => removeGroupListeners.splice(removeGroupListeners.indexOf(cb), 1) }; - }, - onDidAddPanel: (cb: FakeListener) => { - addPanelListeners.push(cb); - return { dispose: () => addPanelListeners.splice(addPanelListeners.indexOf(cb), 1) }; - }, - } as unknown as DockviewApi & { groups: FakeGroup[] }; - - const fake: FakeApi = { - groups: (api as unknown as { groups: FakeGroup[] }).groups, - emit: { - addGroup: (g) => { - fake.groups.push(g); - addGroupListeners.slice().forEach((cb) => cb(g)); - }, - removeGroup: (g) => { - const idx = fake.groups.indexOf(g); - if (idx >= 0) fake.groups.splice(idx, 1); - removeGroupListeners.slice().forEach((cb) => cb(g)); - }, - addPanel: (p) => addPanelListeners.slice().forEach((cb) => cb(p)), - }, - }; - - return { api: api as DockviewApi, fake }; -} - -function makeGroup(id: string, opts: { width?: number; height?: number } = {}): FakeGroup { - return { - id, - element: document.createElement("div"), - api: { - width: opts.width ?? 800, - height: opts.height ?? 600, - setSize: vi.fn(), - }, - }; -} - -describe("useMinimizedGroupSync", () => { - beforeEach(() => { - useDockviewStore.setState({ minimizedGroupIds: new Set() }); - }); - afterEach(() => { - useDockviewStore.setState({ minimizedGroupIds: new Set() }); - }); - - it("stamps data-kandev-minimized=false on every group at mount", () => { - const groupA = makeGroup("A"); - const groupB = makeGroup("B"); - const { api } = makeFakeApi([groupA, groupB]); - - renderHook(() => useMinimizedGroupSync(api)); - - expect(groupA.element.getAttribute(MINIMIZED_ATTR)).toBe("false"); - expect(groupB.element.getAttribute(MINIMIZED_ATTR)).toBe("false"); - }); - - it("does nothing when api is null", () => { - const { result } = renderHook(() => useMinimizedGroupSync(null)); - expect(result.current).toBeUndefined(); - }); - - it("on minimize: sets data-attribute=true, records prior size, calls setSize(0,0)", () => { - const groupA = makeGroup("A", { width: 800, height: 600 }); - const { api } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - - expect(groupA.element.getAttribute(MINIMIZED_ATTR)).toBe("true"); - expect(groupA.api.setSize).toHaveBeenCalledWith({ width: 0, height: 0 }); - }); - - it("skips a redundant setSize when sync re-fires while already minimized", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(groupA.api.setSize).toHaveBeenCalledTimes(1); - - // A subsequent sync (triggered here by an addGroup event) should not re-fire - // setSize on the already-minimized group. - act(() => { - fake.emit.addGroup(makeGroup("B")); - }); - expect(groupA.api.setSize).toHaveBeenCalledTimes(1); - }); - - it("on restore: replays the recorded prior size and resets data-attribute", () => { - const groupA = makeGroup("A", { width: 800, height: 600 }); - const { api } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(groupA.api.setSize).toHaveBeenLastCalledWith({ width: 0, height: 0 }); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(groupA.element.getAttribute(MINIMIZED_ATTR)).toBe("false"); - expect(groupA.api.setSize).toHaveBeenLastCalledWith({ width: 800, height: 600 }); - }); - - it("auto-restores a minimized group when a new panel is added to it", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(true); - - act(() => { - fake.emit.addPanel({ group: groupA }); - }); - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(false); - }); - - it("ignores panel-add events for groups that are not minimized", () => { - const groupA = makeGroup("A"); - const groupB = makeGroup("B"); - const { api, fake } = makeFakeApi([groupA, groupB]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - act(() => { - fake.emit.addPanel({ group: groupB }); - }); - - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(true); - }); -}); - -describe("useMinimizedGroupSync — group lifecycle", () => { - beforeEach(() => { - useDockviewStore.setState({ minimizedGroupIds: new Set() }); - }); - afterEach(() => { - useDockviewStore.setState({ minimizedGroupIds: new Set() }); - }); - - it("evicts a removed group from minimizedGroupIds", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(true); - - act(() => { - fake.emit.removeGroup(groupA); - }); - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(false); - }); - - it("clears recorded prior size when the group is removed (no stale restore on a re-added ID)", () => { - const groupA = makeGroup("A", { width: 800, height: 600 }); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - act(() => { - fake.emit.removeGroup(groupA); - }); - - // A new group with the same ID later — its size should NOT be reset to the - // old prior. We re-add and then minimize+restore; restore should use the - // new group's current width/height, not the deleted one. - const groupA2 = makeGroup("A", { width: 400, height: 300 }); - act(() => { - fake.emit.addGroup(groupA2); - }); - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(groupA2.api.setSize).toHaveBeenLastCalledWith({ width: 400, height: 300 }); - }); - - it("syncs newly added groups", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - const groupB = makeGroup("B"); - act(() => { - fake.emit.addGroup(groupB); - }); - - expect(groupB.element.getAttribute(MINIMIZED_ATTR)).toBe("false"); - }); - - it("does not rewrite data-attribute when unchanged (perf guard)", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - renderHook(() => useMinimizedGroupSync(api)); - - const setAttrSpy = vi.spyOn(groupA.element, "setAttribute"); - // Trigger a sync that wouldn't change the existing attribute. - act(() => { - fake.emit.addGroup(makeGroup("B")); - }); - expect(setAttrSpy).not.toHaveBeenCalledWith(MINIMIZED_ATTR, "false"); - }); - - it("disposes all subscribers on unmount", () => { - const groupA = makeGroup("A"); - const { api, fake } = makeFakeApi([groupA]); - const { unmount } = renderHook(() => useMinimizedGroupSync(api)); - - unmount(); - - // After unmount, store changes must not touch the group element or call setSize. - const setAttrSpy = vi.spyOn(groupA.element, "setAttribute"); - act(() => { - useDockviewStore.getState().toggleGroupMinimized("A"); - }); - expect(setAttrSpy).not.toHaveBeenCalled(); - expect(groupA.api.setSize).not.toHaveBeenCalled(); - - // And dockview events should no longer mutate the store. - act(() => { - fake.emit.removeGroup(groupA); - }); - // The store still has "A" because the onDidRemoveGroup listener was disposed. - expect(useDockviewStore.getState().minimizedGroupIds.has("A")).toBe(true); - }); -}); diff --git a/apps/web/components/task/dockview-minimize-sync.ts b/apps/web/components/task/dockview-minimize-sync.ts deleted file mode 100644 index 1d8ddefd8a..0000000000 --- a/apps/web/components/task/dockview-minimize-sync.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { DockviewApi } from "dockview-react"; -import { useDockviewStore } from "@/lib/state/dockview-store"; - -const MINIMIZED_ATTR = "data-kandev-minimized"; -/** Minimum splitview size we request when minimizing — the splitview clamps to the - * group's actual minimum (tab-strip height / minimum group width) so this can be 0. */ -const MIN_SIZE = 0; - -type PriorSize = { width: number; height: number }; - -/** Apply a single group's minimized state: data-attribute + splitview size shrink/restore. */ -function applyGroupMinimized( - api: DockviewApi, - groupId: string, - minimized: boolean, - priorSizes: Map, -): void { - const group = api.groups.find((g) => g.id === groupId); - if (!group) return; - const next = minimized ? "true" : "false"; - if (group.element.getAttribute(MINIMIZED_ATTR) !== next) { - group.element.setAttribute(MINIMIZED_ATTR, next); - } - if (minimized) { - // Skip the setSize when the group is already minimized (priorSizes already - // has the entry). Re-firing setSize on every sync would re-emit dockview's - // size-change events for no benefit. - if (priorSizes.has(groupId)) return; - priorSizes.set(groupId, { width: group.api.width, height: group.api.height }); - try { - group.api.setSize({ width: MIN_SIZE, height: MIN_SIZE }); - } catch { - /* setSize may throw if the group is being torn down */ - } - } else { - const prior = priorSizes.get(groupId); - if (prior) { - try { - group.api.setSize({ width: prior.width, height: prior.height }); - } catch { - /* same */ - } - priorSizes.delete(groupId); - } - } -} - -/** - * Keep the DOM and splitview sizes in sync with the store's `minimizedGroupIds`. - * - * - On set change: walk every group, set the `data-kandev-minimized` attribute, - * and call `group.api.setSize` to either shrink (clamps to splitview minimum) - * or restore the prior recorded size. - * - On `onDidAddPanel` into a minimized group: auto-restore (user is trying to - * add something they want to see). - * - On `onDidRemoveGroup`: evict the group from `minimizedGroupIds` and prior-sizes. - * - On `onDidAddGroup`: new groups default to not-minimized (no-op — they don't - * appear in the set). - */ -export function useMinimizedGroupSync(api: DockviewApi | null): void { - const priorSizesRef = useRef>(new Map()); - - useEffect(() => { - if (!api) return; - const priorSizes = priorSizesRef.current; - - const sync = () => { - const ids = useDockviewStore.getState().minimizedGroupIds; - for (const group of api.groups) { - applyGroupMinimized(api, group.id, ids.has(group.id), priorSizes); - } - }; - - sync(); - - const unsubscribe = useDockviewStore.subscribe((state, prev) => { - if (state.minimizedGroupIds === prev.minimizedGroupIds) return; - sync(); - }); - - const d1 = api.onDidAddGroup(() => sync()); - const d2 = api.onDidRemoveGroup((group) => { - const ids = useDockviewStore.getState().minimizedGroupIds; - priorSizes.delete(group.id); - if (ids.has(group.id)) { - const next = new Set(ids); - next.delete(group.id); - useDockviewStore.setState({ minimizedGroupIds: next }); - } - }); - const d3 = api.onDidAddPanel((panel) => { - const groupId = panel.group?.id; - if (!groupId) return; - const ids = useDockviewStore.getState().minimizedGroupIds; - if (!ids.has(groupId)) return; - const next = new Set(ids); - next.delete(groupId); - useDockviewStore.setState({ minimizedGroupIds: next }); - }); - - return () => { - unsubscribe(); - d1.dispose(); - d2.dispose(); - d3.dispose(); - }; - }, [api]); -} diff --git a/apps/web/lib/state/dockview-store.test.ts b/apps/web/lib/state/dockview-store.test.ts index fbb189176d..c01977ac08 100644 --- a/apps/web/lib/state/dockview-store.test.ts +++ b/apps/web/lib/state/dockview-store.test.ts @@ -91,52 +91,3 @@ describe("dockview-store resolveFilePath (via onDidActivePanelChange)", () => { expect(useDockviewStore.getState().activeFilePath).toBeNull(); }); }); - -describe("dockview-store minimize state", () => { - beforeEach(() => { - useDockviewStore.setState({ minimizedGroupIds: new Set() }); - }); - - it("toggleGroupMinimized adds then removes a group ID", () => { - const { toggleGroupMinimized } = useDockviewStore.getState(); - toggleGroupMinimized("g1"); - expect(useDockviewStore.getState().minimizedGroupIds.has("g1")).toBe(true); - toggleGroupMinimized("g1"); - expect(useDockviewStore.getState().minimizedGroupIds.has("g1")).toBe(false); - }); - - it("toggleGroupMinimized swaps the Set instance so selectors re-fire", () => { - const before = useDockviewStore.getState().minimizedGroupIds; - useDockviewStore.getState().toggleGroupMinimized("g1"); - const after = useDockviewStore.getState().minimizedGroupIds; - expect(after).not.toBe(before); - }); - - it("toggleGroupMinimized treats distinct IDs independently", () => { - const { toggleGroupMinimized } = useDockviewStore.getState(); - toggleGroupMinimized("g1"); - toggleGroupMinimized("g2"); - const ids = useDockviewStore.getState().minimizedGroupIds; - expect(ids.has("g1")).toBe(true); - expect(ids.has("g2")).toBe(true); - toggleGroupMinimized("g1"); - const ids2 = useDockviewStore.getState().minimizedGroupIds; - expect(ids2.has("g1")).toBe(false); - expect(ids2.has("g2")).toBe(true); - }); - - it("clearMinimizedGroups empties the Set", () => { - useDockviewStore.getState().toggleGroupMinimized("g1"); - useDockviewStore.getState().toggleGroupMinimized("g2"); - expect(useDockviewStore.getState().minimizedGroupIds.size).toBe(2); - useDockviewStore.getState().clearMinimizedGroups(); - expect(useDockviewStore.getState().minimizedGroupIds.size).toBe(0); - }); - - it("clearMinimizedGroups is a no-op when already empty (preserves identity)", () => { - const before = useDockviewStore.getState().minimizedGroupIds; - useDockviewStore.getState().clearMinimizedGroups(); - const after = useDockviewStore.getState().minimizedGroupIds; - expect(after).toBe(before); - }); -}); diff --git a/apps/web/lib/state/dockview-store.ts b/apps/web/lib/state/dockview-store.ts index 2411bbc20d..185d4fa92a 100644 --- a/apps/web/lib/state/dockview-store.ts +++ b/apps/web/lib/state/dockview-store.ts @@ -178,10 +178,6 @@ type DockviewStore = { maximizedGroupId: string | null; maximizeGroup: (groupId: string) => void; exitMaximizedLayout: () => void; - /** Per-group minimized state — content hidden, tab strip visible. Not persisted. */ - minimizedGroupIds: Set; - toggleGroupMinimized: (groupId: string) => void; - clearMinimizedGroups: () => void; }; type StoreGet = () => DockviewStore; @@ -391,7 +387,6 @@ function buildPresetActions(set: StoreSet, get: StoreGet) { sidebarVisible: true, rightPanelsVisible: preset === "default", pinnedWidths: cleanedWidths, - minimizedGroupIds: new Set(), }); requestAnimationFrame(() => { api.layout(safeWidth, safeHeight); @@ -422,11 +417,7 @@ function buildPresetActions(set: StoreSet, get: StoreGet) { const colCount = state?.columns?.length ?? api.groups.length; const sidebarCols = hasSidebar ? 1 : 0; const hasRight = colCount > sidebarCols + 1; - set({ - sidebarVisible: hasSidebar, - rightPanelsVisible: hasRight, - minimizedGroupIds: new Set(), - }); + set({ sidebarVisible: hasSidebar, rightPanelsVisible: hasRight }); requestAnimationFrame(() => { api.layout(safeWidth, safeHeight); syncPinnedWidthsFromApi(api, set); @@ -574,11 +565,7 @@ function buildEnvSwitchAction(set: StoreSet, get: StoreGet) { // outgoing env rather than silently skipping it. const effectiveOld = oldEnvId ?? currentLayoutEnvId; saveOutgoingEnv(api, effectiveOld, preMaximizeLayout, get().pinnedWidths); - set({ - preMaximizeLayout: null, - maximizedGroupId: null, - minimizedGroupIds: new Set(), - }); + set({ preMaximizeLayout: null, maximizedGroupId: null }); set({ isRestoringLayout: true, currentLayoutEnvId: newEnvId }); try { if (restoreMaximizeFromStorage(api, newEnvId, set)) return; @@ -602,26 +589,6 @@ function buildEnvSwitchAction(set: StoreSet, get: StoreGet) { }; } -function buildMinimizeActions(set: StoreSet, get: StoreGet) { - return { - toggleGroupMinimized: (groupId: string) => { - set((prev) => { - const next = new Set(prev.minimizedGroupIds); - if (next.has(groupId)) { - next.delete(groupId); - } else { - next.add(groupId); - } - return { minimizedGroupIds: next }; - }); - }, - clearMinimizedGroups: () => { - if (get().minimizedGroupIds.size === 0) return; - set({ minimizedGroupIds: new Set() }); - }, - }; -} - function buildMaximizeActions(set: StoreSet, get: StoreGet) { return { maximizeGroup: (groupId: string) => { @@ -656,12 +623,7 @@ function buildMaximizeActions(set: StoreSet, get: StoreGet) { groups: [{ panels: targetGroup.panels, activePanel: targetGroup.activePanel }], }); const maximizedLayout: LayoutState = { columns }; - set({ - isRestoringLayout: true, - preMaximizeLayout: current, - maximizedGroupId: groupId, - minimizedGroupIds: new Set(), - }); + set({ isRestoringLayout: true, preMaximizeLayout: current, maximizedGroupId: groupId }); const { width: safeWidth, height: safeHeight } = measureDockviewContainer(api); applyLayoutAndSet(api, maximizedLayout, liveWidths, set); requestAnimationFrame(() => { @@ -683,12 +645,7 @@ function buildMaximizeActions(set: StoreSet, get: StoreGet) { const safeWidth = measured.width; const safeHeight = measured.height; const liveWidths = get().pinnedWidths; - set({ - isRestoringLayout: true, - preMaximizeLayout: null, - maximizedGroupId: null, - minimizedGroupIds: new Set(), - }); + set({ isRestoringLayout: true, preMaximizeLayout: null, maximizedGroupId: null }); if (currentLayoutEnvId) { removeEnvMaximizeState(currentLayoutEnvId); } @@ -731,12 +688,7 @@ function performBuildDefault( const ids = applyLayout(api, state, freshPinned); const hasSidebar = state.columns.some((c) => c.id === "sidebar"); const hasRight = state.columns.length > (hasSidebar ? 2 : 1); - set({ - ...ids, - sidebarVisible: hasSidebar, - rightPanelsVisible: hasRight, - minimizedGroupIds: new Set(), - }); + set({ ...ids, sidebarVisible: hasSidebar, rightPanelsVisible: hasRight }); const pending = get().deferredPanelActions; if (pending.length > 0) { @@ -845,8 +797,6 @@ export const useDockviewStore = create((set, get) => ({ setPendingChatScrollTop: (value) => set({ pendingChatScrollTop: value }), preMaximizeLayout: null, maximizedGroupId: null, - minimizedGroupIds: new Set(), - ...buildMinimizeActions(set, get), ...buildMaximizeActions(set, get), ...buildPanelActions(set, get), ...buildExtraPanelActions(get), @@ -884,7 +834,6 @@ export function releaseLayoutToDefault(oldEnvId: string | null): void { preMaximizeLayout: null, maximizedGroupId: null, currentLayoutEnvId: null, - minimizedGroupIds: new Set(), }); buildDefaultLayout(api); }