From 62fed53aa2e0b48eda1dc77639fc3616f9d5de18 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 19 Jun 2026 10:26:14 +0800 Subject: [PATCH] fix(spaces): clearer base-directories UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the base-dir config surface after review: - Open it by clicking the space name (now a button) or a labeled "Base directories" link on the card — not an unlabeled folder icon. - The modal leads with the existing paths, each removable in one click. - "Add folder…" is a clearly-optional dashed action, not the only button. - A "Done" button (plus X / Esc / backdrop) closes without adding anything. Test: SpaceBaseDirsModal — lists paths, one-click remove, Done closes without adding, renders nothing with no space. Signed-off-by: Mohammod Al Amin Ashik --- .../features/spaces/SpaceBaseDirsModal.tsx | 130 ++++++++++-------- .../src/features/spaces/SpacesPage.tsx | 32 +++-- .../ts/components/SpaceBaseDirsModal.test.tsx | 91 ++++++++++++ 3 files changed, 182 insertions(+), 71 deletions(-) create mode 100644 tests/ts/components/SpaceBaseDirsModal.test.tsx diff --git a/apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx b/apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx index c7468658..7a23efe5 100644 --- a/apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx +++ b/apps/desktop/src/features/spaces/SpaceBaseDirsModal.tsx @@ -14,10 +14,9 @@ import { * Manage a Space's base directories. * * A base dir scopes any workspace root opened at or under it to this Space: - * an unmapped folder there falls back to this Space's Starter set, and the - * self-optimize meta-tools + mapping popup restrict to this Space. Longest - * match wins when base dirs nest across Spaces, and a folder can belong to - * only one Space. + * an unmapped folder there uses this Space's tools, and self-optimize stays + * in this Space. Longest match wins when base dirs nest, and a folder can + * belong to only one Space. */ export function SpaceBaseDirsModal({ space, @@ -29,7 +28,7 @@ export function SpaceBaseDirsModal({ const [dirs, setDirs] = useState([]); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); - const { toasts, success, error: showError, dismiss } = useToast(); + const { toasts, error: showError, dismiss } = useToast(); const spaceId = space?.id ?? null; @@ -66,26 +65,18 @@ export function SpaceBaseDirsModal({ return; } const paths = Array.isArray(picked) ? picked : picked ? [picked] : []; - if (paths.length === 0) return; + if (paths.length === 0) return; // cancelled — nothing added setBusy(true); - let added = 0; for (const p of paths) { try { await addSpaceBaseDir(spaceId, p); - added++; } catch (e) { showError('Could not add folder', e instanceof Error ? e.message : String(e)); } } await load(); setBusy(false); - if (added > 0) { - success( - added === 1 ? 'Base directory added' : `${added} base directories added`, - 'Folders here are now scoped to this space.' - ); - } }; const handleRemove = async (dir: SpaceBaseDir) => { @@ -109,10 +100,11 @@ export function SpaceBaseDirsModal({ onClick={onClose} >
e.stopPropagation()} data-testid="space-base-dirs-modal" > + {/* Header */}
@@ -121,7 +113,7 @@ export function SpaceBaseDirsModal({

Base directories

- Folders scoped to {space.name} + Scoped to {space.name}

@@ -129,69 +121,87 @@ export function SpaceBaseDirsModal({ onClick={onClose} className="rounded-lg p-1.5 text-[rgb(var(--muted))] transition-colors hover:bg-[rgb(var(--surface))] hover:text-[rgb(var(--foreground))]" aria-label="Close" + data-testid="space-base-dirs-close" >
+ {/* Body */}

- Any folder you open here (or under it) is scoped to this space — it uses this - space's tools by default, and self-optimize only sees this space. The most specific - base directory wins, and a folder can belong to only one space. + Folders you open here (or under them) are scoped to this space.

{loading ? ( -
+
- ) : dirs.length === 0 ? ( -
- No base directories yet. Add one to scope its folders to this space. -
) : ( -
    - {dirs.map((dir) => ( -
  • - - - {dir.path} - - -
  • - ))} -
+ <> + {dirs.length > 0 && ( +
    + {dirs.map((dir) => ( +
  • + + + {dir.path} + + +
  • + ))} +
+ )} + + {/* Add row — a clearly optional action, not the only way out. */} + + + {dirs.length === 0 && !busy && ( +

+ No base directories yet. +

+ )} + )}
-
+ {/* Footer — close without adding. */} +
diff --git a/apps/desktop/src/features/spaces/SpacesPage.tsx b/apps/desktop/src/features/spaces/SpacesPage.tsx index 09d1e389..83504fa6 100644 --- a/apps/desktop/src/features/spaces/SpacesPage.tsx +++ b/apps/desktop/src/features/spaces/SpacesPage.tsx @@ -157,21 +157,21 @@ export function SpacesPage() {
{space.icon || '🌐'}
-
-

{space.name}

+
+
- {space.is_default && (
+ + ); diff --git a/tests/ts/components/SpaceBaseDirsModal.test.tsx b/tests/ts/components/SpaceBaseDirsModal.test.tsx new file mode 100644 index 00000000..4d6f11d1 --- /dev/null +++ b/tests/ts/components/SpaceBaseDirsModal.test.tsx @@ -0,0 +1,91 @@ +/** + * Spaces — "Base directories" modal. + * + * The modal must show the space's existing base dirs, let you remove one + * easily, add a folder as a clearly-optional action, and close via "Done" + * without being forced to add anything. + * + * `@mcpmux/ui` is aliased to the real source in vitest.config, so the real + * Button / toast render. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const { listMock, addMock, removeMock } = vi.hoisted(() => ({ + listMock: vi.fn(), + addMock: vi.fn(), + removeMock: vi.fn(), +})); + +vi.mock('@/lib/api/spaces', () => ({ + listSpaceBaseDirs: listMock, + addSpaceBaseDir: addMock, + removeSpaceBaseDir: removeMock, +})); + +import { SpaceBaseDirsModal } from '@/features/spaces/SpaceBaseDirsModal'; + +const SPACE = { + id: 's1', + name: 'Work', + icon: '💼', + description: null, + is_default: false, + sort_order: 0, + created_at: '', + updated_at: '', +}; + +function dir(id: string, path: string) { + return { id, space_id: 's1', path, created_at: '' }; +} + +describe('SpaceBaseDirsModal', () => { + beforeEach(() => { + listMock.mockReset(); + addMock.mockReset(); + removeMock.mockReset(); + }); + + it('lists the space’s existing base directories', async () => { + listMock.mockResolvedValue([dir('d1', '/work/a'), dir('d2', '/work/b')]); + render( {}} />); + + expect(await screen.findByText('/work/a')).toBeTruthy(); + expect(screen.getByText('/work/b')).toBeTruthy(); + }); + + it('removes a directory with one click', async () => { + const user = userEvent.setup(); + removeMock.mockResolvedValue(undefined); + listMock.mockResolvedValue([dir('d1', '/work/a')]); + render( {}} />); + + await screen.findByText('/work/a'); + await user.click(screen.getByTestId('remove-base-dir-d1')); + + await waitFor(() => expect(removeMock).toHaveBeenCalledWith('d1')); + await waitFor(() => expect(screen.queryByText('/work/a')).toBeNull()); + }); + + it('closes via "Done" without adding anything', async () => { + const user = userEvent.setup(); + const onClose = vi.fn(); + listMock.mockResolvedValue([]); + render(); + + await screen.findByTestId('add-base-dir-btn'); // loaded (empty) + await user.click(screen.getByTestId('space-base-dirs-done')); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(addMock).not.toHaveBeenCalled(); + }); + + it('renders nothing when no space is selected', () => { + const { container } = render( {}} />); + expect(container).toBeEmptyDOMElement(); + expect(listMock).not.toHaveBeenCalled(); + }); +});