Merge pull request #1217 from modelcontextprotocol/v2/phase-2-tasks

Phase 2: Refactor Tasks cluster to MCP SDK types
This commit is contained in:
Cliff Hall 2026-04-20 16:14:47 -04:00 • committed by GitHub
commit 0db955ea7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 197 additions and 276 deletions

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Task } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { TaskCard } from "./TaskCard"; import { TaskCard } from "./TaskCard";
@ -14,84 +15,78 @@ const meta: Meta<typeof TaskCard> = {
export default meta; export default meta;
type Story = StoryObj<typeof TaskCard>; type Story = StoryObj<typeof TaskCard>;
const workingTask: Task = {
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "working",
ttl: 300000,
createdAt: "2026-03-29T20:18:20Z",
lastUpdatedAt: "2026-03-29T20:18:22Z",
statusMessage: "Synthesizing findings...",
};
const inputRequiredTask: Task = {
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "input_required",
ttl: 300000,
createdAt: "2026-03-29T20:17:55Z",
lastUpdatedAt: "2026-03-29T20:17:55Z",
statusMessage: "Waiting for user confirmation",
};
const completedTask: Task = {
taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed",
ttl: 300000,
createdAt: "2026-03-29T20:16:47Z",
lastUpdatedAt: "2026-03-29T20:16:51Z",
statusMessage: "Report generated successfully",
};
const failedTask: Task = {
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed",
ttl: 300000,
createdAt: "2026-03-29T20:17:27Z",
lastUpdatedAt: "2026-03-29T20:17:28Z",
statusMessage:
"Connection refused: upstream server not responding after 3 retries",
};
const cancelledTask: Task = {
taskId: "c416bc183cb04468d3f81696f1b868f6",
status: "cancelled",
ttl: 300000,
createdAt: "2026-03-29T20:18:15Z",
lastUpdatedAt: "2026-03-29T20:18:18Z",
};
export const Working: Story = { export const Working: Story = {
args: { args: {
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", task: workingTask,
status: "working", progress: {
method: "tools/call", progress: 650,
target: "generate_report", total: 1000,
progress: 80, message: "Processing records 650 of 1000...",
progressDescription: "Synthesizing findings...", },
startedAt: "3/29/2026, 8:18:20 PM",
lastUpdated: "3/29/2026, 8:18:22 PM",
ttl: 300000,
}, },
}; };
export const InputRequired: Story = { export const InputRequired: Story = {
args: { args: { task: inputRequiredTask },
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "input_required",
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,
},
}; };
export const Completed: Story = { export const Completed: Story = {
args: { args: { task: completedTask },
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...",
},
}; };
export const Failed: Story = { export const Failed: Story = {
args: { args: { task: failedTask },
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed",
method: "tools/call",
target: "fetch_remote_data",
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 = { export const Cancelled: Story = {
args: { args: { task: cancelledTask },
taskId: "c416bc183cb04468d3f81696f1b868f6",
status: "cancelled",
method: "tools/call",
target: "long_running_analysis",
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 = { export const Collapsed: Story = {
args: { args: { task: workingTask, isListExpanded: false },
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "working",
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

@ -8,26 +8,22 @@ import {
Stack, Stack,
Text, Text,
} from "@mantine/core"; } from "@mantine/core";
import type {
ProgressNotification,
Task,
} from "@modelcontextprotocol/sdk/types.js";
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
import { TaskStatusBadge } from "../../elements/TaskStatusBadge/TaskStatusBadge"; import { TaskStatusBadge } from "../../elements/TaskStatusBadge/TaskStatusBadge";
import type { TaskStatus } from "@modelcontextprotocol/sdk/types.js"; export type TaskProgress = Pick<
export type { TaskStatus }; ProgressNotification["params"],
"progress" | "total" | "message"
>;
export interface TaskCardProps { export interface TaskCardProps {
taskId: string; task: Task;
status: TaskStatus; progress?: TaskProgress;
method: string;
target?: string;
progress?: number;
progressDescription?: string;
startedAt?: string;
completedAt?: string;
lastUpdated?: string;
elapsed?: string;
ttl?: number;
error?: string;
isListExpanded: boolean; isListExpanded: boolean;
onCancel: () => void; onCancel: () => void;
} }
@ -63,11 +59,6 @@ const StatusMessageText = Text.withProps({
fs: "italic", fs: "italic",
}); });
const ErrorText = Text.withProps({
size: "sm",
c: "red",
});
const SubtleButton = Button.withProps({ const SubtleButton = Button.withProps({
variant: "subtle", variant: "subtle",
size: "xs", size: "xs",
@ -110,37 +101,13 @@ function formatTtl(ttl: number): string {
return `${ttl}ms`; return `${ttl}ms`;
} }
function buildTaskObject(props: TaskCardProps): string { export function TaskCard({
const obj: Record<string, unknown> = { task,
taskId: props.taskId, progress,
status: props.status, isListExpanded,
method: props.method, onCancel,
}; }: TaskCardProps) {
if (props.target) obj.target = props.target; const { taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage } = task;
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 [isExpanded, setIsExpanded] = useState(isListExpanded);
const isActive = status === "working" || status === "input_required"; const isActive = status === "working" || status === "input_required";
@ -167,22 +134,14 @@ export function TaskCard(props: TaskCardProps) {
<SummaryRow> <SummaryRow>
<DetailRow> <DetailRow>
<Stack gap={2}> <Stack gap={2}>
<DetailLabel>Status</DetailLabel> <DetailLabel>Last Updated</DetailLabel>
<DetailValue>{status}</DetailValue> <DetailValue>{lastUpdatedAt}</DetailValue>
</Stack> </Stack>
{lastUpdated && ( <Stack gap={2}>
<Stack gap={2}> <DetailLabel>Created At</DetailLabel>
<DetailLabel>Last Updated</DetailLabel> <DetailValue>{createdAt}</DetailValue>
<DetailValue>{lastUpdated}</DetailValue> </Stack>
</Stack> {ttl != null && (
)}
{startedAt && (
<Stack gap={2}>
<DetailLabel>Created At</DetailLabel>
<DetailValue>{startedAt}</DetailValue>
</Stack>
)}
{ttl !== undefined && (
<Stack gap={2}> <Stack gap={2}>
<DetailLabel>TTL</DetailLabel> <DetailLabel>TTL</DetailLabel>
<DetailValue>{formatTtl(ttl)}</DetailValue> <DetailValue>{formatTtl(ttl)}</DetailValue>
@ -194,41 +153,24 @@ export function TaskCard(props: TaskCardProps) {
</SubtleButton> </SubtleButton>
</SummaryRow> </SummaryRow>
{progress !== undefined && isActive && ( {progress && isActive ? (
<ProgressDisplay <ProgressDisplay params={progress} />
params={{ ) : (
progress, statusMessage && (
message: progressDescription, <StatusMessageText>{statusMessage}</StatusMessageText>
}} )
/>
)} )}
{isExpanded && ( <Collapse in={isExpanded}>
<Collapse in={isExpanded}> <Stack gap="sm">
<Stack gap="sm"> <Divider />
{progressDescription && !isActive && ( <SectionTitle>Full Task Object</SectionTitle>
<Stack gap={2}> <ContentViewer
<DetailLabel>Status Message</DetailLabel> block={{ type: "text", text: JSON.stringify(task) }}
<StatusMessageText>{progressDescription}</StatusMessageText> copyable
</Stack> />
)} </Stack>
</Collapse>
{error && (
<Stack gap={2}>
<DetailLabel>Error</DetailLabel>
<ErrorText>{error}</ErrorText>
</Stack>
)}
<Divider />
<SectionTitle>Full Task Object</SectionTitle>
<ContentViewer
block={{ type: "text", text: buildTaskObject(props) }}
copyable
/>
</Stack>
</Collapse>
)}
</Stack> </Stack>
</TaskContainer> </TaskContainer>
); );

View file

@ -16,9 +16,9 @@ const ToolbarButton = Button.withProps({
export interface TaskControlsProps { export interface TaskControlsProps {
searchText: string; searchText: string;
statusFilter?: string; statusFilter?: TaskStatus;
onSearchChange: (text: string) => void; onSearchChange: (text: string) => void;
onStatusFilterChange: (status: string) => void; onStatusFilterChange: (status: TaskStatus | undefined) => void;
onRefresh: () => void; onRefresh: () => void;
} }
@ -47,7 +47,13 @@ export function TaskControls({
placeholder="All statuses" placeholder="All statuses"
data={STATUS_OPTIONS} data={STATUS_OPTIONS}
value={statusFilter ?? null} value={statusFilter ?? null}
onChange={(value) => onStatusFilterChange(value ?? "")} onChange={(value) =>
onStatusFilterChange(
value && STATUS_OPTIONS.includes(value as TaskStatus)
? (value as TaskStatus)
: undefined,
)
}
clearable clearable
/> />
</Stack> </Stack>

View file

@ -1,57 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Task } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { TaskListPanel } from "./TaskListPanel"; import { TaskListPanel } from "./TaskListPanel";
import type { TaskCardProps } from "../TaskCard/TaskCard";
const sampleTasks: TaskCardProps[] = [ const sampleTasks: Task[] = [
{ {
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "working", status: "working",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:18:20Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:18:22Z",
statusMessage: "Synthesizing findings...",
}, },
{ {
taskId: "4100b5e0b0ed9cd0023330342d1bf647", taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "input_required", status: "input_required",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:17:55Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:17:55Z",
}, },
{ {
taskId: "d487b49aa39023d907b5a2a5b506cb3", taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed", 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, ttl: 300000,
progressDescription: "Generating report...", createdAt: "2026-03-29T20:16:47Z",
isListExpanded: true, lastUpdatedAt: "2026-03-29T20:16:51Z",
onCancel: fn(), statusMessage: "Report generated successfully",
}, },
{ {
taskId: "e6ebffd9cca84ddd1646d3c579a4d453", taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed", 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, ttl: 300000,
error: "Connection refused: upstream server not responding", createdAt: "2026-03-29T20:17:27Z",
isListExpanded: true, lastUpdatedAt: "2026-03-29T20:17:28Z",
onCancel: fn(), statusMessage: "Connection refused: upstream server not responding",
}, },
]; ];
@ -61,6 +43,7 @@ const meta: Meta<typeof TaskListPanel> = {
args: { args: {
tasks: sampleTasks, tasks: sampleTasks,
searchText: "", searchText: "",
onCancel: fn(),
onClearCompleted: fn(), onClearCompleted: fn(),
}, },
}; };
@ -72,7 +55,7 @@ export const Default: Story = {};
export const FilteredByStatus: Story = { export const FilteredByStatus: Story = {
args: { args: {
statusFilter: "running", statusFilter: "working",
}, },
}; };

View file

@ -8,14 +8,17 @@ import {
Text, Text,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js";
import { TaskCard } from "../TaskCard/TaskCard"; import { TaskCard } from "../TaskCard/TaskCard";
import type { TaskProgress } from "../TaskCard/TaskCard";
import { ListToggle } from "../../elements/ListToggle/ListToggle"; import { ListToggle } from "../../elements/ListToggle/ListToggle";
import type { TaskCardProps } from "../TaskCard/TaskCard";
export interface TaskListPanelProps { export interface TaskListPanelProps {
tasks: TaskCardProps[]; tasks: Task[];
progressByTaskId?: Record<string, TaskProgress>;
searchText: string; searchText: string;
statusFilter?: string; statusFilter?: TaskStatus;
onCancel: (taskId: string) => void;
onClearCompleted: () => void; onClearCompleted: () => void;
} }
@ -37,10 +40,6 @@ const EmptyState = Text.withProps({
py: "xl", py: "xl",
}); });
function taskKey(task: TaskCardProps): string {
return task.taskId;
}
function formatActiveTitle(count: number): string { function formatActiveTitle(count: number): string {
return `Active (${count})`; return `Active (${count})`;
} }
@ -49,20 +48,20 @@ function formatCompletedTitle(count: number): string {
return `Completed (${count})`; return `Completed (${count})`;
} }
function isActiveStatus(status: string): boolean { function isActiveStatus(status: TaskStatus): boolean {
return status === "working" || status === "input_required"; return status === "working" || status === "input_required";
} }
function matchesFilters( function matchesFilters(
task: TaskCardProps, task: Task,
searchText: string, searchText: string,
statusFilter?: string, statusFilter?: TaskStatus,
): boolean { ): boolean {
if (statusFilter && task.status !== statusFilter) return false; if (statusFilter && task.status !== statusFilter) return false;
if (searchText) { if (searchText) {
const term = searchText.toLowerCase(); const term = searchText.toLowerCase();
const searchable = const searchable =
`${task.taskId} ${task.method} ${task.target ?? ""} ${task.status}`.toLowerCase(); `${task.taskId} ${task.status} ${task.statusMessage ?? ""}`.toLowerCase();
if (!searchable.includes(term)) return false; if (!searchable.includes(term)) return false;
} }
return true; return true;
@ -70,8 +69,10 @@ function matchesFilters(
export function TaskListPanel({ export function TaskListPanel({
tasks, tasks,
progressByTaskId,
searchText, searchText,
statusFilter, statusFilter,
onCancel,
onClearCompleted, onClearCompleted,
}: TaskListPanelProps) { }: TaskListPanelProps) {
const [compact, setCompact] = useState(false); const [compact, setCompact] = useState(false);
@ -115,9 +116,11 @@ export function TaskListPanel({
<Title order={5}>{formatActiveTitle(activeTasks.length)}</Title> <Title order={5}>{formatActiveTitle(activeTasks.length)}</Title>
{activeTasks.map((task) => ( {activeTasks.map((task) => (
<TaskCard <TaskCard
key={taskKey(task)} key={task.taskId}
{...task} task={task}
progress={progressByTaskId?.[task.taskId]}
isListExpanded={!compact} isListExpanded={!compact}
onCancel={() => onCancel(task.taskId)}
/> />
))} ))}
</> </>
@ -135,9 +138,11 @@ export function TaskListPanel({
</Group> </Group>
{completedTasks.map((task) => ( {completedTasks.map((task) => (
<TaskCard <TaskCard
key={taskKey(task)} key={task.taskId}
{...task} task={task}
progress={progressByTaskId?.[task.taskId]}
isListExpanded={!compact} isListExpanded={!compact}
onCancel={() => onCancel(task.taskId)}
/> />
))} ))}
</> </>

View file

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite"; import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Task } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test"; import { fn } from "storybook/test";
import { TasksScreen } from "./TasksScreen"; import { TasksScreen } from "./TasksScreen";
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
const meta: Meta<typeof TasksScreen> = { const meta: Meta<typeof TasksScreen> = {
title: "Screens/TasksScreen", title: "Screens/TasksScreen",
@ -10,67 +10,56 @@ const meta: Meta<typeof TasksScreen> = {
args: { args: {
onRefresh: fn(), onRefresh: fn(),
onClearCompleted: fn(), onClearCompleted: fn(),
onCancel: fn(),
}, },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof TasksScreen>; type Story = StoryObj<typeof TasksScreen>;
const sampleTasks: TaskCardProps[] = [ const sampleTasks: Task[] = [
{ {
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "working", status: "working",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:18:20Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:18:22Z",
statusMessage: "Processing records 650 of 1000...",
}, },
{ {
taskId: "4100b5e0b0ed9cd0023330342d1bf647", taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "input_required", status: "input_required",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:17:55Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:17:55Z",
}, },
{ {
taskId: "d487b49aa39023d907b5a2a5b506cb3", taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed", 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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:16:47Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:16:49Z",
}, },
{ {
taskId: "e6ebffd9cca84ddd1646d3c579a4d453", taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed", 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, ttl: 300000,
error: "Timeout waiting for tool response after 30s", createdAt: "2026-03-29T20:17:27Z",
isListExpanded: true, lastUpdatedAt: "2026-03-29T20:17:57Z",
onCancel: fn(), statusMessage: "Timeout waiting for tool response after 30s",
}, },
]; ];
export const Mixed: Story = { export const Mixed: Story = {
args: { args: {
tasks: sampleTasks, tasks: sampleTasks,
progressByTaskId: {
d0b22eba71fa36229ce5c4dfadeaa7de: {
progress: 650,
total: 1000,
message: "Processing records 650 of 1000...",
},
},
}, },
}; };
@ -79,6 +68,13 @@ export const ActiveOnly: Story = {
tasks: sampleTasks.filter( tasks: sampleTasks.filter(
(t) => t.status === "working" || t.status === "input_required", (t) => t.status === "working" || t.status === "input_required",
), ),
progressByTaskId: {
d0b22eba71fa36229ce5c4dfadeaa7de: {
progress: 650,
total: 1000,
message: "Processing records 650 of 1000...",
},
},
}, },
}; };

View file

@ -1,13 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { Card, Flex, Stack } from "@mantine/core"; import { Card, Flex, Stack } from "@mantine/core";
import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js";
import { TaskControls } from "../../groups/TaskControls/TaskControls"; import { TaskControls } from "../../groups/TaskControls/TaskControls";
import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel"; import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel";
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; import type { TaskProgress } from "../../groups/TaskCard/TaskCard";
export interface TasksScreenProps { export interface TasksScreenProps {
tasks: TaskCardProps[]; tasks: Task[];
progressByTaskId?: Record<string, TaskProgress>;
onRefresh: () => void; onRefresh: () => void;
onClearCompleted: () => void; onClearCompleted: () => void;
onCancel: (taskId: string) => void;
} }
const ScreenLayout = Flex.withProps({ const ScreenLayout = Flex.withProps({
@ -29,11 +32,13 @@ const SidebarCard = Card.withProps({
export function TasksScreen({ export function TasksScreen({
tasks, tasks,
progressByTaskId,
onRefresh, onRefresh,
onClearCompleted, onClearCompleted,
onCancel,
}: TasksScreenProps) { }: TasksScreenProps) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [statusFilter, setStatusFilter] = useState<string | undefined>(); const [statusFilter, setStatusFilter] = useState<TaskStatus | undefined>();
return ( return (
<ScreenLayout> <ScreenLayout>
@ -43,17 +48,17 @@ export function TasksScreen({
searchText={searchText} searchText={searchText}
statusFilter={statusFilter} statusFilter={statusFilter}
onSearchChange={setSearchText} onSearchChange={setSearchText}
onStatusFilterChange={(value) => onStatusFilterChange={setStatusFilter}
setStatusFilter(value || undefined)
}
onRefresh={onRefresh} onRefresh={onRefresh}
/> />
</SidebarCard> </SidebarCard>
</Sidebar> </Sidebar>
<TaskListPanel <TaskListPanel
tasks={tasks} tasks={tasks}
progressByTaskId={progressByTaskId}
searchText={searchText} searchText={searchText}
statusFilter={statusFilter} statusFilter={statusFilter}
onCancel={onCancel}
onClearCompleted={onClearCompleted} onClearCompleted={onClearCompleted}
/> />
</ScreenLayout> </ScreenLayout>

View file

@ -549,55 +549,44 @@ export const TasksActive: Story = {
{ {
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
status: "working", status: "working",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:18:20Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:18:22Z",
statusMessage: "Processing records 650 of 1000...",
}, },
{ {
taskId: "4100b5e0b0ed9cd0023330342d1bf647", taskId: "4100b5e0b0ed9cd0023330342d1bf647",
status: "input_required", status: "input_required",
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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:17:55Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:17:55Z",
}, },
{ {
taskId: "d487b49aa39023d907b5a2a5b506cb3", taskId: "d487b49aa39023d907b5a2a5b506cb3",
status: "completed", 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, ttl: 300000,
isListExpanded: true, createdAt: "2026-03-29T20:16:47Z",
onCancel: fn(), lastUpdatedAt: "2026-03-29T20:16:49Z",
}, },
{ {
taskId: "e6ebffd9cca84ddd1646d3c579a4d453", taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
status: "failed", 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, ttl: 300000,
error: "Timeout waiting for tool response after 30s", createdAt: "2026-03-29T20:17:27Z",
isListExpanded: true, lastUpdatedAt: "2026-03-29T20:17:57Z",
onCancel: fn(), statusMessage: "Timeout waiting for tool response after 30s",
}, },
]} ]}
progressByTaskId={{
d0b22eba71fa36229ce5c4dfadeaa7de: {
progress: 650,
total: 1000,
message: "Processing records 650 of 1000...",
},
}}
onRefresh={fn()} onRefresh={fn()}
onClearCompleted={fn()} onClearCompleted={fn()}
onCancel={fn()}
/> />
), ),
}, },