Merge pull request #1217 from modelcontextprotocol/v2/phase-2-tasks
Phase 2: Refactor Tasks cluster to MCP SDK types
This commit is contained in:
commit
0db955ea7e
8 changed files with 197 additions and 276 deletions
|
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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...",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue