feat(KB-028): add multi-agent terminal view
- Create useMultiAgentLogs hook for real-time agent log streaming - Build TerminalModal component with tabbed task interface - Add terminal styles with mobile responsive design - Integrate terminal toggle into Header with in-progress badge - Wire TerminalModal into App.tsx with in-progress task filtering
This commit is contained in:
@@ -97,6 +97,49 @@ describe("Header", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("terminal button", () => {
|
||||
it("renders terminal button with correct title", () => {
|
||||
renderHeader({ onToggleTerminal: noop });
|
||||
expect(screen.getByTitle("Open Terminal View")).toBeDefined();
|
||||
});
|
||||
|
||||
it("calls onToggleTerminal when terminal button is clicked", () => {
|
||||
const onToggleTerminal = vi.fn();
|
||||
renderHeader({ onToggleTerminal, inProgressCount: 1 });
|
||||
fireEvent.click(screen.getByTitle("Open Terminal View"));
|
||||
expect(onToggleTerminal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows badge with count when in-progress tasks exist", () => {
|
||||
renderHeader({ onToggleTerminal: noop, inProgressCount: 3 });
|
||||
expect(screen.getByTestId("terminal-badge")).toBeDefined();
|
||||
expect(screen.getByTestId("terminal-badge").textContent).toBe("3");
|
||||
});
|
||||
|
||||
it("shows badge with 9+ when count exceeds 9", () => {
|
||||
renderHeader({ onToggleTerminal: noop, inProgressCount: 15 });
|
||||
expect(screen.getByTestId("terminal-badge")).toBeDefined();
|
||||
expect(screen.getByTestId("terminal-badge").textContent).toBe("9+");
|
||||
});
|
||||
|
||||
it("does not show badge when no in-progress tasks", () => {
|
||||
renderHeader({ onToggleTerminal: noop, inProgressCount: 0 });
|
||||
expect(screen.queryByTestId("terminal-badge")).toBeNull();
|
||||
});
|
||||
|
||||
it("is disabled when no in-progress tasks", () => {
|
||||
renderHeader({ onToggleTerminal: noop, inProgressCount: 0 });
|
||||
const btn = screen.getByTitle("Open Terminal View");
|
||||
expect(btn.hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("is enabled when in-progress tasks exist", () => {
|
||||
renderHeader({ onToggleTerminal: noop, inProgressCount: 2 });
|
||||
const btn = screen.getByTitle("Open Terminal View");
|
||||
expect(btn.hasAttribute("disabled")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pause controls", () => {
|
||||
it("renders pause button for engine pause", () => {
|
||||
renderHeader();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Settings, Pause, Play, Square, Download, LayoutGrid, List, GitBranch } from "lucide-react";
|
||||
import { Settings, Pause, Play, Square, Download, LayoutGrid, List, Terminal } from "lucide-react";
|
||||
|
||||
interface HeaderProps {
|
||||
onOpenSettings?: () => void;
|
||||
onOpenGitHubImport?: () => void;
|
||||
onOpenGitManager?: () => void;
|
||||
onToggleTerminal?: () => void;
|
||||
inProgressCount?: number;
|
||||
globalPaused?: boolean;
|
||||
enginePaused?: boolean;
|
||||
onToggleGlobalPause?: () => void;
|
||||
@@ -15,7 +16,8 @@ interface HeaderProps {
|
||||
export function Header({
|
||||
onOpenSettings,
|
||||
onOpenGitHubImport,
|
||||
onOpenGitManager,
|
||||
onToggleTerminal,
|
||||
inProgressCount = 0,
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
onToggleGlobalPause,
|
||||
@@ -23,6 +25,8 @@ export function Header({
|
||||
view = "board",
|
||||
onChangeView,
|
||||
}: HeaderProps) {
|
||||
const hasInProgressTasks = inProgressCount > 0;
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
@@ -58,9 +62,20 @@ export function Header({
|
||||
<button className="btn-icon" onClick={onOpenGitHubImport} title="Import from GitHub">
|
||||
<Download size={16} />
|
||||
</button>
|
||||
{/* Git Manager */}
|
||||
<button className="btn-icon" onClick={onOpenGitManager} title="Git Manager">
|
||||
<GitBranch size={16} />
|
||||
{/* Terminal button - shows badge with count when in-progress tasks exist */}
|
||||
<button
|
||||
className={`btn-icon btn-icon--terminal${hasInProgressTasks ? " has-badge" : ""}`}
|
||||
onClick={onToggleTerminal}
|
||||
title="Open Terminal View"
|
||||
disabled={!hasInProgressTasks}
|
||||
data-testid="terminal-toggle-btn"
|
||||
>
|
||||
<Terminal size={16} />
|
||||
{hasInProgressTasks && (
|
||||
<span className="btn-badge" data-testid="terminal-badge">
|
||||
{inProgressCount > 9 ? "9+" : inProgressCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{/* Pause button (soft pause): stops new work, lets agents finish */}
|
||||
<button
|
||||
|
||||
137
packages/dashboard/app/components/TerminalModal.tsx
Normal file
137
packages/dashboard/app/components/TerminalModal.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import type { Task, AgentLogEntry } from "@kb/core";
|
||||
import { useMultiAgentLogs } from "../hooks/useMultiAgentLogs";
|
||||
import { AgentLogViewer } from "./AgentLogViewer";
|
||||
|
||||
interface TerminalModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
interface LogEntryWithTask extends AgentLogEntry {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export function TerminalModal({ isOpen, onClose, tasks }: TerminalModalProps) {
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
|
||||
// Get task IDs for all in-progress tasks
|
||||
const inProgressTaskIds = tasks.map((t) => t.id);
|
||||
|
||||
// Get log state for all tasks
|
||||
const logState = useMultiAgentLogs(inProgressTaskIds);
|
||||
|
||||
// Set initial active task when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && tasks.length > 0) {
|
||||
// If no active task or active task not in current list, set first task
|
||||
if (!activeTaskId || !tasks.find((t) => t.id === activeTaskId)) {
|
||||
setActiveTaskId(tasks[0].id);
|
||||
}
|
||||
}
|
||||
// Reset when modal closes
|
||||
if (!isOpen) {
|
||||
setActiveTaskId(null);
|
||||
}
|
||||
}, [isOpen, tasks, activeTaskId]);
|
||||
|
||||
// Handle escape key to close modal
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", handleKey);
|
||||
return () => document.removeEventListener("keydown", handleKey);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Handle overlay click to close
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Get active task info
|
||||
const activeTask = tasks.find((t) => t.id === activeTaskId);
|
||||
const activeLogState = activeTaskId ? logState[activeTaskId] : null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open" onClick={handleOverlayClick} data-testid="terminal-modal-overlay">
|
||||
<div className="modal terminal-modal" data-testid="terminal-modal">
|
||||
{/* Header with tabs and close button */}
|
||||
<div className="terminal-header">
|
||||
<div className="terminal-tabs" data-testid="terminal-tabs">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="terminal-tab terminal-tab--empty" data-testid="terminal-no-tasks">
|
||||
No active tasks
|
||||
</div>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<button
|
||||
key={task.id}
|
||||
className={`terminal-tab ${activeTaskId === task.id ? "terminal-tab--active" : ""}`}
|
||||
onClick={() => setActiveTaskId(task.id)}
|
||||
data-testid={`terminal-tab-${task.id}`}
|
||||
title={task.title || task.description}
|
||||
>
|
||||
<span className="terminal-tab-label">{task.id}</span>
|
||||
{activeTaskId === task.id && (
|
||||
<span
|
||||
className="terminal-tab-indicator"
|
||||
data-testid={`terminal-tab-indicator-${task.id}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<button className="terminal-close" onClick={onClose} data-testid="terminal-close-btn" title="Close terminal">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log content area */}
|
||||
<div className="terminal-content" data-testid="terminal-content">
|
||||
{tasks.length === 0 ? (
|
||||
<div className="terminal-empty-state" data-testid="terminal-empty-state">
|
||||
<p>No tasks currently in progress.</p>
|
||||
<p>Start a task to see live logs here.</p>
|
||||
</div>
|
||||
) : activeTask && activeLogState ? (
|
||||
<>
|
||||
<div className="terminal-toolbar" data-testid="terminal-toolbar">
|
||||
<div className="terminal-task-info">
|
||||
<span className="terminal-task-id" data-testid="terminal-active-task-id">
|
||||
{activeTask.id}
|
||||
</span>
|
||||
<span className="terminal-task-title" data-testid="terminal-active-task-title">
|
||||
{activeTask.title || activeTask.description}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="terminal-clear-btn"
|
||||
onClick={activeLogState.clear}
|
||||
data-testid="terminal-clear-btn"
|
||||
title="Clear log buffer"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="terminal-log-container" data-testid="terminal-log-container">
|
||||
<AgentLogViewer entries={activeLogState.entries} loading={activeLogState.loading} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, waitFor, act, fireEvent } from "@testing-library/react";
|
||||
import { TerminalModal } from "../TerminalModal";
|
||||
import type { Task } from "@kb/core";
|
||||
import * as useMultiAgentLogsModule from "../../hooks/useMultiAgentLogs";
|
||||
|
||||
// Mock the useMultiAgentLogs hook
|
||||
vi.mock("../../hooks/useMultiAgentLogs", () => ({
|
||||
useMultiAgentLogs: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseMultiAgentLogs = vi.mocked(useMultiAgentLogsModule.useMultiAgentLogs);
|
||||
|
||||
describe("TerminalModal", () => {
|
||||
const mockOnClose = vi.fn();
|
||||
|
||||
const createMockTask = (id: string, title?: string): Task => ({
|
||||
id,
|
||||
title: title || `Task ${id}`,
|
||||
description: `Description for ${id}`,
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
status: undefined,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnClose.mockClear();
|
||||
mockUseMultiAgentLogs.mockReturnValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders without crashing when open with empty task list", () => {
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={[]} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("terminal-modal")).toBeTruthy();
|
||||
expect(screen.getByTestId("terminal-no-tasks").textContent).toContain("No active tasks");
|
||||
expect(screen.getByTestId("terminal-empty-state").textContent).toContain("No tasks currently in progress");
|
||||
});
|
||||
|
||||
it("renders without crashing when open with multiple in-progress tasks", () => {
|
||||
const tasks = [
|
||||
createMockTask("KB-001", "First Task"),
|
||||
createMockTask("KB-002", "Second Task"),
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
"KB-002": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("terminal-modal")).toBeTruthy();
|
||||
expect(screen.getByTestId("terminal-tab-KB-001")).toBeTruthy();
|
||||
expect(screen.getByTestId("terminal-tab-KB-002")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render when closed", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
const { container } = render(
|
||||
<TerminalModal isOpen={false} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("shows appropriate empty state when no in-progress tasks", () => {
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={[]} />
|
||||
);
|
||||
|
||||
expect(screen.getByText("No active tasks")).toBeTruthy();
|
||||
expect(screen.getByText("No tasks currently in progress.")).toBeTruthy();
|
||||
expect(screen.getByText("Start a task to see live logs here.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("tab switching changes which task's logs are displayed", async () => {
|
||||
const tasks = [
|
||||
createMockTask("KB-001", "First Task"),
|
||||
createMockTask("KB-002", "Second Task"),
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
"KB-002": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
// First task should be active by default
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-001");
|
||||
|
||||
// Click on second tab
|
||||
fireEvent.click(screen.getByTestId("terminal-tab-KB-002"));
|
||||
|
||||
// Second task should now be active
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-002");
|
||||
});
|
||||
});
|
||||
|
||||
it("active tab has correct styling with indicator", () => {
|
||||
const tasks = [
|
||||
createMockTask("KB-001", "First Task"),
|
||||
createMockTask("KB-002", "Second Task"),
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
"KB-002": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
// First tab should be active by default
|
||||
const tab1 = screen.getByTestId("terminal-tab-KB-001");
|
||||
const tab2 = screen.getByTestId("terminal-tab-KB-002");
|
||||
|
||||
expect(tab1.className).toContain("terminal-tab--active");
|
||||
expect(tab2.className).not.toContain("terminal-tab--active");
|
||||
|
||||
// Active tab should have indicator
|
||||
expect(screen.getByTestId("terminal-tab-indicator-KB-001")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("clicking clear button clears that tab's log entries", () => {
|
||||
const mockClear = vi.fn();
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "log", type: "text" as const }], loading: false, clear: mockClear },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
const clearBtn = screen.getByTestId("terminal-clear-btn");
|
||||
fireEvent.click(clearBtn);
|
||||
|
||||
expect(mockClear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("modal closes on Escape key press", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("modal closes on overlay click", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
const overlay = screen.getByTestId("terminal-modal-overlay");
|
||||
fireEvent.click(overlay);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("modal does not close when clicking inside modal content", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
const modal = screen.getByTestId("terminal-modal");
|
||||
fireEvent.click(modal);
|
||||
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("modal closes on close button click", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
const closeBtn = screen.getByTestId("terminal-close-btn");
|
||||
fireEvent.click(closeBtn);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("displays task information in toolbar", () => {
|
||||
const tasks = [createMockTask("KB-001", "My Test Task")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-001");
|
||||
expect(screen.getByTestId("terminal-active-task-title").textContent).toBe("My Test Task");
|
||||
});
|
||||
|
||||
it("uses description as title fallback when title is not provided", () => {
|
||||
const tasks = [{
|
||||
...createMockTask("KB-001"),
|
||||
title: undefined,
|
||||
description: "My Description",
|
||||
}];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("terminal-active-task-title").textContent).toBe("My Description");
|
||||
});
|
||||
|
||||
it("passes correct entries to AgentLogViewer", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
const entries = [
|
||||
{ timestamp: "2026-01-01T00:00:00Z", taskId: "KB-001", text: "log1", type: "text" as const },
|
||||
{ timestamp: "2026-01-01T00:01:00Z", taskId: "KB-001", text: "log2", type: "tool" as const },
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries, loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("agent-log-viewer")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("passes loading state to AgentLogViewer", () => {
|
||||
const tasks = [createMockTask("KB-001")];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: true, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("agent-log-viewer")).toBeTruthy();
|
||||
expect(screen.getByText("Loading agent logs…")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("switches to first task when active task is removed from list", () => {
|
||||
const tasks = [
|
||||
createMockTask("KB-001"),
|
||||
createMockTask("KB-002"),
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
"KB-002": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
// Initially KB-001 should be active
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-001");
|
||||
|
||||
// Click KB-002 to make it active
|
||||
act(() => {
|
||||
screen.getByTestId("terminal-tab-KB-002").click();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-002");
|
||||
|
||||
// Rerender with only KB-001
|
||||
rerender(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={[tasks[0]]} />
|
||||
);
|
||||
|
||||
// Should switch back to KB-001
|
||||
expect(screen.getByTestId("terminal-active-task-id").textContent).toBe("KB-001");
|
||||
});
|
||||
|
||||
it("tab labels show task IDs", () => {
|
||||
const tasks = [
|
||||
createMockTask("KB-001"),
|
||||
createMockTask("KB-002"),
|
||||
];
|
||||
|
||||
mockUseMultiAgentLogs.mockReturnValue({
|
||||
"KB-001": { entries: [], loading: false, clear: vi.fn() },
|
||||
"KB-002": { entries: [], loading: false, clear: vi.fn() },
|
||||
});
|
||||
|
||||
render(
|
||||
<TerminalModal isOpen={true} onClose={mockOnClose} tasks={tasks} />
|
||||
);
|
||||
|
||||
const tab1 = screen.getByTestId("terminal-tab-KB-001");
|
||||
const tab2 = screen.getByTestId("terminal-tab-KB-002");
|
||||
|
||||
expect(tab1.textContent).toContain("KB-001");
|
||||
expect(tab2.textContent).toContain("KB-002");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user