diff --git a/client/src/App.tsx b/client/src/App.tsx index 59d15ba06..a47cad915 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -371,6 +371,12 @@ const App = () => { selectedTaskRef.current = selectedTask; }, [selectedTask]); + const listToolsRef = useRef(null); + const listResourcesRef = useRef(null); + const listResourceTemplatesRef = useRef(null); + const listPromptsRef = useRef(null); + const listTasksRef = useRef(null); + const { connectionStatus, serverCapabilities, @@ -402,7 +408,20 @@ const App = () => { setNotifications((prev) => [...prev, notification as ServerNotification]); if (notification.method === "notifications/tasks/list_changed") { - void listTasks(); + void listTasksRef.current?.(); + } + + if (notification.method === "notifications/tools/list_changed") { + void listToolsRef.current?.(false); + } + + if (notification.method === "notifications/resources/list_changed") { + void listResourcesRef.current?.(false); + void listResourceTemplatesRef.current?.(false); + } + + if (notification.method === "notifications/prompts/list_changed") { + void listPromptsRef.current?.(false); } if (notification.method === "notifications/tasks/status") { @@ -858,33 +877,52 @@ const App = () => { } }; - const listResources = async () => { + const listResources = async (loadMore: boolean = false) => { + const cursor = loadMore ? nextResourceCursor : undefined; const response = await sendMCPRequest( { method: "resources/list" as const, - params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, + params: cursor ? { cursor } : {}, }, ListResourcesResultSchema, "resources", ); - setResources(resources.concat(response.resources ?? [])); + if (loadMore) { + setResources((prev) => prev.concat(response.resources ?? [])); + } else { + const newResources = response.resources ?? []; + setResources(newResources); + + // Preserve selection if it still exists + if (selectedResource) { + const stillExists = newResources.some( + (r) => r.uri === selectedResource.uri, + ); + if (!stillExists) { + setSelectedResource(null); + } + } + } setNextResourceCursor(response.nextCursor); }; - const listResourceTemplates = async () => { + const listResourceTemplates = async (loadMore: boolean = false) => { + const cursor = loadMore ? nextResourceTemplateCursor : undefined; const response = await sendMCPRequest( { method: "resources/templates/list" as const, - params: nextResourceTemplateCursor - ? { cursor: nextResourceTemplateCursor } - : {}, + params: cursor ? { cursor } : {}, }, ListResourceTemplatesResultSchema, "resources", ); - setResourceTemplates( - resourceTemplates.concat(response.resourceTemplates ?? []), - ); + if (loadMore) { + setResourceTemplates((prev) => + prev.concat(response.resourceTemplates ?? []), + ); + } else { + setResourceTemplates(response.resourceTemplates ?? []); + } setNextResourceTemplateCursor(response.nextCursor); }; @@ -979,31 +1017,67 @@ const App = () => { } }; - const listPrompts = async () => { + const listPrompts = async (loadMore: boolean = false) => { + const cursor = loadMore ? nextPromptCursor : undefined; const response = await sendMCPRequest( { method: "prompts/list" as const, - params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, + params: cursor ? { cursor } : {}, }, ListPromptsResultSchema, "prompts", ); - setPrompts(response.prompts); + if (loadMore) { + setPrompts((prev) => prev.concat(response.prompts ?? [])); + } else { + const newPrompts = response.prompts ?? []; + setPrompts(newPrompts); + + // Preserve selection if it still exists + if (selectedPrompt) { + const stillExists = newPrompts.some( + (p) => p.name === selectedPrompt.name, + ); + if (!stillExists) { + setSelectedPrompt(null); + setPromptContent(""); + } + } + } setNextPromptCursor(response.nextCursor); }; - const listTools = async () => { + const listTools = async (loadMore: boolean = false) => { + const cursor = loadMore ? nextToolCursor : undefined; const response = await sendMCPRequest( { method: "tools/list" as const, - params: nextToolCursor ? { cursor: nextToolCursor } : {}, + params: cursor ? { cursor } : {}, }, ListToolsResultSchema, "tools", ); - setTools(response.tools); + if (loadMore) { + setTools((prev) => { + const nextTools = prev.concat(response.tools ?? []); + cacheToolOutputSchemas(nextTools); + return nextTools; + }); + } else { + const newTools = response.tools ?? []; + setTools(newTools); + cacheToolOutputSchemas(newTools); + + // Preserve selection if it still exists + if (selectedTool) { + const stillExists = newTools.some((t) => t.name === selectedTool.name); + if (!stillExists) { + setSelectedTool(null); + setToolResult(null); + } + } + } setNextToolCursor(response.nextCursor); - cacheToolOutputSchemas(response.tools); }; const callTool = async ( @@ -1216,20 +1290,46 @@ const App = () => { } }; - const listTasks = useCallback(async () => { - try { - const response = await listMcpTasks(nextTaskCursor); - setTasks(response.tasks); - setNextTaskCursor(response.nextCursor); - // Inline error clear to avoid extra dependency on clearError - setErrors((prev) => ({ ...prev, tasks: null })); - } catch (e) { - setErrors((prev) => ({ - ...prev, - tasks: (e as Error).message ?? String(e), - })); - } - }, [listMcpTasks, nextTaskCursor]); + useEffect(() => { + listToolsRef.current = listTools; + listResourcesRef.current = listResources; + listResourceTemplatesRef.current = listResourceTemplates; + listPromptsRef.current = listPrompts; + listTasksRef.current = listTasks; + }); + + const listTasks = useCallback( + async (loadMore: boolean = false) => { + try { + const cursor = loadMore ? nextTaskCursor : undefined; + const response = await listMcpTasks(cursor); + if (loadMore) { + setTasks((prev) => prev.concat(response.tasks)); + } else { + setTasks(response.tasks); + // Selection is handled via ref in TasksTab, but we should clear selectedTask + // if it's no longer in the list after a full refresh + if (selectedTaskRef.current) { + const stillExists = response.tasks.some( + (t) => t.taskId === selectedTaskRef.current?.taskId, + ); + if (!stillExists) { + setSelectedTask(null); + } + } + } + setNextTaskCursor(response.nextCursor); + // Inline error clear to avoid extra dependency on clearError + setErrors((prev) => ({ ...prev, tasks: null })); + } catch (e) { + setErrors((prev) => ({ + ...prev, + tasks: (e as Error).message ?? String(e), + })); + } + }, + [listMcpTasks, nextTaskCursor], + ); const cancelTask = async (taskId: string) => { try { @@ -1466,17 +1566,17 @@ const App = () => { { + listResources={(loadMore) => { clearError("resources"); - listResources(); + listResources(loadMore); }} clearResources={() => { setResources([]); setNextResourceCursor(undefined); }} - listResourceTemplates={() => { + listResourceTemplates={(loadMore) => { clearError("resources"); - listResourceTemplates(); + listResourceTemplates(loadMore); }} clearResourceTemplates={() => { setResourceTemplates([]); @@ -1512,9 +1612,9 @@ const App = () => { /> { + listPrompts={(loadMore) => { clearError("prompts"); - listPrompts(); + listPrompts(loadMore); }} clearPrompts={() => { setPrompts([]); @@ -1541,9 +1641,9 @@ const App = () => { !!serverCapabilities?.tasks?.requests?.tools?.call } tools={tools} - listTools={() => { + listTools={(loadMore) => { clearError("tools"); - listTools(); + listTools(loadMore); }} clearTools={() => { setTools([]); @@ -1617,9 +1717,9 @@ const App = () => { { + listTools={(loadMore) => { clearError("tools"); - listTools(); + listTools(loadMore); }} callTool={async ( name: string, diff --git a/client/src/components/AppsTab.tsx b/client/src/components/AppsTab.tsx index 2b6d75358..713de306a 100644 --- a/client/src/components/AppsTab.tsx +++ b/client/src/components/AppsTab.tsx @@ -44,7 +44,7 @@ import { interface AppsTabProps { sandboxPath: string; tools: Tool[]; - listTools: () => void; + listTools: (loadMore?: boolean) => void; callTool: ( name: string, params: Record, diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index fd28a386b..01dc53e2b 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -46,7 +46,7 @@ const PromptsTab = ({ error, }: { prompts: Prompt[]; - listPrompts: () => void; + listPrompts: (loadMore?: boolean) => void; clearPrompts: () => void; getPrompt: (name: string, args: Record) => void; selectedPrompt: Prompt | null; @@ -105,7 +105,7 @@ const PromptsTab = ({
listPrompts(!!nextCursor)} clearItems={() => { clearPrompts(); setSelectedPrompt(null); @@ -129,8 +129,14 @@ const PromptsTab = ({
)} title="Prompts" - buttonText={nextCursor ? "List More Prompts" : "List Prompts"} - isButtonDisabled={!nextCursor && prompts.length > 0} + buttonText={ + nextCursor + ? "List More Prompts" + : prompts.length > 0 + ? "Refresh Prompts" + : "List Prompts" + } + isButtonDisabled={false} />
diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 36e5cec8f..0cdb5fac8 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -42,9 +42,9 @@ const ResourcesTab = ({ }: { resources: Resource[]; resourceTemplates: ResourceTemplate[]; - listResources: () => void; + listResources: (loadMore?: boolean) => void; clearResources: () => void; - listResourceTemplates: () => void; + listResourceTemplates: (loadMore?: boolean) => void; clearResourceTemplates: () => void; readResource: (uri: string) => void; selectedResource: Resource | null; @@ -115,7 +115,7 @@ const ResourcesTab = ({
listResources(!!nextCursor)} clearItems={() => { clearResources(); // Condition to check if selected resource is not resource template's resource @@ -141,13 +141,19 @@ const ResourcesTab = ({
)} title="Resources" - buttonText={nextCursor ? "List More Resources" : "List Resources"} - isButtonDisabled={!nextCursor && resources.length > 0} + buttonText={ + nextCursor + ? "List More Resources" + : resources.length > 0 + ? "Refresh Resources" + : "List Resources" + } + isButtonDisabled={false} /> listResourceTemplates(!!nextTemplateCursor)} clearItems={() => { clearResourceTemplates(); // Condition to check if selected resource is resource template's resource @@ -175,9 +181,13 @@ const ResourcesTab = ({ )} title="Resource Templates" buttonText={ - nextTemplateCursor ? "List More Templates" : "List Templates" + nextTemplateCursor + ? "List More Templates" + : resourceTemplates.length > 0 + ? "Refresh Templates" + : "List Templates" } - isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} + isButtonDisabled={false} />
diff --git a/client/src/components/TasksTab.tsx b/client/src/components/TasksTab.tsx index 32ffa7d46..67fe6872f 100644 --- a/client/src/components/TasksTab.tsx +++ b/client/src/components/TasksTab.tsx @@ -44,7 +44,7 @@ const TasksTab = ({ nextCursor, }: { tasks: Task[]; - listTasks: () => void; + listTasks: (loadMore?: boolean) => void; clearTasks: () => void; cancelTask: (taskId: string) => Promise; selectedTask: Task | null; @@ -75,10 +75,16 @@ const TasksTab = ({ title="Tasks" items={tasks} setSelectedItem={setSelectedTask} - listItems={listTasks} + listItems={() => listTasks(!!nextCursor)} clearItems={clearTasks} - buttonText={nextCursor ? "List More Tasks" : "List Tasks"} - isButtonDisabled={!nextCursor && tasks.length > 0} + buttonText={ + nextCursor + ? "List More Tasks" + : tasks.length > 0 + ? "Refresh Tasks" + : "List Tasks" + } + isButtonDisabled={false} renderItem={(task) => (
@@ -212,7 +218,7 @@ const TasksTab = ({ variant="outline" size="sm" className="mt-4" - onClick={listTasks} + onClick={() => listTasks()} > Refresh Tasks diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index febea1d8f..0edbb672e 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -180,7 +180,7 @@ const ToolsTab = ({ serverSupportsTaskRequests, }: { tools: Tool[]; - listTools: () => void; + listTools: (loadMore?: boolean) => void; clearTools: () => void; callTool: ( name: string, @@ -277,7 +277,7 @@ const ToolsTab = ({
listTools(!!nextCursor)} clearItems={() => { clearTools(); setSelectedTool(null); @@ -299,8 +299,14 @@ const ToolsTab = ({
)} title="Tools" - buttonText={nextCursor ? "List More Tools" : "List Tools"} - isButtonDisabled={!nextCursor && tools.length > 0} + buttonText={ + nextCursor + ? "List More Tools" + : tools.length > 0 + ? "Refresh Tools" + : "List Tools" + } + isButtonDisabled={false} />