feat(FN-785): style Agents view state selector and document controls

- Replace plain <select> dropdown with styled agent-state-filter container featuring Filter icon, hover/focus states, and consistent dashboard theming
- Add aria-label for accessibility on the state filter dropdown
- Add CSS styles for .agent-state-filter wrapper with border, color transitions, and focus ring
- Add unit test for styled filter container rendering and aria-label presence
- Document Agents view features (state filter, view modes, CRUD, health monitoring) in README
This commit is contained in:
gsxdsm
2026-04-03 13:02:36 -07:00
parent d12eec09c6
commit c1425b0d8f
3 changed files with 75 additions and 14 deletions

View File

@@ -92,6 +92,16 @@ A persistent footer status bar at the bottom of the dashboard displays real-time
**API Endpoint**:
- `GET /api/executor/stats` - Returns `globalPause`, `enginePaused`, `maxConcurrent`, and `lastActivityAt` for state derivation. Column-based counts (running, blocked, stuck, queued, in-review) are derived client-side from the shared task list.
### Agents View
Manage AI agents with a dedicated control surface accessible from the main dashboard navigation.
**Features**:
- **State Filter**: Styled dropdown to filter agents by state (All States, Idle, Active, Paused, Terminated) with icon and consistent dashboard styling
- **View Modes**: Board (compact grid) and list (detailed card) layouts, persisted to localStorage
- **Agent CRUD**: Create agents with name and role, change state, update roles inline, delete terminated agents
- **Health Monitoring**: Heartbeat-based health status (Healthy, Unresponsive, Starting, Paused, Terminated)
- **Agent Detail**: Click any agent card to open a detail modal with full agent information
### Interactive Terminal
Access a fully functional PTY (pseudo-terminal) shell directly from the dashboard. Click the terminal icon in the header to open the interactive terminal modal.

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from "react";
import type { JSX } from "react";
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight } from "lucide-react";
import { Plus, Play, Pause, Square, Activity, Heart, Trash2, RefreshCw, Bot, LayoutGrid, List, ChevronRight, Filter } from "lucide-react";
import type { Agent, AgentCapability, AgentState } from "../api";
import { fetchAgents, createAgent, updateAgent, updateAgentState, deleteAgent } from "../api";
import { AgentDetailView } from "./AgentDetailView";
@@ -189,17 +189,21 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
<div className="agents-view-content">
{/* Filter and Create Bar */}
<div className="agent-controls">
<select
className="select"
value={filterState}
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
>
<option value="all">All States</option>
<option value="idle">Idle</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="terminated">Terminated</option>
</select>
<div className="agent-state-filter">
<Filter size={14} />
<select
className="agent-state-filter-select"
value={filterState}
onChange={(e) => setFilterState(e.target.value as AgentState | "all")}
aria-label="Filter agents by state"
>
<option value="all">All States</option>
<option value="idle">Idle</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="terminated">Terminated</option>
</select>
</div>
<button
className="btn btn--primary"
@@ -568,8 +572,38 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
margin-bottom: 16px;
}
.agent-controls .select {
width: auto;
.agent-state-filter {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
transition: border-color var(--transition-fast), color var(--transition-fast);
}
.agent-state-filter:hover {
border-color: var(--text-dim);
color: var(--text);
}
.agent-state-filter:focus-within {
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.agent-state-filter-select {
appearance: none;
background: transparent;
border: none;
color: var(--text);
font-size: 13px;
font-family: var(--font-primary);
cursor: pointer;
outline: none;
padding-right: 4px;
}
.agent-create-form {

View File

@@ -216,6 +216,23 @@ describe("AgentsView", () => {
});
describe("filter agents by state", () => {
it("renders the state filter with styled container", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Styled filter container exists
const filterContainer = document.querySelector(".agent-state-filter");
expect(filterContainer).toBeTruthy();
// Select has correct aria-label
const filterSelect = screen.getByLabelText("Filter agents by state");
expect(filterSelect).toBeTruthy();
expect(filterSelect).toHaveValue("all");
});
it("can filter agents by state", async () => {
render(<AgentsView addToast={mockAddToast} />);