diff --git a/src/components/chat/tools/AddYoutubeVideoToolUI.tsx b/src/components/chat/tools/AddYoutubeVideoToolUI.tsx index 6d19ac0a..39647527 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 138e12e1..2e28a410 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 e4c9d8b6..c5f21552 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 3923d0d3..6ed4d861 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 e672ebdc..9304fda8 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 6158c493..bcbac63f 100644 --- a/src/components/chat/tools/WebSearchToolUI.tsx +++ b/src/components/chat/tools/WebSearchToolUI.tsx @@ -189,20 +189,30 @@ 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" + : argsQuery + ? `Searched the web for "${argsQuery}"` + : "Searched the web"; + return ( } /> @@ -277,12 +287,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 315c3db4..d5ee9927 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 9bb8ec77..2486d048 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 e3064654..8ca076b3 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 205838da..be89d7c9 100644 --- a/src/lib/ai/tools/tool-utils.ts +++ b/src/lib/ai/tools/tool-utils.ts @@ -2,11 +2,31 @@ * 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) + .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.). Strongly encouraged on every call so the user sees meaningful progress instead of a generic loading label.', + ); + /** * Sanitize tool results for the model's context window. *