Redesign Tasks screen: expandable TaskCard, sidebar controls, and list panel
- Redesign TaskCard to match v1 Task Details panel layout: header with task ID + status badge + cancel button, always-visible summary row (Status, Last Updated, Created At, TTL) with Expand/Collapse button, expandable section with status message, progress bar, error, and Full Task Object JSON viewer - Add TaskControls group: search input + status filter dropdown + Refresh/Clear History buttons (mirrors HistoryControls pattern) - Add TaskListPanel group: scrollable panel with ListToggle for expand/collapse all, splits tasks into Active/Completed sections with counts, filters by search text and status - Refactor TasksScreen to sidebar + panel layout matching HistoryScreen: manages search/filter state internally, flat tasks array - Simplify TaskCard callbacks to just onCancel - Extract named .withProps() subcomponents (HeaderLeftGroup, HeaderRightGroup, SectionTitle, SummaryRow, DetailRow) for AGENTS.md compliance - Update all stories (TaskCard, TaskControls, TaskListPanel, TasksScreen, ConnectedView) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a0e763a315
commit
c494d2feb4
9 changed files with 639 additions and 276 deletions
|
|
@ -6,10 +6,8 @@ const meta: Meta<typeof TaskCard> = {
|
||||||
title: "Groups/TaskCard",
|
title: "Groups/TaskCard",
|
||||||
component: TaskCard,
|
component: TaskCard,
|
||||||
args: {
|
args: {
|
||||||
onViewDetails: fn(),
|
|
||||||
onViewResult: fn(),
|
|
||||||
onCancel: fn(),
|
onCancel: fn(),
|
||||||
onDismiss: fn(),
|
isListExpanded: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -18,77 +16,82 @@ type Story = StoryObj<typeof TaskCard>;
|
||||||
|
|
||||||
export const Running: Story = {
|
export const Running: Story = {
|
||||||
args: {
|
args: {
|
||||||
taskId: "task-abc-123",
|
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||||
status: "running",
|
status: "running",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "generate_report",
|
target: "generate_report",
|
||||||
progress: 80,
|
progress: 80,
|
||||||
progressDescription: "Generating report section 4 of 5...",
|
progressDescription: "Synthesizing findings...",
|
||||||
startedAt: "2026-03-17T10:30:00Z",
|
startedAt: "3/29/2026, 8:18:20 PM",
|
||||||
elapsed: "2m 15s",
|
lastUpdated: "3/29/2026, 8:18:22 PM",
|
||||||
},
|
ttl: 300000,
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Waiting: Story = {
|
export const Waiting: Story = {
|
||||||
args: {
|
args: {
|
||||||
taskId: "task-ghi-789",
|
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
|
||||||
status: "waiting",
|
status: "waiting",
|
||||||
method: "resources/read",
|
method: "resources/read",
|
||||||
target: "file:///data/report.csv",
|
target: "file:///data/report.csv",
|
||||||
progress: 0,
|
startedAt: "3/29/2026, 8:17:55 PM",
|
||||||
startedAt: "2026-03-17T10:35:00Z",
|
lastUpdated: "3/29/2026, 8:17:55 PM",
|
||||||
|
ttl: 300000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Completed: Story = {
|
export const Completed: Story = {
|
||||||
args: {
|
args: {
|
||||||
taskId: "task-jkl-012",
|
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "generate_summary",
|
target: "generate_summary",
|
||||||
progress: 100,
|
startedAt: "3/29/2026, 8:16:47 PM",
|
||||||
startedAt: "2026-03-17T10:20:00Z",
|
completedAt: "3/29/2026, 8:16:51 PM",
|
||||||
completedAt: "2026-03-17T10:25:00Z",
|
lastUpdated: "3/29/2026, 8:16:51 PM",
|
||||||
elapsed: "5m 00s",
|
ttl: 300000,
|
||||||
|
progressDescription: "Generating report...",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Failed: Story = {
|
export const Failed: Story = {
|
||||||
args: {
|
args: {
|
||||||
taskId: "task-mno-345",
|
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "fetch_remote_data",
|
target: "fetch_remote_data",
|
||||||
progress: 45,
|
startedAt: "3/29/2026, 8:17:27 PM",
|
||||||
startedAt: "2026-03-17T10:28:00Z",
|
completedAt: "3/29/2026, 8:17:28 PM",
|
||||||
completedAt: "2026-03-17T10:29:30Z",
|
lastUpdated: "3/29/2026, 8:17:28 PM",
|
||||||
elapsed: "1m 30s",
|
ttl: 300000,
|
||||||
error: "Connection refused: upstream server not responding after 3 retries",
|
error: "Connection refused: upstream server not responding after 3 retries",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Cancelled: Story = {
|
export const Cancelled: Story = {
|
||||||
args: {
|
args: {
|
||||||
taskId: "task-pqr-678",
|
taskId: "c416bc183cb04468d3f81696f1b868f6",
|
||||||
status: "cancelled",
|
status: "cancelled",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "long_running_analysis",
|
target: "long_running_analysis",
|
||||||
progress: 30,
|
startedAt: "3/29/2026, 8:18:15 PM",
|
||||||
startedAt: "2026-03-17T10:15:00Z",
|
completedAt: "3/29/2026, 8:18:18 PM",
|
||||||
completedAt: "2026-03-17T10:18:00Z",
|
lastUpdated: "3/29/2026, 8:18:18 PM",
|
||||||
elapsed: "3m 00s",
|
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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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";
|
import { ProgressDisplay } from "../../elements/ProgressDisplay/ProgressDisplay";
|
||||||
|
|
||||||
export type TaskStatus =
|
export type TaskStatus =
|
||||||
|
|
@ -17,12 +28,12 @@ export interface TaskCardProps {
|
||||||
progressDescription?: string;
|
progressDescription?: string;
|
||||||
startedAt?: string;
|
startedAt?: string;
|
||||||
completedAt?: string;
|
completedAt?: string;
|
||||||
|
lastUpdated?: string;
|
||||||
elapsed?: string;
|
elapsed?: string;
|
||||||
|
ttl?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
onViewDetails: () => void;
|
isListExpanded: boolean;
|
||||||
onViewResult: () => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDismiss: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: Record<TaskStatus, string> = {
|
const STATUS_COLORS: Record<TaskStatus, string> = {
|
||||||
|
|
@ -38,27 +49,37 @@ const TaskContainer = Card.withProps({
|
||||||
padding: "md",
|
padding: "md",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HeaderRow = Group.withProps({
|
||||||
|
justify: "space-between",
|
||||||
|
wrap: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
const TaskIdText = Text.withProps({
|
const TaskIdText = Text.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
ff: "monospace",
|
ff: "monospace",
|
||||||
|
c: "dimmed",
|
||||||
});
|
});
|
||||||
|
|
||||||
const DetailText = Text.withProps({
|
const DetailLabel = Text.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
c: "dimmed",
|
c: "dimmed",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ValueSpan = Text.withProps({
|
const DetailValue = Text.withProps({
|
||||||
component: "span",
|
size: "sm",
|
||||||
fw: 600,
|
fw: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ProgressNote = Text.withProps({
|
const StatusMessageText = Text.withProps({
|
||||||
size: "sm",
|
size: "sm",
|
||||||
c: "dimmed",
|
|
||||||
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",
|
||||||
|
|
@ -70,89 +91,152 @@ const CancelButton = Button.withProps({
|
||||||
size: "xs",
|
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 {
|
function formatTaskId(taskId: string): string {
|
||||||
return `Task: ${taskId}`;
|
return `ID: ${taskId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeline(
|
function formatTtl(ttl: number): string {
|
||||||
startedAt: string,
|
return `${ttl}ms`;
|
||||||
completedAt?: string,
|
|
||||||
elapsed?: string,
|
|
||||||
): string {
|
|
||||||
let result = `Started: ${startedAt}`;
|
|
||||||
if (completedAt) result += ` | Completed: ${completedAt}`;
|
|
||||||
if (elapsed) result += ` | Elapsed: ${elapsed}`;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TaskCard({
|
function buildTaskObject(props: TaskCardProps): string {
|
||||||
taskId,
|
const obj: Record<string, unknown> = {
|
||||||
status,
|
taskId: props.taskId,
|
||||||
method,
|
status: props.status,
|
||||||
target,
|
method: props.method,
|
||||||
progress,
|
};
|
||||||
progressDescription,
|
if (props.target) obj.target = props.target;
|
||||||
startedAt,
|
if (props.ttl !== undefined) obj.ttl = props.ttl;
|
||||||
completedAt,
|
if (props.startedAt) obj.createdAt = props.startedAt;
|
||||||
elapsed,
|
if (props.lastUpdated) obj.lastUpdatedAt = props.lastUpdated;
|
||||||
error,
|
if (props.completedAt) obj.completedAt = props.completedAt;
|
||||||
onViewDetails,
|
if (props.progress !== undefined) obj.progress = props.progress;
|
||||||
onViewResult,
|
if (props.progressDescription) obj.statusMessage = props.progressDescription;
|
||||||
onCancel,
|
if (props.error) obj.error = props.error;
|
||||||
onDismiss,
|
return JSON.stringify(obj);
|
||||||
}: TaskCardProps) {
|
}
|
||||||
|
|
||||||
|
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";
|
const isActive = status === "waiting" || status === "running";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsExpanded(isListExpanded);
|
||||||
|
}, [isListExpanded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaskContainer>
|
<TaskContainer>
|
||||||
<Stack gap="xs">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<HeaderRow>
|
||||||
<Group>
|
<HeaderLeftGroup>
|
||||||
|
<SectionTitle>Task Details</SectionTitle>
|
||||||
<TaskIdText>{formatTaskId(taskId)}</TaskIdText>
|
<TaskIdText>{formatTaskId(taskId)}</TaskIdText>
|
||||||
|
</HeaderLeftGroup>
|
||||||
|
<HeaderRightGroup>
|
||||||
|
{isActive && (
|
||||||
|
<CancelButton onClick={onCancel}>Cancel Task</CancelButton>
|
||||||
|
)}
|
||||||
<Badge color={STATUS_COLORS[status]}>{status}</Badge>
|
<Badge color={STATUS_COLORS[status]}>{status}</Badge>
|
||||||
</Group>
|
</HeaderRightGroup>
|
||||||
{progress !== undefined && <ProgressDisplay progress={progress} />}
|
</HeaderRow>
|
||||||
</Group>
|
|
||||||
|
|
||||||
<DetailText>
|
<SummaryRow>
|
||||||
Method: <ValueSpan>{method}</ValueSpan>
|
<DetailRow>
|
||||||
</DetailText>
|
<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 && (
|
{isExpanded && (
|
||||||
<DetailText>
|
<Collapse in={isExpanded}>
|
||||||
Tool/Resource: <ValueSpan>{target}</ValueSpan>
|
<Stack gap="sm">
|
||||||
</DetailText>
|
{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>
|
</Stack>
|
||||||
</TaskContainer>
|
</TaskContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import { fn } from "storybook/test";
|
||||||
|
import { TaskListPanel } from "./TaskListPanel";
|
||||||
|
import type { TaskCardProps } from "../TaskCard/TaskCard";
|
||||||
|
|
||||||
|
const sampleTasks: TaskCardProps[] = [
|
||||||
|
{
|
||||||
|
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||||
|
status: "running",
|
||||||
|
method: "tools/call",
|
||||||
|
target: "generate_report",
|
||||||
|
progress: 80,
|
||||||
|
progressDescription: "Synthesizing findings...",
|
||||||
|
startedAt: "3/29/2026, 8:18:20 PM",
|
||||||
|
lastUpdated: "3/29/2026, 8:18:22 PM",
|
||||||
|
ttl: 300000,
|
||||||
|
isListExpanded: true,
|
||||||
|
onCancel: fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
|
||||||
|
status: "waiting",
|
||||||
|
method: "resources/read",
|
||||||
|
target: "file:///data/report.csv",
|
||||||
|
startedAt: "3/29/2026, 8:17:55 PM",
|
||||||
|
lastUpdated: "3/29/2026, 8:17:55 PM",
|
||||||
|
ttl: 300000,
|
||||||
|
isListExpanded: true,
|
||||||
|
onCancel: fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||||
|
status: "completed",
|
||||||
|
method: "tools/call",
|
||||||
|
target: "generate_summary",
|
||||||
|
startedAt: "3/29/2026, 8:16:47 PM",
|
||||||
|
completedAt: "3/29/2026, 8:16:51 PM",
|
||||||
|
lastUpdated: "3/29/2026, 8:16:51 PM",
|
||||||
|
ttl: 300000,
|
||||||
|
progressDescription: "Generating report...",
|
||||||
|
isListExpanded: true,
|
||||||
|
onCancel: fn(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||||
|
status: "failed",
|
||||||
|
method: "tools/call",
|
||||||
|
target: "fetch_remote_data",
|
||||||
|
startedAt: "3/29/2026, 8:17:27 PM",
|
||||||
|
lastUpdated: "3/29/2026, 8:17:28 PM",
|
||||||
|
ttl: 300000,
|
||||||
|
error: "Connection refused: upstream server not responding",
|
||||||
|
isListExpanded: true,
|
||||||
|
onCancel: fn(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const meta: Meta<typeof TaskListPanel> = {
|
||||||
|
title: "Groups/TaskListPanel",
|
||||||
|
component: TaskListPanel,
|
||||||
|
args: {
|
||||||
|
tasks: sampleTasks,
|
||||||
|
searchText: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TaskListPanel>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const FilteredByStatus: Story = {
|
||||||
|
args: {
|
||||||
|
statusFilter: "running",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithSearch: Story = {
|
||||||
|
args: {
|
||||||
|
searchText: "generate",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
args: {
|
||||||
|
tasks: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Group, Paper, ScrollArea, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { TaskCard } from "../TaskCard/TaskCard";
|
||||||
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
|
import type { TaskCardProps } from "../TaskCard/TaskCard";
|
||||||
|
|
||||||
|
export interface TaskListPanelProps {
|
||||||
|
tasks: TaskCardProps[];
|
||||||
|
searchText: string;
|
||||||
|
statusFilter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelContainer = Paper.withProps({
|
||||||
|
withBorder: true,
|
||||||
|
p: "lg",
|
||||||
|
flex: 1,
|
||||||
|
variant: "panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
const EmptyState = Text.withProps({
|
||||||
|
c: "dimmed",
|
||||||
|
ta: "center",
|
||||||
|
py: "xl",
|
||||||
|
});
|
||||||
|
|
||||||
|
function taskKey(task: TaskCardProps): string {
|
||||||
|
return task.taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatActiveTitle(count: number): string {
|
||||||
|
return `Active (${count})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCompletedTitle(count: number): string {
|
||||||
|
return `Completed (${count})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveStatus(status: string): boolean {
|
||||||
|
return status === "waiting" || status === "running";
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilters(
|
||||||
|
task: TaskCardProps,
|
||||||
|
searchText: string,
|
||||||
|
statusFilter?: string,
|
||||||
|
): boolean {
|
||||||
|
if (statusFilter && task.status !== statusFilter) return false;
|
||||||
|
if (searchText) {
|
||||||
|
const term = searchText.toLowerCase();
|
||||||
|
const searchable =
|
||||||
|
`${task.taskId} ${task.method} ${task.target ?? ""} ${task.status}`.toLowerCase();
|
||||||
|
if (!searchable.includes(term)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskListPanel({
|
||||||
|
tasks,
|
||||||
|
searchText,
|
||||||
|
statusFilter,
|
||||||
|
}: TaskListPanelProps) {
|
||||||
|
const [compact, setCompact] = useState(false);
|
||||||
|
|
||||||
|
const filteredTasks = useMemo(
|
||||||
|
() => tasks.filter((t) => matchesFilters(t, searchText, statusFilter)),
|
||||||
|
[tasks, searchText, statusFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeTasks = useMemo(
|
||||||
|
() => filteredTasks.filter((t) => isActiveStatus(t.status)),
|
||||||
|
[filteredTasks],
|
||||||
|
);
|
||||||
|
|
||||||
|
const completedTasks = useMemo(
|
||||||
|
() => filteredTasks.filter((t) => !isActiveStatus(t.status)),
|
||||||
|
[filteredTasks],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasResults = filteredTasks.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelContainer>
|
||||||
|
<Group justify="space-between" mb="sm">
|
||||||
|
<Title order={4}>Tasks</Title>
|
||||||
|
{hasResults && (
|
||||||
|
<ListToggle
|
||||||
|
compact={compact}
|
||||||
|
onToggle={() => setCompact((c) => !c)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{!hasResults ? (
|
||||||
|
<EmptyState>No tasks</EmptyState>
|
||||||
|
) : (
|
||||||
|
<ScrollArea.Autosize mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)">
|
||||||
|
<Stack gap="md">
|
||||||
|
{activeTasks.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title order={5}>{formatActiveTitle(activeTasks.length)}</Title>
|
||||||
|
{activeTasks.map((task) => (
|
||||||
|
<TaskCard
|
||||||
|
key={taskKey(task)}
|
||||||
|
{...task}
|
||||||
|
isListExpanded={!compact}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{completedTasks.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title order={5}>
|
||||||
|
{formatCompletedTitle(completedTasks.length)}
|
||||||
|
</Title>
|
||||||
|
{completedTasks.map((task) => (
|
||||||
|
<TaskCard
|
||||||
|
key={taskKey(task)}
|
||||||
|
{...task}
|
||||||
|
isListExpanded={!compact}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
)}
|
||||||
|
</PanelContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -16,85 +16,82 @@ const meta: Meta<typeof TasksScreen> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof TasksScreen>;
|
type Story = StoryObj<typeof TasksScreen>;
|
||||||
|
|
||||||
const activeTask1: TaskCardProps = {
|
const sampleTasks: TaskCardProps[] = [
|
||||||
taskId: "task-001",
|
{
|
||||||
status: "running",
|
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||||
method: "tools/call",
|
status: "running",
|
||||||
target: "send_message",
|
method: "tools/call",
|
||||||
progress: 45,
|
target: "batch_process",
|
||||||
progressDescription: "Sending message to remote server...",
|
progress: 65,
|
||||||
startedAt: "2026-03-17T10:00:00Z",
|
progressDescription: "Processing records 650 of 1000...",
|
||||||
onViewDetails: fn(),
|
startedAt: "3/29/2026, 8:18:20 PM",
|
||||||
onViewResult: fn(),
|
lastUpdated: "3/29/2026, 8:18:22 PM",
|
||||||
onCancel: fn(),
|
ttl: 300000,
|
||||||
onDismiss: fn(),
|
isListExpanded: true,
|
||||||
};
|
onCancel: fn(),
|
||||||
|
},
|
||||||
const activeTask2: TaskCardProps = {
|
{
|
||||||
taskId: "task-002",
|
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
|
||||||
status: "waiting",
|
status: "waiting",
|
||||||
method: "resources/read",
|
method: "resources/read",
|
||||||
target: "config.json",
|
target: "file:///data/large-dataset.csv",
|
||||||
startedAt: "2026-03-17T10:01:00Z",
|
startedAt: "3/29/2026, 8:17:55 PM",
|
||||||
onViewDetails: fn(),
|
lastUpdated: "3/29/2026, 8:17:55 PM",
|
||||||
onViewResult: fn(),
|
ttl: 300000,
|
||||||
onCancel: fn(),
|
isListExpanded: true,
|
||||||
onDismiss: fn(),
|
onCancel: fn(),
|
||||||
};
|
},
|
||||||
|
{
|
||||||
const completedTask1: TaskCardProps = {
|
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||||
taskId: "task-003",
|
status: "completed",
|
||||||
status: "completed",
|
method: "tools/call",
|
||||||
method: "tools/call",
|
target: "send_message",
|
||||||
target: "list_users",
|
startedAt: "3/29/2026, 8:16:47 PM",
|
||||||
startedAt: "2026-03-17T09:50:00Z",
|
completedAt: "3/29/2026, 8:16:49 PM",
|
||||||
completedAt: "2026-03-17T09:50:02Z",
|
lastUpdated: "3/29/2026, 8:16:49 PM",
|
||||||
elapsed: "2.1s",
|
ttl: 300000,
|
||||||
onViewDetails: fn(),
|
isListExpanded: true,
|
||||||
onViewResult: fn(),
|
onCancel: fn(),
|
||||||
onCancel: fn(),
|
},
|
||||||
onDismiss: fn(),
|
{
|
||||||
};
|
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||||
|
status: "failed",
|
||||||
const completedTask2: TaskCardProps = {
|
method: "tools/call",
|
||||||
taskId: "task-004",
|
target: "delete_records",
|
||||||
status: "failed",
|
startedAt: "3/29/2026, 8:17:27 PM",
|
||||||
method: "tools/call",
|
completedAt: "3/29/2026, 8:17:57 PM",
|
||||||
target: "delete_records",
|
lastUpdated: "3/29/2026, 8:17:57 PM",
|
||||||
startedAt: "2026-03-17T09:45:00Z",
|
ttl: 300000,
|
||||||
completedAt: "2026-03-17T09:45:01Z",
|
error: "Timeout waiting for tool response after 30s",
|
||||||
elapsed: "0.8s",
|
isListExpanded: true,
|
||||||
error: "Permission denied: insufficient privileges",
|
onCancel: fn(),
|
||||||
onViewDetails: fn(),
|
},
|
||||||
onViewResult: fn(),
|
];
|
||||||
onCancel: fn(),
|
|
||||||
onDismiss: fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Mixed: Story = {
|
export const Mixed: Story = {
|
||||||
args: {
|
args: {
|
||||||
activeTasks: [activeTask1, activeTask2],
|
tasks: sampleTasks,
|
||||||
completedTasks: [completedTask1, completedTask2],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveOnly: Story = {
|
export const ActiveOnly: Story = {
|
||||||
args: {
|
args: {
|
||||||
activeTasks: [activeTask1, activeTask2],
|
tasks: sampleTasks.filter(
|
||||||
completedTasks: [],
|
(t) => t.status === "running" || t.status === "waiting",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompletedOnly: Story = {
|
export const CompletedOnly: Story = {
|
||||||
args: {
|
args: {
|
||||||
activeTasks: [],
|
tasks: sampleTasks.filter(
|
||||||
completedTasks: [completedTask1, completedTask2],
|
(t) => t.status !== "running" && t.status !== "waiting",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
activeTasks: [],
|
tasks: [],
|
||||||
completedTasks: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,61 @@
|
||||||
import {
|
import { useState } from "react";
|
||||||
Button,
|
import { Card, Flex, Stack } from "@mantine/core";
|
||||||
Container,
|
import { TaskControls } from "../../groups/TaskControls/TaskControls";
|
||||||
Group,
|
import { TaskListPanel } from "../../groups/TaskListPanel/TaskListPanel";
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { TaskCard } from "../../groups/TaskCard/TaskCard";
|
|
||||||
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
|
import type { TaskCardProps } from "../../groups/TaskCard/TaskCard";
|
||||||
|
|
||||||
export interface TasksScreenProps {
|
export interface TasksScreenProps {
|
||||||
activeTasks: TaskCardProps[];
|
tasks: TaskCardProps[];
|
||||||
completedTasks: TaskCardProps[];
|
|
||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
onClearHistory: () => void;
|
onClearHistory: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PageContainer = Container.withProps({
|
const ScreenLayout = Flex.withProps({
|
||||||
size: "xl",
|
variant: "screen",
|
||||||
py: "xl",
|
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,
|
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({
|
export function TasksScreen({
|
||||||
activeTasks,
|
tasks,
|
||||||
completedTasks,
|
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onClearHistory,
|
onClearHistory,
|
||||||
}: TasksScreenProps) {
|
}: TasksScreenProps) {
|
||||||
return (
|
const [searchText, setSearchText] = useState("");
|
||||||
<PageContainer>
|
const [statusFilter, setStatusFilter] = useState<string | undefined>();
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
return (
|
||||||
<Title order={4}>
|
<ScreenLayout>
|
||||||
{formatSectionTitle("Completed Tasks", completedTasks.length)}
|
<Sidebar>
|
||||||
</Title>
|
<SidebarCard>
|
||||||
{completedTasks.length > 0 && (
|
<TaskControls
|
||||||
<CompactButton onClick={onClearHistory}>
|
searchText={searchText}
|
||||||
Clear History
|
statusFilter={statusFilter}
|
||||||
</CompactButton>
|
onSearchChange={setSearchText}
|
||||||
)}
|
onStatusFilterChange={(value) =>
|
||||||
</Group>
|
setStatusFilter(value || undefined)
|
||||||
{completedTasks.length === 0 ? (
|
}
|
||||||
<Text c="dimmed">No completed tasks</Text>
|
onRefresh={onRefresh}
|
||||||
) : (
|
onClearHistory={onClearHistory}
|
||||||
<Stack gap="md">
|
/>
|
||||||
{completedTasks.map((task) => (
|
</SidebarCard>
|
||||||
<TaskCard key={task.taskId} {...task} />
|
</Sidebar>
|
||||||
))}
|
<TaskListPanel
|
||||||
</Stack>
|
tasks={tasks}
|
||||||
)}
|
searchText={searchText}
|
||||||
</Stack>
|
statusFilter={statusFilter}
|
||||||
</ContentPanel>
|
/>
|
||||||
</PageContainer>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -390,59 +390,55 @@ export const TasksActive: Story = {
|
||||||
activeTab: "Tasks",
|
activeTab: "Tasks",
|
||||||
children: (
|
children: (
|
||||||
<TasksScreen
|
<TasksScreen
|
||||||
activeTasks={[
|
tasks={[
|
||||||
{
|
{
|
||||||
taskId: "task-001",
|
taskId: "d0b22eba71fa36229ce5c4dfadeaa7de",
|
||||||
status: "running",
|
status: "running",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "batch_process",
|
target: "batch_process",
|
||||||
progress: 65,
|
progress: 65,
|
||||||
progressDescription: "Processing records 650 of 1000...",
|
progressDescription: "Processing records 650 of 1000...",
|
||||||
startedAt: "2026-03-17T10:05:00Z",
|
startedAt: "3/29/2026, 8:18:20 PM",
|
||||||
onViewDetails: fn(),
|
lastUpdated: "3/29/2026, 8:18:22 PM",
|
||||||
onViewResult: fn(),
|
ttl: 300000,
|
||||||
|
isListExpanded: true,
|
||||||
onCancel: fn(),
|
onCancel: fn(),
|
||||||
onDismiss: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
taskId: "task-002",
|
taskId: "4100b5e0b0ed9cd0023330342d1bf647",
|
||||||
status: "waiting",
|
status: "waiting",
|
||||||
method: "resources/read",
|
method: "resources/read",
|
||||||
target: "file:///data/large-dataset.csv",
|
target: "file:///data/large-dataset.csv",
|
||||||
startedAt: "2026-03-17T10:06:12Z",
|
startedAt: "3/29/2026, 8:17:55 PM",
|
||||||
onViewDetails: fn(),
|
lastUpdated: "3/29/2026, 8:17:55 PM",
|
||||||
onViewResult: fn(),
|
ttl: 300000,
|
||||||
|
isListExpanded: true,
|
||||||
onCancel: fn(),
|
onCancel: fn(),
|
||||||
onDismiss: fn(),
|
|
||||||
},
|
},
|
||||||
]}
|
|
||||||
completedTasks={[
|
|
||||||
{
|
{
|
||||||
taskId: "task-000",
|
taskId: "d487b49aa39023d907b5a2a5b506cb3",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "send_message",
|
target: "send_message",
|
||||||
startedAt: "2026-03-17T09:58:00Z",
|
startedAt: "3/29/2026, 8:16:47 PM",
|
||||||
completedAt: "2026-03-17T09:58:02Z",
|
completedAt: "3/29/2026, 8:16:49 PM",
|
||||||
elapsed: "2s",
|
lastUpdated: "3/29/2026, 8:16:49 PM",
|
||||||
onViewDetails: fn(),
|
ttl: 300000,
|
||||||
onViewResult: fn(),
|
isListExpanded: true,
|
||||||
onCancel: fn(),
|
onCancel: fn(),
|
||||||
onDismiss: fn(),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
taskId: "task-099",
|
taskId: "e6ebffd9cca84ddd1646d3c579a4d453",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
method: "tools/call",
|
method: "tools/call",
|
||||||
target: "delete_records",
|
target: "delete_records",
|
||||||
startedAt: "2026-03-17T09:50:00Z",
|
startedAt: "3/29/2026, 8:17:27 PM",
|
||||||
completedAt: "2026-03-17T09:50:30Z",
|
completedAt: "3/29/2026, 8:17:57 PM",
|
||||||
elapsed: "30s",
|
lastUpdated: "3/29/2026, 8:17:57 PM",
|
||||||
|
ttl: 300000,
|
||||||
error: "Timeout waiting for tool response after 30s",
|
error: "Timeout waiting for tool response after 30s",
|
||||||
onViewDetails: fn(),
|
isListExpanded: true,
|
||||||
onViewResult: fn(),
|
|
||||||
onCancel: fn(),
|
onCancel: fn(),
|
||||||
onDismiss: fn(),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onRefresh={fn()}
|
onRefresh={fn()}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue