feat(KB-659): add agents view toggle and merge dashboard updates

- Add the agents view toggle to the dashboard header and App view state
- Preserve multi-project dashboard behavior while resolving App merge conflicts
- Merge Header and App tests for agents view, search visibility, and deep-link handling
- Keep scripts, changed-files modal, and project overview integrations working together
This commit is contained in:
gsxdsm
2026-04-01 12:44:01 -07:00
parent 83ab5b0fda
commit 91b3bbf564
3 changed files with 108 additions and 14 deletions

View File

@@ -59,11 +59,11 @@ function AppInner() {
const [autoMerge, setAutoMerge] = useState(true);
const [globalPaused, setGlobalPaused] = useState(false);
const [enginePaused, setEnginePaused] = useState(false);
// Multi-project state
const { projects, loading: projectsLoading, register, update: updateProject, unregister: unregisterProject } = useProjects();
const { projects, loading: projectsLoading, update: updateProject, unregister: unregisterProject } = useProjects();
const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects);
// View state: "overview" for all projects, "project" for single project task view
const [viewMode, setViewMode] = useState<"overview" | "project">(() => {
if (typeof window !== "undefined") {
@@ -74,7 +74,7 @@ function AppInner() {
}
return "overview";
});
// Task view state (only meaningful when viewMode="project")
const [taskView, setTaskView] = useState<"board" | "list" | "agents">(() => {
if (typeof window !== "undefined") {
@@ -85,14 +85,14 @@ function AppInner() {
}
return "board";
});
const [searchQuery, setSearchQuery] = useState("");
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
// Setup wizard state
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
// Tasks hook with project context
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone } = useTasks(
currentProject ? { projectId: currentProject.id } : undefined
@@ -105,14 +105,14 @@ function AppInner() {
useEffect(() => {
// Wait for both loading states to complete before making decision
if (projectsLoading || currentProjectLoading) return;
// Don't open if wizard is already open
if (setupWizardOpen) return;
// Don't open if we have projects OR a saved current project
// (currentProject from localStorage means user was previously viewing a project)
if (projects.length > 0 || currentProject) return;
// Only open when truly no projects exist and no project is being restored
const timer = setTimeout(() => {
setSetupWizardOpen(true);
@@ -131,7 +131,7 @@ function AppInner() {
useEffect(() => {
// Wait for both loading states to complete before syncing
if (projectsLoading || currentProjectLoading) return;
// If we have a restored current project but viewMode is overview, sync to project view
if (currentProject && viewMode === "overview") {
setViewMode("project");

View File

@@ -35,6 +35,7 @@ vi.mock("../../api", async (importOriginal) => {
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
fetchModels: vi.fn(() => Promise.resolve([])),
fetchGitRemotes: vi.fn(() => Promise.resolve([])),
fetchAgents: vi.fn(() => Promise.resolve([])),
fetchTaskDetail: vi.fn((id: string) => Promise.resolve({
id,
title: `Task ${id}`,
@@ -512,15 +513,67 @@ describe("App view switching", () => {
localStorage.removeItem("kb-dashboard-view-mode");
});
it("shows view toggle buttons in header", async () => {
it("shows view toggle buttons in header including agents", async () => {
render(<App />);
// Wait for the header to render with view toggle
await waitFor(() => {
expect(screen.getByTitle("Board view")).toBeTruthy();
expect(screen.getByTitle("List view")).toBeTruthy();
expect(screen.getByTitle("Agents view")).toBeTruthy();
});
});
it("renders AgentsView when agents view is selected", async () => {
render(<App />);
// Wait for the header to render
await waitFor(() => {
expect(screen.getByTitle("Agents view")).toBeTruthy();
});
// Click to switch to agents view
fireEvent.click(screen.getByTitle("Agents view"));
// Agents view should be rendered (it has a agents-view container)
await waitFor(() => {
expect(document.querySelector(".agents-view")).toBeTruthy();
});
// Should NOT show board or list view
expect(document.querySelector(".board")).toBeNull();
expect(document.querySelector(".list-view")).toBeNull();
});
it("persists agents view preference to localStorage", async () => {
localStorage.removeItem("kb-dashboard-view");
render(<App />);
await waitFor(() => {
expect(screen.getByTitle("Agents view")).toBeTruthy();
});
fireEvent.click(screen.getByTitle("Agents view"));
await waitFor(() => {
expect(localStorage.getItem("kb-dashboard-view")).toBe("agents");
});
});
it("initializes agents view from localStorage if saved", async () => {
localStorage.setItem("kb-dashboard-view", "agents");
render(<App />);
await waitFor(() => {
expect(document.querySelector(".agents-view")).toBeTruthy();
});
expect(screen.getByTitle("Agents view").className).toContain("active");
localStorage.removeItem("kb-dashboard-view");
});
});
describe("App GitHub import", () => {

View File

@@ -251,6 +251,47 @@ describe("Header", () => {
expect(boardBtn.getAttribute("aria-pressed")).toBe("false");
});
// ── Search Visibility by View ─────────────────────────────────────
it("shows search input when view is 'board'", () => {
const onSearchChange = vi.fn();
render(
<Header
view="board"
onChangeView={vi.fn()}
searchQuery=""
onSearchChange={onSearchChange}
/>
);
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
});
it("hides search input when view is 'list'", () => {
const onSearchChange = vi.fn();
render(
<Header
view="list"
onChangeView={vi.fn()}
searchQuery=""
onSearchChange={onSearchChange}
/>
);
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
});
it("hides search input when view is 'agents'", () => {
const onSearchChange = vi.fn();
render(
<Header
view="agents"
onChangeView={vi.fn()}
searchQuery=""
onSearchChange={onSearchChange}
/>
);
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
});
// ── Terminal Button ─────────────────────────────────────────────
it("renders terminal button with correct title", () => {
@@ -647,7 +688,7 @@ describe("Header", () => {
onViewAllProjects={vi.fn()}
/>
);
// Open selector
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// Click on a project in the dropdown
@@ -668,7 +709,7 @@ describe("Header", () => {
onViewAllProjects={vi.fn()}
/>
);
expect(screen.getByText("Project One")).toBeDefined();
});
});