From 3e13967cdc15dffba4ca701549d350d24ab12ced Mon Sep 17 00:00:00 2001 From: Urjit Chakraborty <135136842+urjitc@users.noreply.github.com> Date: Sat, 2 May 2026 18:06:08 -0400 Subject: [PATCH 1/2] Show meaningful tool loading labels in chat UI Add a model-supplied `title` field to execute-code and search-workspace schemas (where existing args are too technical for lay users) and thread existing args (title/query) into the loading labels of create-document, create-flashcards, create-quiz, add-youtube-video, web-search, and youtube-search. Also strip https:// from displayed URLs in the web-fetch and web-map tool UIs. Co-authored-by: Cursor --- .../chat/tools/AddYoutubeVideoToolUI.tsx | 9 ++++++++- .../chat/tools/CreateDocumentToolUI.tsx | 11 ++++++++++- .../chat/tools/CreateFlashcardToolUI.tsx | 12 +++++++++++- .../chat/tools/CreateQuizToolUI.tsx | 9 ++++++++- .../chat/tools/ExecuteCodeToolUI.tsx | 10 +++++++--- .../chat/tools/SearchWorkspaceToolUI.tsx | 11 ++++++++--- .../chat/tools/URLContextToolUI.tsx | 2 +- src/components/chat/tools/WebMapToolUI.tsx | 4 ++-- src/components/chat/tools/WebSearchToolUI.tsx | 15 ++++++++++++--- .../chat/tools/YouTubeSearchToolUI.tsx | 8 +++++++- src/lib/ai/tools/execute-code.ts | 2 ++ src/lib/ai/tools/search-workspace.ts | 3 ++- src/lib/ai/tools/tool-utils.ts | 19 +++++++++++++++++++ 13 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/components/chat/tools/AddYoutubeVideoToolUI.tsx b/src/components/chat/tools/AddYoutubeVideoToolUI.tsx index 6d19ac0ad..396475276 100644 --- a/src/components/chat/tools/AddYoutubeVideoToolUI.tsx +++ b/src/components/chat/tools/AddYoutubeVideoToolUI.tsx @@ -130,7 +130,14 @@ export const renderAddYoutubeVideoToolUI: ChatToolUIProps< if (parsed?.success) { content = ; } else if (status.type === "running") { - content = ; + const titleRunning = args?.title?.trim(); + content = ( + + ); } else if (status.type === "complete" && parsed && !parsed.success) { content = ( ); } else if (status.type === "running") { - content = ; + const titleRunning = args?.title?.trim(); + content = ( + + ); } else if (status.type === "incomplete" && status.reason === "error") { content = ( ; + const argsObjRunning = isCreateFlashcardArgsObject(args) ? args : null; + const titleRunning = argsObjRunning?.title?.trim(); + content = ( + + ); } else if (status.type === "complete" && !parsed) { logger.error("🎨 [CreateFlashcardTool] Complete status had no parseable result"); content = ( diff --git a/src/components/chat/tools/CreateQuizToolUI.tsx b/src/components/chat/tools/CreateQuizToolUI.tsx index 138e12e1b..2e28a4103 100644 --- a/src/components/chat/tools/CreateQuizToolUI.tsx +++ b/src/components/chat/tools/CreateQuizToolUI.tsx @@ -244,7 +244,14 @@ function CreateQuizToolRenderer({ let content: ReactNode = null; if (status.type === "running") { - content = ; + const titleRunning = args?.title?.trim(); + content = ( + + ); } else if (status.type === "complete" && parsed?.success) { content = ( ["render"] = ({ status, args, result }) => { + const runningLabel = + typeof args?.title === "string" && args.title.trim().length > 0 + ? `${args.title.trim()}…` + : "Calculating…"; return ( {status.type === "running" ? ( - + ) : status.type === "incomplete" ? (
Couldn't complete this step. Try again or rephrase your question. @@ -54,7 +58,7 @@ function ExecuteCodeResult({ args, result, }: { - args: { code: string }; + args: { title?: string; code: string }; result: CodeExecuteResult | undefined; }) { const [showCode, setShowCode] = useState(false); diff --git a/src/components/chat/tools/SearchWorkspaceToolUI.tsx b/src/components/chat/tools/SearchWorkspaceToolUI.tsx index e4c9d8b68..c5f215524 100644 --- a/src/components/chat/tools/SearchWorkspaceToolUI.tsx +++ b/src/components/chat/tools/SearchWorkspaceToolUI.tsx @@ -6,17 +6,22 @@ import { ToolUIErrorBoundary } from "@/components/tool-ui/shared"; import { ToolUILoadingShell } from "./tool-ui-loading-shell"; import { ToolUIErrorShell } from "./tool-ui-error-shell"; -type GrepArgs = { pattern: string; include?: string; path?: string }; +type GrepArgs = { title?: string; pattern: string; include?: string; path?: string }; type GrepResult = { success: boolean; matches?: number; output?: string; message?: string }; export const renderSearchWorkspaceToolUI: ChatToolUIProps< GrepArgs, GrepResult ->["render"] = ({ status, result }) => { +>["render"] = ({ args, status, result }) => { let content: React.ReactNode = null; if (status.type === "running") { - content = ; + const title = args?.title?.trim(); + content = ( + + ); } else if (status.type === "complete" && result) { if (!result.success && result.message) { content = ( diff --git a/src/components/chat/tools/URLContextToolUI.tsx b/src/components/chat/tools/URLContextToolUI.tsx index 3923d0d35..6ed4d861b 100644 --- a/src/components/chat/tools/URLContextToolUI.tsx +++ b/src/components/chat/tools/URLContextToolUI.tsx @@ -265,7 +265,7 @@ export const renderURLContextToolUI: ChatToolUIProps<{ rel="noopener noreferrer" className="text-xs text-primary hover:underline break-all" > - {url} + {url.replace(/^https?:\/\//, "")}
{urlMeta && isComplete && urlMeta.urlRetrievalStatus && ( diff --git a/src/components/chat/tools/WebMapToolUI.tsx b/src/components/chat/tools/WebMapToolUI.tsx index e672ebdc2..9304fda86 100644 --- a/src/components/chat/tools/WebMapToolUI.tsx +++ b/src/components/chat/tools/WebMapToolUI.tsx @@ -269,7 +269,7 @@ export const renderWebMapToolUI: ChatToolUIProps< rel="noopener noreferrer" className="text-xs text-primary hover:underline break-all" > - {sourceUrl} + {sourceUrl.replace(/^https?:\/\//, "")} )} @@ -316,7 +316,7 @@ export const renderWebMapToolUI: ChatToolUIProps< rel="noopener noreferrer" className="text-xs text-primary hover:underline break-all" > - {link.url} + {link.url.replace(/^https?:\/\//, "")} {link.title && ( diff --git a/src/components/chat/tools/WebSearchToolUI.tsx b/src/components/chat/tools/WebSearchToolUI.tsx index 6158c493b..956c68321 100644 --- a/src/components/chat/tools/WebSearchToolUI.tsx +++ b/src/components/chat/tools/WebSearchToolUI.tsx @@ -189,20 +189,28 @@ const getDomain = (url: string) => { }; const WebSearchContent: FC<{ + args: { query?: string }; status: { type: string }; result: WebSearchResult | null; -}> = ({ status, result }) => { +}> = ({ args, status, result }) => { const isRunning = status.type === "running"; const parsed = result ? parseWebSearchResult(result) : null; const metadata = parsed?.groundingMetadata; const queries = metadata?.webSearchQueries; const sources = parsed?.sources ?? []; + const argsQuery = args?.query?.trim(); + const triggerLabel = isRunning + ? argsQuery + ? `Searching the web for "${argsQuery}"` + : "Searching the web" + : "Searched the web"; + return ( } /> @@ -277,12 +285,13 @@ export const renderWebSearchToolUI: ChatToolUIProps< { query: string }, WebSearchResult >["render"] = ({ + args, status, result, }) => { return ( - + ); }; diff --git a/src/components/chat/tools/YouTubeSearchToolUI.tsx b/src/components/chat/tools/YouTubeSearchToolUI.tsx index 315c3db4b..d5ee9927f 100644 --- a/src/components/chat/tools/YouTubeSearchToolUI.tsx +++ b/src/components/chat/tools/YouTubeSearchToolUI.tsx @@ -256,7 +256,13 @@ const YouTubeSearchContent: FC<{
- {isRunning ? "Searching YouTube..." : "YouTube Search"} + {isRunning + ? args?.query?.trim() + ? `Searching YouTube for "${args.query.trim()}"…` + : "Searching YouTube…" + : args?.query?.trim() + ? `YouTube: "${args.query.trim()}"` + : "YouTube Search"} {status.type === "complete" && result && ( diff --git a/src/lib/ai/tools/execute-code.ts b/src/lib/ai/tools/execute-code.ts index 9bb8ec77a..2486d0485 100644 --- a/src/lib/ai/tools/execute-code.ts +++ b/src/lib/ai/tools/execute-code.ts @@ -8,6 +8,7 @@ import { type CodeExecuteResult, type CodeExecuteStep, } from "@/lib/ai/code-execute-shared"; +import { toolTitleField } from "./tool-utils"; const SANDBOX_TIMEOUT_MS = 300_000; const EXECUTION_TIMEOUT_MS = 60_000; @@ -84,6 +85,7 @@ export function createExecuteCodeTool() { description: buildCodeExecuteToolDescription(), inputSchema: zodSchema( z.object({ + title: toolTitleField, code: z .string() .min(1) diff --git a/src/lib/ai/tools/search-workspace.ts b/src/lib/ai/tools/search-workspace.ts index e30646545..8ca076b3f 100644 --- a/src/lib/ai/tools/search-workspace.ts +++ b/src/lib/ai/tools/search-workspace.ts @@ -1,6 +1,6 @@ import { tool, zodSchema } from "ai"; import { z } from "zod"; -import { loadStateForTool } from "./tool-utils"; +import { loadStateForTool, toolTitleField } from "./tool-utils"; import { extractSearchableText } from "./workspace-search-utils"; import { getVirtualPath } from "@/lib/utils/workspace-fs"; import type { WorkspaceToolContext } from "./workspace-tools"; @@ -44,6 +44,7 @@ export function createSearchWorkspaceTool(ctx: WorkspaceToolContext) { "Grep search across workspace. All item types match on path/title, including documents, flashcards, PDFs, quizzes, audio, images, websites, and YouTube cards. Types with readable body content or metadata also search that body. Line numbers for content matches align with workspace_read(path, lineStart). include: optional item type filter. path: folder prefix or exact item path; for long items use workspace_read(path, lineStart) on matches. Plain text or regex. Max 100 matches.", inputSchema: zodSchema( z.object({ + title: toolTitleField, pattern: z.string().describe("Search pattern (plain text or regex)"), include: z.string().optional().describe('Optional item type filter, e.g. "document", "flashcard", "pdf", "quiz", "audio", "image", "website", or "youtube"'), path: z.string().optional().describe("Folder prefix (Physics/) or exact item path (Physics/documents/File.md)"), diff --git a/src/lib/ai/tools/tool-utils.ts b/src/lib/ai/tools/tool-utils.ts index 205838da1..4d91e5738 100644 --- a/src/lib/ai/tools/tool-utils.ts +++ b/src/lib/ai/tools/tool-utils.ts @@ -2,11 +2,30 @@ * Shared utilities for AI tools */ +import { z } from "zod"; import { loadWorkspaceState } from "@/lib/workspace/workspace-state-read"; import type { Item } from "@/lib/workspace-state/types"; import type { WorkspaceToolContext } from "./workspace-tools"; import { resolveItemByPath } from "./workspace-search-utils"; +/** + * Shared `title` field for tool inputs whose other arguments are technical + * (regex patterns, raw URLs, generated code). The model writes a short + * present-tense gerund phrase that the UI shows while the tool runs, in + * place of a generic "Loading…" label. + * + * Place this as the FIRST field in a tool's input schema so it streams in + * before the heavier arguments and the loading shell can render it + * immediately during `input-streaming`. + */ +export const toolTitleField = z + .string() + .min(1) + .max(60) + .describe( + 'A short present-tense gerund phrase (3–6 words) describing what this tool call is doing in plain language for a non-technical user. Shown in the UI while the tool runs. Examples: "Searching your notes for photosynthesis", "Reading the Wikipedia article", "Computing average grades". No trailing punctuation, no tool names, no jargon (regex, URL, etc.).', + ); + /** * Sanitize tool results for the model's context window. * From 6244496a39c1504300371aaeffcd3f4dc2234377 Mon Sep 17 00:00:00 2001 From: Urjit Chakraborty <135136842+urjitc@users.noreply.github.com> Date: Sat, 2 May 2026 18:14:10 -0400 Subject: [PATCH 2/2] Address PR review: make title optional, keep query in completed web search label - Make `toolTitleField` optional so older saved chats and occasional LLM omissions don't fail input validation; UI already has fallbacks. - Preserve the search query in the completed-state label of WebSearchToolUI for consistency with YouTubeSearchToolUI. Co-authored-by: Cursor --- src/components/chat/tools/WebSearchToolUI.tsx | 4 +++- src/lib/ai/tools/tool-utils.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/chat/tools/WebSearchToolUI.tsx b/src/components/chat/tools/WebSearchToolUI.tsx index 956c68321..bcbac63f6 100644 --- a/src/components/chat/tools/WebSearchToolUI.tsx +++ b/src/components/chat/tools/WebSearchToolUI.tsx @@ -204,7 +204,9 @@ const WebSearchContent: FC<{ ? argsQuery ? `Searching the web for "${argsQuery}"` : "Searching the web" - : "Searched the web"; + : argsQuery + ? `Searched the web for "${argsQuery}"` + : "Searched the web"; return ( diff --git a/src/lib/ai/tools/tool-utils.ts b/src/lib/ai/tools/tool-utils.ts index 4d91e5738..be89d7c9b 100644 --- a/src/lib/ai/tools/tool-utils.ts +++ b/src/lib/ai/tools/tool-utils.ts @@ -22,8 +22,9 @@ export const toolTitleField = z .string() .min(1) .max(60) + .optional() .describe( - 'A short present-tense gerund phrase (3–6 words) describing what this tool call is doing in plain language for a non-technical user. Shown in the UI while the tool runs. Examples: "Searching your notes for photosynthesis", "Reading the Wikipedia article", "Computing average grades". No trailing punctuation, no tool names, no jargon (regex, URL, etc.).', + 'A short present-tense gerund phrase (3–6 words) describing what this tool call is doing in plain language for a non-technical user. Shown in the UI while the tool runs. Examples: "Searching your notes for photosynthesis", "Reading the Wikipedia article", "Computing average grades". No trailing punctuation, no tool names, no jargon (regex, URL, etc.). Strongly encouraged on every call so the user sees meaningful progress instead of a generic loading label.', ); /**