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:
cliffhall 2026-03-29 22:03:54 -04:00
commit c494d2feb4
9 changed files with 639 additions and 276 deletions

View file

@ -6,10 +6,8 @@ const meta: Meta<typeof TaskCard> = {
title: "Groups/TaskCard",
component: TaskCard,
args: {
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
isListExpanded: true,
},
};
@ -18,77 +16,82 @@ type Story = StoryObj<typeof TaskCard>;
export const Running: Story = {
args: {
taskId: "task-abc-123",
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "running",
method: "tools/call",
target: "generate_report",
progress: 80,
progressDescription: "Generating report section 4 of 5...",
startedAt: "2026-03-17T10:30:00Z",
elapsed: "2m 15s",
},
};
export const RunningWithProgress: Story = {
args: {
taskId: "task-def-456",
status: "running",
method: "tools/call",
target: "analyze_data",
progress: 45,
progressDescription: "Analyzing dataset...",
startedAt: "2026-03-17T10:32:00Z",
elapsed: "30s",
progressDescription: "Synthesizing findings...",
startedAt: "3/29/2026, 8:18:20 PM",
lastUpdated: "3/29/2026, 8:18:22 PM",
ttl: 300000,
},
};
export const Waiting: Story = {
args: {
taskId: "task-ghi-789",
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "waiting",
method: "resources/read",
target: "file:///data/report.csv",
progress: 0,
startedAt: "2026-03-17T10:35:00Z",
startedAt: "3/29/2026, 8:17:55 PM",
lastUpdated: "3/29/2026, 8:17:55 PM",
ttl: 300000,
},
};
export const Completed: Story = {
args: {
taskId: "task-jkl-012",
taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed",
method: "tools/call",
target: "generate_summary",
progress: 100,
startedAt: "2026-03-17T10:20:00Z",
completedAt: "2026-03-17T10:25:00Z",
elapsed: "5m 00s",
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...",
},
};
export const Failed: Story = {
args: {
taskId: "task-mno-345",
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed",
method: "tools/call",
target: "fetch_remote_data",
progress: 45,
startedAt: "2026-03-17T10:28:00Z",
completedAt: "2026-03-17T10:29:30Z",
elapsed: "1m 30s",
startedAt: "3/29/2026, 8:17:27 PM",
completedAt: "3/29/2026, 8:17:28 PM",
lastUpdated: "3/29/2026, 8:17:28 PM",
ttl: 300000,
error: "Connection refused: upstream server not responding after 3 retries",
},
};
export const Cancelled: Story = {
args: {
taskId: "task-pqr-678",
taskId: "c416bc183cb04468d3f81696f1b868f6",
status: "cancelled",
method: "tools/call",
target: "long_running_analysis",
progress: 30,
startedAt: "2026-03-17T10:15:00Z",
completedAt: "2026-03-17T10:18:00Z",
elapsed: "3m 00s",
startedAt: "3/29/2026, 8:18:15 PM",
completedAt: "3/29/2026, 8:18:18 PM",
lastUpdated: "3/29/2026, 8:18:18 PM",
ttl: 300000,
},
};
export const Collapsed: Story = {
args: {
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: false,
},
};

View file

@ -1,4 +1,15 @@
import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
import { useEffect, useState } from "react";
import {
Badge,
Button,
Card,
Collapse,
Divider,
Group,
Stack,
Text,
} from "@mantine/core";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
export type TaskStatus =
@ -17,12 +28,12 @@ export interface TaskCardProps {
progressDescription?: string;
startedAt?: string;
completedAt?: string;
lastUpdated?: string;
elapsed?: string;
ttl?: number;
error?: string;
onViewDetails: () => void;
onViewResult: () => void;
isListExpanded: boolean;
onCancel: () => void;
onDismiss: () => void;
}
const STATUS_COLORS: Record<TaskStatus, string> = {
@ -38,27 +49,37 @@ const TaskContainer = Card.withProps({
padding: "md",
});
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const TaskIdText = Text.withProps({
size: "sm",
ff: "monospace",
c: "dimmed",
});
const DetailText = Text.withProps({
const DetailLabel = Text.withProps({
size: "sm",
c: "dimmed",
});
const ValueSpan = Text.withProps({
component: "span",
fw: 600,
const DetailValue = Text.withProps({
size: "sm",
fw: 500,
});
const ProgressNote = Text.withProps({
const StatusMessageText = Text.withProps({
size: "sm",
c: "dimmed",
fs: "italic",
});
const ErrorText = Text.withProps({
size: "sm",
c: "red",
});
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
@ -70,89 +91,152 @@ const CancelButton = Button.withProps({
size: "xs",
});
const DetailRow = Group.withProps({
gap: "xl",
wrap: "wrap",
});
const SummaryRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const HeaderLeftGroup = Group.withProps({
gap: "sm",
});
const HeaderRightGroup = Group.withProps({
gap: "xs",
});
const SectionTitle = Text.withProps({
size: "sm",
fw: 600,
});
function formatTaskId(taskId: string): string {
return `Task: ${taskId}`;
return `ID: ${taskId}`;
}
function formatTimeline(
startedAt: string,
completedAt?: string,
elapsed?: string,
): string {
let result = `Started: ${startedAt}`;
if (completedAt) result += ` | Completed: ${completedAt}`;
if (elapsed) result += ` | Elapsed: ${elapsed}`;
return result;
function formatTtl(ttl: number): string {
return `${ttl}ms`;
}
export function TaskCard({
taskId,
status,
method,
target,
progress,
progressDescription,
startedAt,
completedAt,
elapsed,
error,
onViewDetails,
onViewResult,
onCancel,
onDismiss,
}: TaskCardProps) {
function buildTaskObject(props: TaskCardProps): string {
const obj: Record<string, unknown> = {
taskId: props.taskId,
status: props.status,
method: props.method,
};
if (props.target) obj.target = props.target;
if (props.ttl !== undefined) obj.ttl = props.ttl;
if (props.startedAt) obj.createdAt = props.startedAt;
if (props.lastUpdated) obj.lastUpdatedAt = props.lastUpdated;
if (props.completedAt) obj.completedAt = props.completedAt;
if (props.progress !== undefined) obj.progress = props.progress;
if (props.progressDescription) obj.statusMessage = props.progressDescription;
if (props.error) obj.error = props.error;
return JSON.stringify(obj);
}
export function TaskCard(props: TaskCardProps) {
const {
taskId,
status,
progress,
progressDescription,
startedAt,
lastUpdated,
ttl,
error,
isListExpanded,
onCancel,
} = props;
const [isExpanded, setIsExpanded] = useState(isListExpanded);
const isActive = status === "waiting" || status === "running";
useEffect(() => {
setIsExpanded(isListExpanded);
}, [isListExpanded]);
return (
<TaskContainer>
<Stack gap="xs">
<Group justify="space-between">
<Group>
<Stack gap="sm">
<HeaderRow>
<HeaderLeftGroup>
<SectionTitle>Task Details</SectionTitle>
<TaskIdText>{formatTaskId(taskId)}</TaskIdText>
</HeaderLeftGroup>
<HeaderRightGroup>
{isActive && (
<CancelButton onClick={onCancel}>Cancel Task</CancelButton>
)}
<Badge color={STATUS_COLORS[status]}>{status}</Badge>
</Group>
{progress !== undefined && <ProgressDisplay progress={progress} />}
</Group>
</HeaderRightGroup>
</HeaderRow>
<DetailText>
Method: <ValueSpan>{method}</ValueSpan>
</DetailText>
<SummaryRow>
<DetailRow>
<Stack gap={2}>
<DetailLabel>Status</DetailLabel>
<DetailValue>{status}</DetailValue>
</Stack>
{lastUpdated && (
<Stack gap={2}>
<DetailLabel>Last Updated</DetailLabel>
<DetailValue>{lastUpdated}</DetailValue>
</Stack>
)}
{startedAt && (
<Stack gap={2}>
<DetailLabel>Created At</DetailLabel>
<DetailValue>{startedAt}</DetailValue>
</Stack>
)}
{ttl !== undefined && (
<Stack gap={2}>
<DetailLabel>TTL</DetailLabel>
<DetailValue>{formatTtl(ttl)}</DetailValue>
</Stack>
)}
</DetailRow>
<SubtleButton onClick={() => setIsExpanded((v) => !v)}>
{isExpanded ? "Collapse" : "Expand"}
</SubtleButton>
</SummaryRow>
{target && (
<DetailText>
Tool/Resource: <ValueSpan>{target}</ValueSpan>
</DetailText>
{isExpanded && (
<Collapse in={isExpanded}>
<Stack gap="sm">
{progressDescription && (
<Stack gap={2}>
<DetailLabel>Status Message</DetailLabel>
<StatusMessageText>{progressDescription}</StatusMessageText>
</Stack>
)}
{progress !== undefined && (
<ProgressDisplay progress={progress} />
)}
{error && (
<Stack gap={2}>
<DetailLabel>Error</DetailLabel>
<ErrorText>{error}</ErrorText>
</Stack>
)}
<Divider />
<SectionTitle>Full Task Object</SectionTitle>
<ContentViewer
type="json"
content={buildTaskObject(props)}
copyable
/>
</Stack>
</Collapse>
)}
{startedAt && (
<DetailText>
{formatTimeline(startedAt, completedAt, elapsed)}
</DetailText>
)}
{progressDescription && (
<ProgressNote>{progressDescription}</ProgressNote>
)}
{error && (
<Text size="sm" c="red">
{error}
</Text>
)}
<Group justify="flex-end">
{isActive ? (
<>
<SubtleButton onClick={onViewDetails}>View Details</SubtleButton>
<CancelButton onClick={onCancel}>Cancel</CancelButton>
</>
) : (
<>
<SubtleButton onClick={onViewResult}>View Result</SubtleButton>
<SubtleButton onClick={onDismiss}>Dismiss</SubtleButton>
</>
)}
</Group>
</Stack>
</TaskContainer>
);

View file

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { TaskControls } from "./TaskControls";
const meta: Meta<typeof TaskControls> = {
title: "Groups/TaskControls",
component: TaskControls,
args: {
searchText: "",
onSearchChange: fn(),
onStatusFilterChange: fn(),
onRefresh: fn(),
onClearHistory: fn(),
},
};
export default meta;
type Story = StoryObj<typeof TaskControls>;
export const Default: Story = {};
export const WithSearch: Story = {
args: {
searchText: "generate",
},
};
export const WithFilter: Story = {
args: {
statusFilter: "running",
},
};

View file

@ -0,0 +1,60 @@
import { Button, Group, Select, Stack, TextInput, Title } from "@mantine/core";
import type { TaskStatus } from "../TaskCard/TaskCard";
const STATUS_OPTIONS: TaskStatus[] = [
"waiting",
"running",
"completed",
"failed",
"cancelled",
];
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
export interface TaskControlsProps {
searchText: string;
statusFilter?: string;
onSearchChange: (text: string) => void;
onStatusFilterChange: (status: string) => void;
onRefresh: () => void;
onClearHistory: () => void;
}
export function TaskControls({
searchText,
statusFilter,
onSearchChange,
onStatusFilterChange,
onRefresh,
onClearHistory,
}: TaskControlsProps) {
return (
<Stack gap="md">
<Title order={4}>Tasks</Title>
<Title order={5}>Search</Title>
<TextInput
placeholder="Search..."
value={searchText}
onChange={(event) => onSearchChange(event.currentTarget.value)}
/>
<Title order={6}>Filter by Status</Title>
<Select
placeholder="All statuses"
data={STATUS_OPTIONS}
value={statusFilter}
onChange={(value) => onStatusFilterChange(value ?? "")}
clearable
/>
<Group>
<ToolbarButton onClick={onRefresh}>Refresh</ToolbarButton>
<ToolbarButton onClick={onClearHistory}>Clear History</ToolbarButton>
</Group>
</Stack>
);
}

View file

@ -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: [],
},
};

View file

@ -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>
);
}

View file

@ -16,85 +16,82 @@ const meta: Meta<typeof TasksScreen> = {
export default meta;
type Story = StoryObj<typeof TasksScreen>;
const activeTask1: TaskCardProps = {
taskId: "task-001",
status: "running",
method: "tools/call",
target: "send_message",
progress: 45,
progressDescription: "Sending message to remote server...",
startedAt: "2026-03-17T10:00:00Z",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
};
const activeTask2: TaskCardProps = {
taskId: "task-002",
status: "waiting",
method: "resources/read",
target: "config.json",
startedAt: "2026-03-17T10:01:00Z",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
};
const completedTask1: TaskCardProps = {
taskId: "task-003",
status: "completed",
method: "tools/call",
target: "list_users",
startedAt: "2026-03-17T09:50:00Z",
completedAt: "2026-03-17T09:50:02Z",
elapsed: "2.1s",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
};
const completedTask2: TaskCardProps = {
taskId: "task-004",
status: "failed",
method: "tools/call",
target: "delete_records",
startedAt: "2026-03-17T09:45:00Z",
completedAt: "2026-03-17T09:45:01Z",
elapsed: "0.8s",
error: "Permission denied: insufficient privileges",
onViewDetails: fn(),
onViewResult: fn(),
onCancel: fn(),
onDismiss: fn(),
};
const sampleTasks: TaskCardProps[] = [
{
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "running",
method: "tools/call",
target: "batch_process",
progress: 65,
progressDescription: "Processing records 650 of 1000...",
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/large-dataset.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: "send_message",
startedAt: "3/29/2026, 8:16:47 PM",
completedAt: "3/29/2026, 8:16:49 PM",
lastUpdated: "3/29/2026, 8:16:49 PM",
ttl: 300000,
isListExpanded: true,
onCancel: fn(),
},
{
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed",
method: "tools/call",
target: "delete_records",
startedAt: "3/29/2026, 8:17:27 PM",
completedAt: "3/29/2026, 8:17:57 PM",
lastUpdated: "3/29/2026, 8:17:57 PM",
ttl: 300000,
error: "Timeout waiting for tool response after 30s",
isListExpanded: true,
onCancel: fn(),
},
];
export const Mixed: Story = {
args: {
activeTasks: [activeTask1, activeTask2],
completedTasks: [completedTask1, completedTask2],
tasks: sampleTasks,
},
};
export const ActiveOnly: Story = {
args: {
activeTasks: [activeTask1, activeTask2],
completedTasks: [],
tasks: sampleTasks.filter(
(t) => t.status === "running" || t.status === "waiting",
),
},
};
export const CompletedOnly: Story = {
args: {
activeTasks: [],
completedTasks: [completedTask1, completedTask2],
tasks: sampleTasks.filter(
(t) => t.status !== "running" && t.status !== "waiting",
),
},
};
export const Empty: Story = {
args: {
activeTasks: [],
completedTasks: [],
tasks: [],
},
};

View file

@ -1,88 +1,61 @@
import {
Button,
Container,
Group,
Paper,
Stack,
Text,
Title,
} from "@mantine/core";
import { TaskCard } from "../../groups/TaskCard/TaskCard";
import { useState } from "react";
import { Card, Flex, Stack } from "@mantine/core";
import { TaskControls } from "../../groups/TaskControls/TaskControls";
import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel";
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
export interface TasksScreenProps {
activeTasks: TaskCardProps[];
completedTasks: TaskCardProps[];
tasks: TaskCardProps[];
onRefresh: () => void;
onClearHistory: () => void;
}
const PageContainer = Container.withProps({
size: "xl",
py: "xl",
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100vh - var(--app-shell-header-height, 0px))",
gap: "xl",
p: "xl",
});
const ContentPanel = Paper.withProps({
const Sidebar = Stack.withProps({
w: 280,
flex: "0 0 auto",
});
const SidebarCard = Card.withProps({
withBorder: true,
p: "md",
padding: "lg",
});
const CompactButton = Button.withProps({
variant: "light",
size: "xs",
});
function formatSectionTitle(label: string, count: number): string {
return `${label} (${count})`;
}
export function TasksScreen({
activeTasks,
completedTasks,
tasks,
onRefresh,
onClearHistory,
}: TasksScreenProps) {
return (
<PageContainer>
<ContentPanel>
<Stack gap="lg">
<Group justify="space-between">
<Title order={4}>
{formatSectionTitle("Active Tasks", activeTasks.length)}
</Title>
<CompactButton onClick={onRefresh}>Refresh Tasks</CompactButton>
</Group>
{activeTasks.length === 0 ? (
<Text c="dimmed">No active tasks</Text>
) : (
<Stack gap="md">
{activeTasks.map((task) => (
<TaskCard key={task.taskId} {...task} />
))}
</Stack>
)}
const [searchText, setSearchText] = useState("");
const [statusFilter, setStatusFilter] = useState<string | undefined>();
<Group justify="space-between">
<Title order={4}>
{formatSectionTitle("Completed Tasks", completedTasks.length)}
</Title>
{completedTasks.length > 0 && (
<CompactButton onClick={onClearHistory}>
Clear History
</CompactButton>
)}
</Group>
{completedTasks.length === 0 ? (
<Text c="dimmed">No completed tasks</Text>
) : (
<Stack gap="md">
{completedTasks.map((task) => (
<TaskCard key={task.taskId} {...task} />
))}
</Stack>
)}
</Stack>
</ContentPanel>
</PageContainer>
return (
<ScreenLayout>
<Sidebar>
<SidebarCard>
<TaskControls
searchText={searchText}
statusFilter={statusFilter}
onSearchChange={setSearchText}
onStatusFilterChange={(value) =>
setStatusFilter(value || undefined)
}
onRefresh={onRefresh}
onClearHistory={onClearHistory}
/>
</SidebarCard>
</Sidebar>
<TaskListPanel
tasks={tasks}
searchText={searchText}
statusFilter={statusFilter}
/>
</ScreenLayout>
);
}

View file

@ -390,59 +390,55 @@ export const TasksActive: Story = {
activeTab: "Tasks",
children: (
<TasksScreen
activeTasks={[
tasks={[
{
taskId: "task-001",
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "running",
method: "tools/call",
target: "batch_process",
progress: 65,
progressDescription: "Processing records 650 of 1000...",
startedAt: "2026-03-17T10:05:00Z",
onViewDetails: fn(),
onViewResult: fn(),
startedAt: "3/29/2026, 8:18:20 PM",
lastUpdated: "3/29/2026, 8:18:22 PM",
ttl: 300000,
isListExpanded: true,
onCancel: fn(),
onDismiss: fn(),
},
{
taskId: "task-002",
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "waiting",
method: "resources/read",
target: "file:///data/large-dataset.csv",
startedAt: "2026-03-17T10:06:12Z",
onViewDetails: fn(),
onViewResult: fn(),
startedAt: "3/29/2026, 8:17:55 PM",
lastUpdated: "3/29/2026, 8:17:55 PM",
ttl: 300000,
isListExpanded: true,
onCancel: fn(),
onDismiss: fn(),
},
]}
completedTasks={[
{
taskId: "task-000",
taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed",
method: "tools/call",
target: "send_message",
startedAt: "2026-03-17T09:58:00Z",
completedAt: "2026-03-17T09:58:02Z",
elapsed: "2s",
onViewDetails: fn(),
onViewResult: fn(),
startedAt: "3/29/2026, 8:16:47 PM",
completedAt: "3/29/2026, 8:16:49 PM",
lastUpdated: "3/29/2026, 8:16:49 PM",
ttl: 300000,
isListExpanded: true,
onCancel: fn(),
onDismiss: fn(),
},
{
taskId: "task-099",
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed",
method: "tools/call",
target: "delete_records",
startedAt: "2026-03-17T09:50:00Z",
completedAt: "2026-03-17T09:50:30Z",
elapsed: "30s",
startedAt: "3/29/2026, 8:17:27 PM",
completedAt: "3/29/2026, 8:17:57 PM",
lastUpdated: "3/29/2026, 8:17:57 PM",
ttl: 300000,
error: "Timeout waiting for tool response after 30s",
onViewDetails: fn(),
onViewResult: fn(),
isListExpanded: true,
onCancel: fn(),
onDismiss: fn(),
},
]}
onRefresh={fn()}