Redesign Tasks screen: expandable TaskCard, sidebar controls, and list panel
- Redesign TaskCard to match v1 Task Details panel layout: header with task ID + status badge + cancel button, always-visible summary row (Status, Last Updated, Created At, TTL) with Expand/Collapse button, expandable section with status message, progress bar, error, and Full Task Object JSON viewer - Add TaskControls group: search input + status filter dropdown + Refresh/Clear History buttons (mirrors HistoryControls pattern) - Add TaskListPanel group: scrollable panel with ListToggle for expand/collapse all, splits tasks into Active/Completed sections with counts, filters by search text and status - Refactor TasksScreen to sidebar + panel layout matching HistoryScreen: manages search/filter state internally, flat tasks array - Simplify TaskCard callbacks to just onCancel - Extract named .withProps() subcomponents (HeaderLeftGroup, HeaderRightGroup, SectionTitle, SummaryRow, DetailRow) for AGENTS.md compliance - Update all stories (TaskCard, TaskControls, TaskListPanel, TasksScreen, ConnectedView) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a0e763a315
commit
c494d2feb4
2 changed files with 639 additions and 276 deletions
|
|
@ -0,0 +1,88 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { fn } from "storybook/test";
|
||||
import { TaskListPanel } from "./TaskListPanel";
|
||||
import type { TaskCardProps } from "../TaskCard/TaskCard";
|
||||
|
||||
const sampleTasks: TaskCardProps[] = [
|
||||
{
|
||||
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||
status: "running",
|
||||
method: "tools/call",
|
||||
target: "generate_report",
|
||||
progress: 80,
|
||||
progressDescription: "Synthesizing findings...",
|
||||
startedAt: "3/29/2026, 8:18:20 PM",
|
||||
lastUpdated: "3/29/2026, 8:18:22 PM",
|
||||
ttl: 300000,
|
||||
isListExpanded: true,
|
||||
onCancel: fn(),
|
||||
},
|
||||
{
|
||||
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
|
||||
status: "waiting",
|
||||
method: "resources/read",
|
||||
target: "file:///data/report.csv",
|
||||
startedAt: "3/29/2026, 8:17:55 PM",
|
||||
lastUpdated: "3/29/2026, 8:17:55 PM",
|
||||
ttl: 300000,
|
||||
isListExpanded: true,
|
||||
onCancel: fn(),
|
||||
},
|
||||
{
|
||||
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||
status: "completed",
|
||||
method: "tools/call",
|
||||
target: "generate_summary",
|
||||
startedAt: "3/29/2026, 8:16:47 PM",
|
||||
completedAt: "3/29/2026, 8:16:51 PM",
|
||||
lastUpdated: "3/29/2026, 8:16:51 PM",
|
||||
ttl: 300000,
|
||||
progressDescription: "Generating report...",
|
||||
isListExpanded: true,
|
||||
onCancel: fn(),
|
||||
},
|
||||
{
|
||||
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||
status: "failed",
|
||||
method: "tools/call",
|
||||
target: "fetch_remote_data",
|
||||
startedAt: "3/29/2026, 8:17:27 PM",
|
||||
lastUpdated: "3/29/2026, 8:17:28 PM",
|
||||
ttl: 300000,
|
||||
error: "Connection refused: upstream server not responding",
|
||||
isListExpanded: true,
|
||||
onCancel: fn(),
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof TaskListPanel> = {
|
||||
title: "Groups/TaskListPanel",
|
||||
component: TaskListPanel,
|
||||
args: {
|
||||
tasks: sampleTasks,
|
||||
searchText: "",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TaskListPanel>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const FilteredByStatus: Story = {
|
||||
args: {
|
||||
statusFilter: "running",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSearch: Story = {
|
||||
args: {
|
||||
searchText: "generate",
|
||||
},
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
tasks: [],
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { Group, Paper, ScrollArea, Stack, Text, Title } from "@mantine/core";
|
||||
import { TaskCard } from "../TaskCard/TaskCard";
|
||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||
import type { TaskCardProps } from "../TaskCard/TaskCard";
|
||||
|
||||
export interface TaskListPanelProps {
|
||||
tasks: TaskCardProps[];
|
||||
searchText: string;
|
||||
statusFilter?: string;
|
||||
}
|
||||
|
||||
const PanelContainer = Paper.withProps({
|
||||
withBorder: true,
|
||||
p: "lg",
|
||||
flex: 1,
|
||||
variant: "panel",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
c: "dimmed",
|
||||
ta: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
function taskKey(task: TaskCardProps): string {
|
||||
return task.taskId;
|
||||
}
|
||||
|
||||
function formatActiveTitle(count: number): string {
|
||||
return `Active (${count})`;
|
||||
}
|
||||
|
||||
function formatCompletedTitle(count: number): string {
|
||||
return `Completed (${count})`;
|
||||
}
|
||||
|
||||
function isActiveStatus(status: string): boolean {
|
||||
return status === "waiting" || status === "running";
|
||||
}
|
||||
|
||||
function matchesFilters(
|
||||
task: TaskCardProps,
|
||||
searchText: string,
|
||||
statusFilter?: string,
|
||||
): boolean {
|
||||
if (statusFilter && task.status !== statusFilter) return false;
|
||||
if (searchText) {
|
||||
const term = searchText.toLowerCase();
|
||||
const searchable =
|
||||
`${task.taskId} ${task.method} ${task.target ?? ""} ${task.status}`.toLowerCase();
|
||||
if (!searchable.includes(term)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function TaskListPanel({
|
||||
tasks,
|
||||
searchText,
|
||||
statusFilter,
|
||||
}: TaskListPanelProps) {
|
||||
const [compact, setCompact] = useState(false);
|
||||
|
||||
const filteredTasks = useMemo(
|
||||
() => tasks.filter((t) => matchesFilters(t, searchText, statusFilter)),
|
||||
[tasks, searchText, statusFilter],
|
||||
);
|
||||
|
||||
const activeTasks = useMemo(
|
||||
() => filteredTasks.filter((t) => isActiveStatus(t.status)),
|
||||
[filteredTasks],
|
||||
);
|
||||
|
||||
const completedTasks = useMemo(
|
||||
() => filteredTasks.filter((t) => !isActiveStatus(t.status)),
|
||||
[filteredTasks],
|
||||
);
|
||||
|
||||
const hasResults = filteredTasks.length > 0;
|
||||
|
||||
return (
|
||||
<PanelContainer>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Title order={4}>Tasks</Title>
|
||||
{hasResults && (
|
||||
<ListToggle
|
||||
compact={compact}
|
||||
onToggle={() => setCompact((c) => !c)}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{!hasResults ? (
|
||||
<EmptyState>No tasks</EmptyState>
|
||||
) : (
|
||||
<ScrollArea.Autosize mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)">
|
||||
<Stack gap="md">
|
||||
{activeTasks.length > 0 && (
|
||||
<>
|
||||
<Title order={5}>{formatActiveTitle(activeTasks.length)}</Title>
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={taskKey(task)}
|
||||
{...task}
|
||||
isListExpanded={!compact}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{completedTasks.length > 0 && (
|
||||
<>
|
||||
<Title order={5}>
|
||||
{formatCompletedTitle(completedTasks.length)}
|
||||
</Title>
|
||||
{completedTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={taskKey(task)}
|
||||
{...task}
|
||||
isListExpanded={!compact}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
)}
|
||||
</PanelContainer>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue