From 457078937d90b2ae7a264cd927cb991c3371c50a Mon Sep 17 00:00:00 2001
From: go165 <196723798+go165@users.noreply.github.com>
Date: Sun, 14 Jun 2026 14:09:50 +0800
Subject: [PATCH] fix(client): keep field descriptions visible
---
client/src/components/AppsTab.tsx | 32 ++++++++++++++++++-
.../src/components/SchemaFieldDescription.tsx | 22 +++++++++++++
client/src/components/ToolsTab.tsx | 29 ++++++++++++++++-
.../src/components/__tests__/AppsTab.test.tsx | 31 ++++++++++++++++++
.../components/__tests__/ToolsTab.test.tsx | 30 +++++++++++++++++
5 files changed, 142 insertions(+), 2 deletions(-)
create mode 100644 client/src/components/SchemaFieldDescription.tsx
diff --git a/client/src/components/AppsTab.tsx b/client/src/components/AppsTab.tsx
index 2b6d75358..0518fca0b 100644
--- a/client/src/components/AppsTab.tsx
+++ b/client/src/components/AppsTab.tsx
@@ -21,6 +21,7 @@ import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge"
import AppRenderer from "./AppRenderer";
import ListPane from "./ListPane";
import IconDisplay, { WithIcons } from "./IconDisplay";
+import SchemaFieldDescription from "./SchemaFieldDescription";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -411,6 +412,12 @@ const AppsTab = ({
key,
inputSchema,
);
+ const descriptionId = `${key}-description`;
+ const showFieldDescription =
+ !!prop.description &&
+ (prop.type === "string" ||
+ prop.type === "number" ||
+ prop.type === "integer");
return (
@@ -496,7 +503,14 @@ const AppsTab = ({
});
}}
>
-
+
)}
+ {showFieldDescription && (
+
+ )}
);
diff --git a/client/src/components/SchemaFieldDescription.tsx b/client/src/components/SchemaFieldDescription.tsx
new file mode 100644
index 000000000..9f242fb0e
--- /dev/null
+++ b/client/src/components/SchemaFieldDescription.tsx
@@ -0,0 +1,22 @@
+interface SchemaFieldDescriptionProps {
+ id: string;
+ description?: string;
+}
+
+export default function SchemaFieldDescription({
+ id,
+ description,
+}: SchemaFieldDescriptionProps) {
+ if (!description) {
+ return null;
+ }
+
+ return (
+
+ {description}
+
+ );
+}
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 15b85fd67..dbffab5f7 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -40,6 +40,7 @@ import { useEffect, useState, useRef } from "react";
import ListPane from "./ListPane";
import JsonView from "./JsonView";
import ToolResults from "./ToolResults";
+import SchemaFieldDescription from "./SchemaFieldDescription";
import { useToast } from "@/lib/hooks/useToast";
import useCopy from "@/lib/hooks/useCopy";
import IconDisplay, { WithIcons } from "./IconDisplay";
@@ -354,6 +355,12 @@ const ToolsTab = ({
const inputSchema =
selectedTool.inputSchema as JsonSchemaType;
const required = isPropertyRequired(key, inputSchema);
+ const descriptionId = `${key}-description`;
+ const showFieldDescription =
+ !!prop.description &&
+ (prop.type === "string" ||
+ prop.type === "number" ||
+ prop.type === "integer");
return (
@@ -447,7 +454,15 @@ const ToolsTab = ({
}
}}
>
-
+
)}
+ {showFieldDescription && (
+
+ )}
);
diff --git a/client/src/components/__tests__/AppsTab.test.tsx b/client/src/components/__tests__/AppsTab.test.tsx
index 315a95ccf..233839eb9 100644
--- a/client/src/components/__tests__/AppsTab.test.tsx
+++ b/client/src/components/__tests__/AppsTab.test.tsx
@@ -198,6 +198,37 @@ describe("AppsTab", () => {
expect(screen.getByText("Tool: fieldsApp")).toBeInTheDocument();
});
+ it("should keep input field descriptions visible after values are filled", () => {
+ const toolWithDescription: Tool = {
+ name: "describedApp",
+ inputSchema: {
+ type: "object",
+ properties: {
+ query: {
+ type: "string",
+ description: "Search phrase to render",
+ },
+ },
+ },
+ _meta: { ui: { resourceUri: "ui://described" } },
+ } as Tool & { _meta?: { ui?: { resourceUri?: string } } };
+
+ renderAppsTab({
+ tools: [toolWithDescription],
+ });
+
+ fireEvent.click(screen.getByText("describedApp"));
+
+ const input = screen.getByLabelText("query");
+ expect(input).toHaveAttribute("aria-describedby", "query-description");
+ expect(screen.getByText("Search phrase to render")).toBeVisible();
+
+ fireEvent.change(input, { target: { value: "filled value" } });
+
+ expect(input).toHaveValue("filled value");
+ expect(screen.getByText("Search phrase to render")).toBeVisible();
+ });
+
it("should close app renderer when close button is clicked", async () => {
renderAppsTab({
tools: [mockAppTool],
diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx
index 5678914d6..695ccb938 100644
--- a/client/src/components/__tests__/ToolsTab.test.tsx
+++ b/client/src/components/__tests__/ToolsTab.test.tsx
@@ -1015,6 +1015,36 @@ describe("ToolsTab", () => {
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
expect(screen.getByRole("textbox")).toBeInTheDocument();
});
+
+ it("should keep string parameter descriptions visible after input is filled", () => {
+ const toolWithStringParam: Tool = {
+ name: "stringTool",
+ description: "Tool with regular string parameter",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ text: {
+ type: "string" as const,
+ description: "Some text input",
+ },
+ },
+ },
+ };
+
+ renderToolsTab({
+ tools: [toolWithStringParam],
+ selectedTool: toolWithStringParam,
+ });
+
+ const input = screen.getByRole("textbox");
+ expect(input).toHaveAttribute("aria-describedby", "text-description");
+ expect(screen.getByText("Some text input")).toBeVisible();
+
+ fireEvent.change(input, { target: { value: "filled value" } });
+
+ expect(input).toHaveValue("filled value");
+ expect(screen.getByText("Some text input")).toBeVisible();
+ });
});
describe("JSON Validation Integration", () => {