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,92 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { WorkspaceSelector } from "../WorkspaceSelector";
import type { WorkspaceInfo } from "../../hooks/useWorkspaces";
const workspaces: WorkspaceInfo[] = [
{
id: "KB-123",
label: "KB-123",
title: "Implement a very long task title that should be truncated in the dropdown list",
worktree: "/repo/.worktrees/kb-123",
kind: "task",
},
{
id: "KB-200",
label: "KB-200",
title: "Short title",
worktree: "/repo/.worktrees/kb-200",
kind: "task",
},
];
describe("WorkspaceSelector", () => {
it("shows the project name when project root is selected", () => {
render(
<WorkspaceSelector
currentWorkspace="project"
projectName="kb"
workspaces={workspaces}
onSelect={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: /kb/i })).toBeInTheDocument();
});
it("opens the menu and highlights the current workspace", async () => {
const user = userEvent.setup();
render(
<WorkspaceSelector
currentWorkspace="KB-200"
projectName="kb"
workspaces={workspaces}
onSelect={vi.fn()}
/>,
);
await user.click(screen.getByRole("button", { name: /KB-200/i }));
expect(screen.getByText("Project Root")).toBeInTheDocument();
const menu = screen.getByRole("listbox", { name: /select workspace/i });
const activeOption = within(menu).getByRole("button", { name: /KB-200 Short title/i });
expect(activeOption.className).toContain("active");
});
it("calls onSelect and closes when choosing a workspace", async () => {
const user = userEvent.setup();
const onSelect = vi.fn();
render(
<WorkspaceSelector
currentWorkspace="project"
projectName="kb"
workspaces={workspaces}
onSelect={onSelect}
/>,
);
await user.click(screen.getByRole("button", { name: /kb/i }));
await user.click(screen.getByRole("button", { name: /KB-123/i }));
expect(onSelect).toHaveBeenCalledWith("KB-123");
expect(screen.queryByText("Task Worktrees")).not.toBeInTheDocument();
});
it("truncates long task titles in the dropdown", async () => {
const user = userEvent.setup();
render(
<WorkspaceSelector
currentWorkspace="project"
projectName="kb"
workspaces={workspaces}
onSelect={vi.fn()}
/>,
);
await user.click(screen.getByRole("button", { name: /kb/i }));
expect(screen.getByText(/Implement a very long task title that shoul/)).toBeInTheDocument();
expect(screen.getByText(/…$/)).toBeInTheDocument();
});
});