feat(KB-086): complete Step 7 — integrate ActivityLogModal with Header and App
This commit is contained in:
@@ -17,6 +17,7 @@ import { GitManagerModal } from "./components/GitManagerModal";
|
||||
import { UsageIndicator } from "./components/UsageIndicator";
|
||||
import { NewTaskModal } from "./components/NewTaskModal";
|
||||
import { ScheduledTasksModal } from "./components/ScheduledTasksModal";
|
||||
import { ActivityLogModal } from "./components/ActivityLogModal";
|
||||
import { useTasks } from "./hooks/useTasks";
|
||||
import { ToastProvider, useToast } from "./hooks/useToast";
|
||||
import { useTheme } from "./hooks/useTheme";
|
||||
@@ -32,6 +33,7 @@ function AppInner() {
|
||||
const [usageOpen, setUsageOpen] = useState(false);
|
||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
const [filesOpen, setFilesOpen] = useState(false);
|
||||
const [activityLogOpen, setActivityLogOpen] = useState(false);
|
||||
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
||||
const [maxConcurrent, setMaxConcurrent] = useState(2);
|
||||
const [rootDir, setRootDir] = useState<string>(".");
|
||||
@@ -202,6 +204,10 @@ function AppInner() {
|
||||
setFilesOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Activity log handlers
|
||||
const handleOpenActivityLog = useCallback(() => setActivityLogOpen(true), []);
|
||||
const handleCloseActivityLog = useCallback(() => setActivityLogOpen(false), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@@ -209,6 +215,7 @@ function AppInner() {
|
||||
onOpenGitHubImport={() => setGitHubImportOpen(true)}
|
||||
onOpenPlanning={handlePlanningOpen}
|
||||
onOpenUsage={handleOpenUsage}
|
||||
onOpenActivityLog={handleOpenActivityLog}
|
||||
onOpenSchedules={handleOpenSchedules}
|
||||
onToggleTerminal={handleToggleTerminal}
|
||||
onToggleFiles={handleToggleFiles}
|
||||
@@ -325,6 +332,12 @@ function AppInner() {
|
||||
addToast={addToast}
|
||||
onPlanningMode={handleNewTaskPlanningMode}
|
||||
/>
|
||||
<ActivityLogModal
|
||||
isOpen={activityLogOpen}
|
||||
onClose={handleCloseActivityLog}
|
||||
tasks={tasks}
|
||||
onOpenTaskDetail={handleDetailOpen}
|
||||
/>
|
||||
<ToastContainer toasts={toasts} onRemove={removeToast} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Settings, Pause, Play, Square, Download, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder } from "lucide-react";
|
||||
import { Settings, Pause, Play, Square, Download, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History } from "lucide-react";
|
||||
|
||||
interface HeaderProps {
|
||||
onOpenSettings?: () => void;
|
||||
onOpenGitHubImport?: () => void;
|
||||
onOpenPlanning?: () => void;
|
||||
onOpenUsage?: () => void;
|
||||
onOpenActivityLog?: () => void;
|
||||
onOpenSchedules?: () => void;
|
||||
onToggleTerminal?: () => void;
|
||||
onToggleFiles?: () => void;
|
||||
@@ -224,6 +225,13 @@ export function Header({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Activity Log button */}
|
||||
{onOpenActivityLog && (
|
||||
<button className="btn-icon" onClick={onOpenActivityLog} title="View Activity Log">
|
||||
<History size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Desktop actions */}
|
||||
{!isMobile && (
|
||||
<button className="btn-icon" onClick={onOpenGitHubImport} title="Import from GitHub">
|
||||
|
||||
@@ -1485,11 +1485,11 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Spec tab", () => {
|
||||
it("shows Spec tab alongside other tabs", () => {
|
||||
describe("Definition tab edit mode", () => {
|
||||
it("shows Edit button in Definition tab", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
task={makeTask({ prompt: "# Test\n\nSpec content." })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1499,10 +1499,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Spec")).toBeTruthy();
|
||||
expect(screen.getByText("Edit")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("switches to Spec tab when clicked", () => {
|
||||
it("clicking Edit shows textarea with current prompt content", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Test\n\nSpec content." })}
|
||||
@@ -1515,17 +1515,19 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Initially not showing Spec content
|
||||
expect(container.querySelector(".spec-editor")).toBeNull();
|
||||
// Initially showing markdown view
|
||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||
|
||||
// Click Spec tab
|
||||
fireEvent.click(screen.getByText("Spec"));
|
||||
// Click Edit button
|
||||
fireEvent.click(screen.getByText("Edit"));
|
||||
|
||||
// Should show SpecEditor
|
||||
expect(container.querySelector(".spec-editor")).toBeTruthy();
|
||||
// Should show textarea
|
||||
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
|
||||
expect(textarea).toBeTruthy();
|
||||
expect(textarea.value).toBe("# Test\n\nSpec content.");
|
||||
});
|
||||
|
||||
it("Spec tab shows SpecEditor with task prompt", () => {
|
||||
it("clicking Cancel returns to view mode without saving", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Test Task\n\nTest specification." })}
|
||||
@@ -1538,13 +1540,102 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Spec"));
|
||||
fireEvent.click(screen.getByText("Edit"));
|
||||
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
|
||||
fireEvent.change(textarea, { target: { value: "Modified content" } });
|
||||
|
||||
// Should show the spec content (without leading heading)
|
||||
expect(container.querySelector(".spec-editor")).toBeTruthy();
|
||||
// Click Cancel
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
// Should show markdown view with original content
|
||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||
expect(screen.queryByRole("textbox")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows all 5 tabs in correct order", () => {
|
||||
it("saving updates the task and returns to view mode", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate.mockResolvedValueOnce({ id: "KB-099" } as Task);
|
||||
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "KB-099", prompt: "# Original" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Edit"));
|
||||
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
|
||||
fireEvent.change(textarea, { target: { value: "# Updated" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("KB-099", { prompt: "# Updated" });
|
||||
});
|
||||
|
||||
// Should return to view mode
|
||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("AI revision feedback section appears in edit mode", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Test" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Edit"));
|
||||
|
||||
expect(screen.getByText("Ask AI to Revise")).toBeTruthy();
|
||||
expect(screen.getByPlaceholderText(/e.g., 'Add more details/)).toBeTruthy();
|
||||
expect(screen.getByText("Request AI Revision")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("requesting AI revision works and closes modal", async () => {
|
||||
const { requestSpecRevision } = await import("../../api");
|
||||
vi.mocked(requestSpecRevision).mockResolvedValueOnce({});
|
||||
const onClose = vi.fn();
|
||||
const addToast = vi.fn();
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "KB-099", column: "todo", prompt: "# Test" })}
|
||||
onClose={onClose}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={addToast}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Edit"));
|
||||
|
||||
const feedbackInput = screen.getByPlaceholderText(/e.g., 'Add more details/);
|
||||
fireEvent.change(feedbackInput, { target: { value: "Please add more error handling details" } });
|
||||
|
||||
fireEvent.click(screen.getByText("Request AI Revision"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestSpecRevision).toHaveBeenCalledWith("KB-099", "Please add more error handling details");
|
||||
expect(addToast).toHaveBeenCalledWith("AI revision requested. Task moved to triage.", "success");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows all 5 tabs in correct order (Spec tab removed)", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
@@ -1558,17 +1649,16 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(tabs.length).toBe(6);
|
||||
expect(tabs.length).toBe(5);
|
||||
expect(tabs[0].textContent).toBe("Definition");
|
||||
expect(tabs[1].textContent).toBe("Activity");
|
||||
expect(tabs[2].textContent).toBe("Agent Log");
|
||||
expect(tabs[3].textContent).toBe("Steering");
|
||||
expect(tabs[4].textContent).toBe("Model");
|
||||
expect(tabs[5].textContent).toBe("Spec");
|
||||
});
|
||||
|
||||
it("shows empty state in Spec tab when no prompt", () => {
|
||||
const { container } = render(
|
||||
it("shows empty state and Edit button when no prompt", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "" })}
|
||||
onClose={noop}
|
||||
@@ -1580,10 +1670,8 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Spec"));
|
||||
|
||||
// Should show spec editor (view mode with empty state)
|
||||
expect(container.querySelector(".spec-editor")).toBeTruthy();
|
||||
expect(screen.getByText("(no prompt)")).toBeTruthy();
|
||||
expect(screen.getByText("Edit")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user