Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 262 additions & 12 deletions client/src/App.tsx

Large diffs are not rendered by default.

227 changes: 227 additions & 0 deletions client/src/components/TasksTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { TabsContent } from "@/components/ui/tabs";
import { Task } from "@modelcontextprotocol/sdk/types.js";
import {
AlertCircle,
RefreshCw,
XCircle,
Clock,
CheckCircle2,
AlertTriangle,
PlayCircle,
} from "lucide-react";
import ListPane from "./ListPane";
import { useState } from "react";
import JsonView from "./JsonView";
import { cn } from "@/lib/utils";

const TaskStatusIcon = ({ status }: { status: Task["status"] }) => {
switch (status) {
case "working":
return <Clock className="h-4 w-4 animate-pulse text-blue-500" />;
case "input_required":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
case "completed":
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case "failed":
return <XCircle className="h-4 w-4 text-red-500" />;
case "cancelled":
return <XCircle className="h-4 w-4 text-gray-500" />;
default:
return <PlayCircle className="h-4 w-4" />;
}
};

const TasksTab = ({
tasks,
listTasks,
clearTasks,
cancelTask,
selectedTask,
setSelectedTask,
error,
nextCursor,
}: {
tasks: Task[];
listTasks: () => void;
clearTasks: () => void;
cancelTask: (taskId: string) => Promise<void>;
selectedTask: Task | null;
setSelectedTask: (task: Task | null) => void;
error: string | null;
nextCursor?: string;
}) => {
const [isCancelling, setIsCancelling] = useState<string | null>(null);

const displayedTask = selectedTask
? tasks.find((t) => t.taskId === selectedTask.taskId) || selectedTask
: null;

const handleCancel = async (taskId: string) => {
setIsCancelling(taskId);
try {
await cancelTask(taskId);
} finally {
setIsCancelling(null);
}
};

return (
<TabsContent value="tasks" className="flex-1 overflow-hidden p-0 m-0">
<div className="flex h-full overflow-hidden p-4 gap-4">
<div className="w-1/3">
<ListPane
title="Tasks"
items={tasks}
setSelectedItem={setSelectedTask}
listItems={listTasks}
clearItems={clearTasks}
buttonText={nextCursor ? "List More Tasks" : "List Tasks"}
isButtonDisabled={!nextCursor && tasks.length > 0}
renderItem={(task) => (
<div className="flex items-center gap-2 overflow-hidden w-full">
<TaskStatusIcon status={task.status} />
<div className="flex flex-col overflow-hidden">
<span className="truncate font-medium">{task.taskId}</span>
<span className="truncate text-xs text-muted-foreground">
{task.status} -{" "}
{new Date(task.lastUpdatedAt).toLocaleString()}
</span>
</div>
</div>
)}
/>
</div>

<div className="flex-1 overflow-y-auto p-4 bg-background border border-border rounded-lg">
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}

{displayedTask ? (
<div className="space-y-6">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h2 className="text-2xl font-bold tracking-tight">
Task Details
</h2>
<p className="text-muted-foreground">
ID: {displayedTask.taskId}
</p>
</div>
{displayedTask.status === "working" && (
<Button
variant="destructive"
size="sm"
onClick={() => handleCancel(displayedTask.taskId)}
disabled={isCancelling === displayedTask.taskId}
>
{isCancelling === displayedTask.taskId ? (
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
) : (
<XCircle className="mr-2 h-4 w-4" />
)}
Cancel Task
</Button>
)}
</div>

<div className="grid grid-cols-2 gap-4">
<div className="rounded-lg border p-3">
<p className="text-sm font-medium text-muted-foreground">
Status
</p>
<div className="mt-1 flex items-center gap-2">
<TaskStatusIcon status={displayedTask.status} />
<span
className={cn(
"font-semibold capitalize",
displayedTask.status === "working" && "text-blue-500",
displayedTask.status === "completed" &&
"text-green-500",
displayedTask.status === "failed" && "text-red-500",
displayedTask.status === "cancelled" && "text-gray-500",
displayedTask.status === "input_required" &&
"text-yellow-500",
)}
>
{displayedTask.status.replace("_", " ")}
</span>
</div>
</div>
<div className="rounded-lg border p-3">
<p className="text-sm font-medium text-muted-foreground">
Last Updated
</p>
<p className="mt-1 font-medium">
{new Date(displayedTask.lastUpdatedAt).toLocaleString()}
</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-sm font-medium text-muted-foreground">
Created At
</p>
<p className="mt-1 font-medium">
{new Date(displayedTask.createdAt).toLocaleString()}
</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-sm font-medium text-muted-foreground">
TTL
</p>
<p className="mt-1 font-medium">
{displayedTask.ttl === null
? "Infinite"
: `${displayedTask.ttl}s`}
</p>
</div>
</div>

{displayedTask.statusMessage && (
<div className="rounded-lg border p-3">
<p className="text-sm font-medium text-muted-foreground">
Status Message
</p>
<p className="mt-1 whitespace-pre-wrap">
{displayedTask.statusMessage}
</p>
</div>
)}

<div className="space-y-2">
<h3 className="text-lg font-semibold">Full Task Object</h3>
<div className="rounded-md border">
<JsonView data={displayedTask} />
</div>
</div>
</div>
) : (
<div className="flex h-full items-center justify-center text-muted-foreground">
<div className="text-center">
<Clock className="mx-auto mb-4 h-12 w-12 opacity-20" />
<h3 className="text-lg font-medium">No Task Selected</h3>
<p>Select a task from the list to view its details.</p>
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={listTasks}
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh Tasks
</Button>
</div>
</div>
)}
</div>
</div>
</TabsContent>
);
};

export default TasksTab;
17 changes: 17 additions & 0 deletions client/src/components/ToolResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ToolResultsProps {
selectedTool: Tool | null;
resourceContent: Record<string, string>;
onReadResource?: (uri: string) => void;
isPollingTask?: boolean;
}

const checkContentCompatibility = (
Expand Down Expand Up @@ -69,6 +70,7 @@ const ToolResults = ({
selectedTool,
resourceContent,
onReadResource,
isPollingTask,
}: ToolResultsProps) => {
if (!toolResult) return null;

Expand All @@ -89,6 +91,19 @@ const ToolResults = ({
const structuredResult = parsedResult.data;
const isError = structuredResult.isError ?? false;

// Check if this is a running task
const relatedTask = structuredResult._meta?.[
"io.modelcontextprotocol/related-task"
] as { taskId: string } | undefined;
const isTaskRunning =
isPollingTask ||
(!!relatedTask &&
structuredResult.content.some(
(c) =>
c.type === "text" &&
(c.text?.includes("Polling") || c.text?.includes("Task status")),
));

let validationResult = null;
const toolHasOutputSchema =
selectedTool && hasOutputSchema(selectedTool.name);
Expand Down Expand Up @@ -127,6 +142,8 @@ const ToolResults = ({
Tool Result:{" "}
{isError ? (
<span className="text-red-600 font-semibold">Error</span>
) : isTaskRunning ? (
<span className="text-yellow-600 font-semibold">Task Running</span>
) : (
<span className="text-green-600 font-semibold">Success</span>
)}
Expand Down
28 changes: 26 additions & 2 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const ToolsTab = ({
selectedTool,
setSelectedTool,
toolResult,
isPollingTask,
nextCursor,
error,
resourceContent,
Expand All @@ -76,16 +77,19 @@ const ToolsTab = ({
name: string,
params: Record<string, unknown>,
metadata?: Record<string, unknown>,
runAsTask?: boolean,
) => Promise<void>;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool | null) => void;
toolResult: CompatibilityCallToolResult | null;
isPollingTask?: boolean;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
resourceContent: Record<string, string>;
onReadResource?: (uri: string) => void;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
const [runAsTask, setRunAsTask] = useState(false);
const [isToolRunning, setIsToolRunning] = useState(false);
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
const [isMetadataExpanded, setIsMetadataExpanded] = useState(false);
Expand Down Expand Up @@ -125,6 +129,7 @@ const ToolsTab = ({
];
});
setParams(Object.fromEntries(params));
setRunAsTask(false);

// Reset validation errors when switching tools
setHasValidationErrors(false);
Expand Down Expand Up @@ -157,6 +162,7 @@ const ToolsTab = ({
clearItems={() => {
clearTools();
setSelectedTool(null);
setRunAsTask(false);
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
Expand Down Expand Up @@ -651,6 +657,21 @@ const ToolsTab = ({
</div>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="run-as-task"
checked={runAsTask}
onCheckedChange={(checked: boolean) =>
setRunAsTask(checked)
}
/>
<Label
htmlFor="run-as-task"
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
>
Run as task
</Label>
</div>
<Button
onClick={async () => {
// Validate JSON inputs before calling tool
Expand All @@ -676,23 +697,25 @@ const ToolsTab = ({
selectedTool.name,
params,
Object.keys(metadata).length ? metadata : undefined,
runAsTask,
);
} finally {
setIsToolRunning(false);
}
}}
disabled={
isToolRunning ||
isPollingTask ||
hasValidationErrors ||
hasReservedMetadataEntry ||
hasInvalidMetaPrefixEntry ||
hasInvalidMetaNameEntry
}
>
{isToolRunning ? (
{isToolRunning || isPollingTask ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Running...
{isPollingTask ? "Polling Task..." : "Running..."}
</>
) : (
<>
Expand Down Expand Up @@ -731,6 +754,7 @@ const ToolsTab = ({
selectedTool={selectedTool}
resourceContent={resourceContent}
onReadResource={onReadResource}
isPollingTask={isPollingTask}
/>
</div>
) : (
Expand Down
Loading