From 6abb39a5fe875a677a016eb95e8b09fce645058b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 10:28:24 -0400 Subject: [PATCH 1/7] Phase 2: Refactor Tasks cluster to MCP SDK types Replace ad hoc TaskCardProps data shape with SDK Task type across the Tasks group components and TasksScreen. - TaskCard: accept `task: Task` instead of flat props (taskId, status, method, target, progress, etc.); derive all display fields from the SDK Task object (taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage); remove fields not present on SDK Task (method, target, progress as number, error, completedAt, elapsed) - TaskControls: type statusFilter as `TaskStatus | undefined`; add runtime validation guard on Select onChange - TaskListPanel: accept `Task[]` instead of `TaskCardProps[]`; pass `onCancel(taskId)` through to each TaskCard; use `task.taskId` as key - TasksScreen: accept `Task[]` + `onCancel(taskId)` callback; type internal statusFilter state as `TaskStatus | undefined` - Delete re-export of `TaskStatus` from TaskCard (import directly from SDK where needed) - Update all stories with real SDK `Task` fixtures - Update ConnectedView TasksActive story Co-Authored-By: Claude Opus 4.6 (1M context) --- .../groups/TaskCard/TaskCard.stories.tsx | 116 ++++++++---------- .../components/groups/TaskCard/TaskCard.tsx | 100 +++------------ .../groups/TaskControls/TaskControls.tsx | 12 +- .../TaskListPanel/TaskListPanel.stories.tsx | 47 +++---- .../groups/TaskListPanel/TaskListPanel.tsx | 30 ++--- .../TasksScreen/TasksScreen.stories.tsx | 44 ++----- .../screens/TasksScreen/TasksScreen.tsx | 13 +- .../ConnectedView/ConnectedView.stories.tsx | 40 ++---- 8 files changed, 140 insertions(+), 262 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx index 1c39bfe8..bc077989 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TaskCard } from "./TaskCard"; @@ -14,84 +15,71 @@ const meta: Meta = { export default meta; type Story = StoryObj; +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 = { - args: { - 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, - }, + args: { task: workingTask }, }; export const InputRequired: Story = { - args: { - 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, - }, + args: { task: inputRequiredTask }, }; export const Completed: Story = { - args: { - 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...", - }, + args: { task: completedTask }, }; export const Failed: Story = { - args: { - 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", - }, + args: { task: failedTask }, }; export const Cancelled: Story = { - args: { - 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, - }, + args: { task: cancelledTask }, }; export const Collapsed: Story = { - args: { - 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, - }, + args: { task: workingTask, isListExpanded: false }, }; diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index 1ff14d07..d183dce7 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -8,26 +8,12 @@ import { Stack, Text, } from "@mantine/core"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; -import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; import { TaskStatusBadge } from "../../elements/TaskStatusBadge/TaskStatusBadge"; -import type { TaskStatus } from "@modelcontextprotocol/sdk/types.js"; -export type { TaskStatus }; - export interface TaskCardProps { - taskId: string; - status: TaskStatus; - method: string; - target?: string; - progress?: number; - progressDescription?: string; - startedAt?: string; - completedAt?: string; - lastUpdated?: string; - elapsed?: string; - ttl?: number; - error?: string; + task: Task; isListExpanded: boolean; onCancel: () => void; } @@ -63,11 +49,6 @@ const StatusMessageText = Text.withProps({ fs: "italic", }); -const ErrorText = Text.withProps({ - size: "sm", - c: "red", -}); - const SubtleButton = Button.withProps({ variant: "subtle", size: "xs", @@ -110,37 +91,8 @@ function formatTtl(ttl: number): string { return `${ttl}ms`; } -function buildTaskObject(props: TaskCardProps): string { - const obj: Record = { - 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; - +export function TaskCard({ task, isListExpanded, onCancel }: TaskCardProps) { + const { taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage } = task; const [isExpanded, setIsExpanded] = useState(isListExpanded); const isActive = status === "working" || status === "input_required"; @@ -170,19 +122,15 @@ export function TaskCard(props: TaskCardProps) { Status {status} - {lastUpdated && ( - - Last Updated - {lastUpdated} - - )} - {startedAt && ( - - Created At - {startedAt} - - )} - {ttl !== undefined && ( + + Last Updated + {lastUpdatedAt} + + + Created At + {createdAt} + + {ttl !== null && ( TTL {formatTtl(ttl)} @@ -194,36 +142,24 @@ export function TaskCard(props: TaskCardProps) { - {progress !== undefined && isActive && ( - + {statusMessage && isActive && ( + {statusMessage} )} {isExpanded && ( - {progressDescription && !isActive && ( + {statusMessage && !isActive && ( Status Message - {progressDescription} - - )} - - {error && ( - - Error - {error} + {statusMessage} )} Full Task Object diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.tsx index 720491f6..d2f346b5 100644 --- a/clients/web/src/components/groups/TaskControls/TaskControls.tsx +++ b/clients/web/src/components/groups/TaskControls/TaskControls.tsx @@ -16,9 +16,9 @@ const ToolbarButton = Button.withProps({ export interface TaskControlsProps { searchText: string; - statusFilter?: string; + statusFilter?: TaskStatus; onSearchChange: (text: string) => void; - onStatusFilterChange: (status: string) => void; + onStatusFilterChange: (status: TaskStatus | undefined) => void; onRefresh: () => void; } @@ -47,7 +47,13 @@ export function TaskControls({ placeholder="All statuses" data={STATUS_OPTIONS} value={statusFilter ?? null} - onChange={(value) => onStatusFilterChange(value ?? "")} + onChange={(value) => + onStatusFilterChange( + value && STATUS_OPTIONS.includes(value as TaskStatus) + ? (value as TaskStatus) + : undefined, + ) + } clearable /> diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx index b516b9d2..2a6efa56 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.stories.tsx @@ -1,57 +1,39 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TaskListPanel } from "./TaskListPanel"; -import type { TaskCardProps } from "../TaskCard/TaskCard"; -const sampleTasks: TaskCardProps[] = [ +const sampleTasks: Task[] = [ { 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: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Synthesizing findings...", }, { 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, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { 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(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:51Z", + statusMessage: "Report generated successfully", }, { 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(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:28Z", + statusMessage: "Connection refused: upstream server not responding", }, ]; @@ -61,6 +43,7 @@ const meta: Meta = { args: { tasks: sampleTasks, searchText: "", + onCancel: fn(), onClearCompleted: fn(), }, }; @@ -72,7 +55,7 @@ export const Default: Story = {}; export const FilteredByStatus: Story = { args: { - statusFilter: "running", + statusFilter: "working", }, }; diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx index 57dcad0b..1096081c 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx @@ -8,14 +8,15 @@ import { Text, Title, } from "@mantine/core"; +import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskCard } from "../TaskCard/TaskCard"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; -import type { TaskCardProps } from "../TaskCard/TaskCard"; export interface TaskListPanelProps { - tasks: TaskCardProps[]; + tasks: Task[]; searchText: string; - statusFilter?: string; + statusFilter?: TaskStatus; + onCancel: (taskId: string) => void; onClearCompleted: () => void; } @@ -37,10 +38,6 @@ const EmptyState = Text.withProps({ py: "xl", }); -function taskKey(task: TaskCardProps): string { - return task.taskId; -} - function formatActiveTitle(count: number): string { return `Active (${count})`; } @@ -49,20 +46,20 @@ function formatCompletedTitle(count: number): string { return `Completed (${count})`; } -function isActiveStatus(status: string): boolean { +function isActiveStatus(status: TaskStatus): boolean { return status === "working" || status === "input_required"; } function matchesFilters( - task: TaskCardProps, + task: Task, searchText: string, - statusFilter?: string, + statusFilter?: TaskStatus, ): 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(); + `${task.taskId} ${task.status} ${task.statusMessage ?? ""}`.toLowerCase(); if (!searchable.includes(term)) return false; } return true; @@ -72,6 +69,7 @@ export function TaskListPanel({ tasks, searchText, statusFilter, + onCancel, onClearCompleted, }: TaskListPanelProps) { const [compact, setCompact] = useState(false); @@ -115,9 +113,10 @@ export function TaskListPanel({ {formatActiveTitle(activeTasks.length)} {activeTasks.map((task) => ( onCancel(task.taskId)} /> ))} @@ -135,9 +134,10 @@ export function TaskListPanel({ {completedTasks.map((task) => ( onCancel(task.taskId)} /> ))} diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index 40085855..1f6883a0 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Task } from "@modelcontextprotocol/sdk/types.js"; import { fn } from "storybook/test"; import { TasksScreen } from "./TasksScreen"; -import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; const meta: Meta = { title: "Screens/TasksScreen", @@ -10,61 +10,43 @@ const meta: Meta = { args: { onRefresh: fn(), onClearCompleted: fn(), + onCancel: fn(), }, }; export default meta; type Story = StoryObj; -const sampleTasks: TaskCardProps[] = [ +const sampleTasks: Task[] = [ { taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", 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, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Processing records 650 of 1000...", }, { taskId: "4100b5e0b0ed9cd0023330342d1bf647", 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, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { 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(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:49Z", }, { 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(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:57Z", + statusMessage: "Timeout waiting for tool response after 30s", }, ]; diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx index fd7b4417..b6a0730c 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx @@ -1,13 +1,14 @@ import { useState } from "react"; import { Card, Flex, Stack } from "@mantine/core"; +import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskControls } from "../../groups/TaskControls/TaskControls"; import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel"; -import type { TaskCardProps } from "../../groups/TaskCard/TaskCard"; export interface TasksScreenProps { - tasks: TaskCardProps[]; + tasks: Task[]; onRefresh: () => void; onClearCompleted: () => void; + onCancel: (taskId: string) => void; } const ScreenLayout = Flex.withProps({ @@ -31,9 +32,10 @@ export function TasksScreen({ tasks, onRefresh, onClearCompleted, + onCancel, }: TasksScreenProps) { const [searchText, setSearchText] = useState(""); - const [statusFilter, setStatusFilter] = useState(); + const [statusFilter, setStatusFilter] = useState(); return ( @@ -43,9 +45,7 @@ export function TasksScreen({ searchText={searchText} statusFilter={statusFilter} onSearchChange={setSearchText} - onStatusFilterChange={(value) => - setStatusFilter(value || undefined) - } + onStatusFilterChange={setStatusFilter} onRefresh={onRefresh} /> @@ -54,6 +54,7 @@ export function TasksScreen({ tasks={tasks} searchText={searchText} statusFilter={statusFilter} + onCancel={onCancel} onClearCompleted={onClearCompleted} /> diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index 5c70c104..09b15693 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -549,55 +549,37 @@ export const TasksActive: Story = { { taskId: "d0b22eba71fa36229ce5c4dfadeaa7de", 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, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:18:20Z", + lastUpdatedAt: "2026-03-29T20:18:22Z", + statusMessage: "Processing records 650 of 1000...", }, { taskId: "4100b5e0b0ed9cd0023330342d1bf647", 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, - isListExpanded: true, - onCancel: fn(), + createdAt: "2026-03-29T20:17:55Z", + lastUpdatedAt: "2026-03-29T20:17:55Z", }, { 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(), + createdAt: "2026-03-29T20:16:47Z", + lastUpdatedAt: "2026-03-29T20:16:49Z", }, { 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(), + createdAt: "2026-03-29T20:17:27Z", + lastUpdatedAt: "2026-03-29T20:17:57Z", + statusMessage: "Timeout waiting for tool response after 30s", }, ]} onRefresh={fn()} onClearCompleted={fn()} + onCancel={fn()} /> ), }, From b487279d369dedc2e38c7436e63d4874e4c9b4a9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 10:53:44 -0400 Subject: [PATCH 2/7] Add progress bar back to TaskCard via optional progress prop Progress notifications (notifications/progress) are sent separately from the Task object per the MCP spec. Add an optional progress prop typed as Pick so the parent can pass progress data when available. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../groups/TaskCard/TaskCard.stories.tsx | 5 ++++- .../components/groups/TaskCard/TaskCard.tsx | 21 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx index bc077989..84ee9e5c 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx @@ -61,7 +61,10 @@ const cancelledTask: Task = { }; export const Working: Story = { - args: { task: workingTask }, + args: { + task: workingTask, + progress: { progress: 650, total: 1000, message: "Processing records..." }, + }, }; export const InputRequired: Story = { diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index d183dce7..f732de23 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -8,12 +8,22 @@ import { Stack, Text, } from "@mantine/core"; -import type { Task } from "@modelcontextprotocol/sdk/types.js"; +import type { + ProgressNotification, + Task, +} from "@modelcontextprotocol/sdk/types.js"; import { ContentViewer } from "../../elements/ContentViewer/ContentViewer"; +import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay"; import { TaskStatusBadge } from "../../elements/TaskStatusBadge/TaskStatusBadge"; +export type TaskProgress = Pick< + ProgressNotification["params"], + "progress" | "total" | "message" +>; + export interface TaskCardProps { task: Task; + progress?: TaskProgress; isListExpanded: boolean; onCancel: () => void; } @@ -91,7 +101,12 @@ function formatTtl(ttl: number): string { return `${ttl}ms`; } -export function TaskCard({ task, isListExpanded, onCancel }: TaskCardProps) { +export function TaskCard({ + task, + progress, + isListExpanded, + onCancel, +}: TaskCardProps) { const { taskId, status, ttl, createdAt, lastUpdatedAt, statusMessage } = task; const [isExpanded, setIsExpanded] = useState(isListExpanded); const isActive = status === "working" || status === "input_required"; @@ -146,6 +161,8 @@ export function TaskCard({ task, isListExpanded, onCancel }: TaskCardProps) { {statusMessage} )} + {progress && isActive && } + {isExpanded && ( From f9f239c8c7d6e0fbd1896317a421933a03efff8b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 11:07:43 -0400 Subject: [PATCH 3/7] Thread progressByTaskId through TaskListPanel and TasksScreen Progress notifications arrive separately from the Task object, so the parent provides a map of progress data keyed by taskId. TaskListPanel and TasksScreen accept an optional progressByTaskId prop and pass the matching entry to each TaskCard. Updated Mixed story and ConnectedView TasksActive story to demonstrate the progress bar on the active "working" task. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/groups/TaskListPanel/TaskListPanel.tsx | 5 +++++ .../components/screens/TasksScreen/TasksScreen.stories.tsx | 7 +++++++ .../web/src/components/screens/TasksScreen/TasksScreen.tsx | 4 ++++ .../views/ConnectedView/ConnectedView.stories.tsx | 7 +++++++ 4 files changed, 23 insertions(+) diff --git a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx index 1096081c..c512f7fa 100644 --- a/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx +++ b/clients/web/src/components/groups/TaskListPanel/TaskListPanel.tsx @@ -10,10 +10,12 @@ import { } from "@mantine/core"; import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskCard } from "../TaskCard/TaskCard"; +import type { TaskProgress } from "../TaskCard/TaskCard"; import { ListToggle } from "../../elements/ListToggle/ListToggle"; export interface TaskListPanelProps { tasks: Task[]; + progressByTaskId?: Record; searchText: string; statusFilter?: TaskStatus; onCancel: (taskId: string) => void; @@ -67,6 +69,7 @@ function matchesFilters( export function TaskListPanel({ tasks, + progressByTaskId, searchText, statusFilter, onCancel, @@ -115,6 +118,7 @@ export function TaskListPanel({ onCancel(task.taskId)} /> @@ -136,6 +140,7 @@ export function TaskListPanel({ onCancel(task.taskId)} /> diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index 1f6883a0..cea894b0 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -53,6 +53,13 @@ const sampleTasks: Task[] = [ export const Mixed: Story = { args: { tasks: sampleTasks, + progressByTaskId: { + d0b22eba71fa36229ce5c4dfadeaa7de: { + progress: 650, + total: 1000, + message: "Processing records...", + }, + }, }, }; diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx index b6a0730c..b0fe9a1d 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.tsx @@ -3,9 +3,11 @@ import { Card, Flex, Stack } from "@mantine/core"; import type { Task, TaskStatus } from "@modelcontextprotocol/sdk/types.js"; import { TaskControls } from "../../groups/TaskControls/TaskControls"; import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel"; +import type { TaskProgress } from "../../groups/TaskCard/TaskCard"; export interface TasksScreenProps { tasks: Task[]; + progressByTaskId?: Record; onRefresh: () => void; onClearCompleted: () => void; onCancel: (taskId: string) => void; @@ -30,6 +32,7 @@ const SidebarCard = Card.withProps({ export function TasksScreen({ tasks, + progressByTaskId, onRefresh, onClearCompleted, onCancel, @@ -52,6 +55,7 @@ export function TasksScreen({ Date: Mon, 20 Apr 2026 13:07:18 -0400 Subject: [PATCH 4/7] Show progress bar OR statusMessage, not both When progress notification data is available for an active task, show the ProgressDisplay (which includes its own message). Fall back to the Task's statusMessage only when no progress data is present. Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/web/src/components/groups/TaskCard/TaskCard.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index f732de23..2868905b 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -157,12 +157,13 @@ export function TaskCard({ - {statusMessage && isActive && ( - {statusMessage} + {progress && isActive ? ( + + ) : ( + statusMessage && + isActive && {statusMessage} )} - {progress && isActive && } - {isExpanded && ( From 6261a7e270fa137caa78886e4bc0063785a95194 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 13:37:30 -0400 Subject: [PATCH 5/7] Use descriptive progress message in task stories Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/components/groups/TaskCard/TaskCard.stories.tsx | 6 +++++- .../components/screens/TasksScreen/TasksScreen.stories.tsx | 2 +- .../views/ConnectedView/ConnectedView.stories.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx index 84ee9e5c..b11bc1b7 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.stories.tsx @@ -63,7 +63,11 @@ const cancelledTask: Task = { export const Working: Story = { args: { task: workingTask, - progress: { progress: 650, total: 1000, message: "Processing records..." }, + progress: { + progress: 650, + total: 1000, + message: "Processing records 650 of 1000...", + }, }, }; diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index cea894b0..745e4e15 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -57,7 +57,7 @@ export const Mixed: Story = { d0b22eba71fa36229ce5c4dfadeaa7de: { progress: 650, total: 1000, - message: "Processing records...", + message: "Processing records 650 of 1000...", }, }, }, diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index 24ec4775..e2832878 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -581,7 +581,7 @@ export const TasksActive: Story = { d0b22eba71fa36229ce5c4dfadeaa7de: { progress: 650, total: 1000, - message: "Processing records...", + message: "Processing records 650 of 1000...", }, }} onRefresh={fn()} From 6af51e59634183115d68bb7ecda61df3b5e3fd3c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 13:40:07 -0400 Subject: [PATCH 6/7] Add progress bar to ActiveOnly story in TasksScreen Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/screens/TasksScreen/TasksScreen.stories.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx index 745e4e15..409573d2 100644 --- a/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx +++ b/clients/web/src/components/screens/TasksScreen/TasksScreen.stories.tsx @@ -68,6 +68,13 @@ export const ActiveOnly: Story = { tasks: sampleTasks.filter( (t) => t.status === "working" || t.status === "input_required", ), + progressByTaskId: { + d0b22eba71fa36229ce5c4dfadeaa7de: { + progress: 650, + total: 1000, + message: "Processing records 650 of 1000...", + }, + }, }, }; From 3d6280eb0ec93cb870493c34b763c21211665444 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 20 Apr 2026 15:49:14 -0400 Subject: [PATCH 7/7] Address PR #1217 code review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Collapse animation: remove outer {isExpanded && ...} wrapper so Mantine's Collapse component drives the mount/unmount animation - Show statusMessage for all tasks, not just active ones, so failed/ cancelled tasks display their error reason without expanding - Remove redundant "Status" detail row — the TaskStatusBadge in the header already communicates status - Fix ttl guard: change !== null to != null to correctly handle the SDK's optional (undefined) field Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/groups/TaskCard/TaskCard.tsx | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/clients/web/src/components/groups/TaskCard/TaskCard.tsx b/clients/web/src/components/groups/TaskCard/TaskCard.tsx index 2868905b..2a08a183 100644 --- a/clients/web/src/components/groups/TaskCard/TaskCard.tsx +++ b/clients/web/src/components/groups/TaskCard/TaskCard.tsx @@ -133,10 +133,6 @@ export function TaskCard({ - - Status - {status} - Last Updated {lastUpdatedAt} @@ -145,7 +141,7 @@ export function TaskCard({ Created At {createdAt} - {ttl !== null && ( + {ttl != null && ( TTL {formatTtl(ttl)} @@ -160,29 +156,21 @@ export function TaskCard({ {progress && isActive ? ( ) : ( - statusMessage && - isActive && {statusMessage} + statusMessage && ( + {statusMessage} + ) )} - {isExpanded && ( - - - {statusMessage && !isActive && ( - - Status Message - {statusMessage} - - )} - - - Full Task Object - - - - )} + + + + Full Task Object + + + );