feat(KB-175): move file browser to header with workspace support

- Add workspace file APIs and hooks for session-scoped file browsing
- Create WorkspaceSelector component for switching between workspaces
- Update FileBrowserModal with workspace mode and improved UX
- Add files button to header with workspace-aware file browsing
- Add session files indicator to task cards for quick file access
- Include comprehensive tests for all new components and hooks
This commit is contained in:
gsxdsm
2026-03-31 01:26:57 -07:00
parent 18c4f0cb25
commit 9e03c47b91
30 changed files with 1779 additions and 829 deletions

View File

@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useWorkspaceFileBrowser } from "../useWorkspaceFileBrowser";
import * as api from "../../api";
import type { FileListResponse } from "../../api";
vi.mock("../../api", () => ({
fetchWorkspaceFileList: vi.fn(),
}));
const mockFetchWorkspaceFileList = vi.mocked(api.fetchWorkspaceFileList);
describe("useWorkspaceFileBrowser", () => {
beforeEach(() => {
mockFetchWorkspaceFileList.mockReset();
});
afterEach(() => {
vi.clearAllMocks();
});
it("fetches workspace files when enabled", async () => {
const mockResponse: FileListResponse = {
path: ".",
entries: [{ name: "src", type: "directory", mtime: "2024-01-01T00:00:00Z" }],
};
mockFetchWorkspaceFileList.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useWorkspaceFileBrowser("KB-123", true));
expect(result.current.loading).toBe(true);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.entries).toEqual(mockResponse.entries);
expect(mockFetchWorkspaceFileList).toHaveBeenCalledWith("KB-123", undefined);
});
it("resets path when workspace changes", async () => {
mockFetchWorkspaceFileList
.mockResolvedValueOnce({ path: ".", entries: [{ name: "src", type: "directory", mtime: "2024-01-01T00:00:00Z" }] })
.mockResolvedValueOnce({ path: "src", entries: [{ name: "index.ts", type: "file", size: 1, mtime: "2024-01-01T00:00:00Z" }] })
.mockResolvedValueOnce({ path: ".", entries: [{ name: "README.md", type: "file", size: 5, mtime: "2024-01-01T00:00:00Z" }] });
const { result, rerender } = renderHook(
({ workspace }) => useWorkspaceFileBrowser(workspace, true),
{ initialProps: { workspace: "KB-123" } },
);
await waitFor(() => expect(result.current.loading).toBe(false));
act(() => {
result.current.setPath("src");
});
await waitFor(() => expect(result.current.currentPath).toBe("src"));
await waitFor(() => expect(mockFetchWorkspaceFileList).toHaveBeenLastCalledWith("KB-123", "src"));
rerender({ workspace: "project" });
await waitFor(() => expect(result.current.currentPath).toBe("."));
await waitFor(() => expect(mockFetchWorkspaceFileList).toHaveBeenLastCalledWith("project", undefined));
});
it("handles fetch errors", async () => {
mockFetchWorkspaceFileList.mockRejectedValueOnce(new Error("Failed to load files"));
const { result } = renderHook(() => useWorkspaceFileBrowser("KB-404", true));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Failed to load files");
expect(result.current.entries).toEqual([]);
});
});