feat(studio): differentiate web search and URL fetch in chat tool UI (#4802)

Differentiate web_search query searches from URL fetches in the Studio chat UI.

Backend (llama_cpp.py):
- Emit "Reading: hostname" for URL fetches and "Searching: query" for query searches in SSE status events
- Only show hostname for valid http/https URLs; schemeless/non-http URLs get "Reading page..." generic fallback
- Strip www. prefix for consistency with the frontend

Frontend (tool-ui-web-search.tsx):
- Tool card shows "Read hostname" / "Reading hostname..." for URL fetches
- Shows "Searched query" / "Searching for query..." for query searches
- Uses new URL() with protocol check; falls back to "Read page" / "Reading page..." for non-http URLs
This commit is contained in:
Wasim Yousef Said 2026-04-03 14:03:27 +02:00 committed by GitHub
commit 5b7c0615f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 4 deletions

View file

@ -22,6 +22,7 @@ import threading
import time
from pathlib import Path
from typing import Generator, Optional
from urllib.parse import urlparse
import httpx
@ -2270,7 +2271,7 @@ class LlamaCppBackend:
Agentic loop: let the model call tools, execute them, and continue.
Yields dicts with:
{"type": "status", "text": "Searching: ..."} -- tool status updates
{"type": "status", "text": "Searching: ..."/"Reading: ..."} -- tool status updates
{"type": "content", "text": "token"} -- streamed content tokens (cumulative)
{"type": "reasoning", "text": "token"} -- streamed reasoning tokens (cumulative)
"""
@ -2837,7 +2838,18 @@ class LlamaCppBackend:
arguments = raw_args
if tool_name == "web_search":
status_text = f"Searching: {arguments.get('query', '')}"
_ws_url = (arguments.get("url") or "").strip()
if _ws_url:
_parsed = urlparse(_ws_url)
if _parsed.scheme in ("http", "https") and _parsed.hostname:
_ws_host = _parsed.hostname
if _ws_host.startswith("www."):
_ws_host = _ws_host[4:]
status_text = f"Reading: {_ws_host}"
else:
status_text = "Reading page..."
else:
status_text = f"Searching: {arguments.get('query', '')}"
elif tool_name == "python":
preview = (
(arguments.get("code") or "").strip().split("\n")[0][:60]

View file

@ -52,6 +52,18 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({
status,
}) => {
const query = (args as { query?: string })?.query ?? "";
const url = ((args as { url?: string })?.url ?? "").trim();
const isUrlFetch = !!url;
const displayDomain = (() => {
if (!url) return "";
try {
const parsed = new URL(url);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "";
return parsed.hostname.replace(/^www\./, "");
} catch {
return "";
}
})();
const isRunning = status?.type === "running";
const sources = result
? parseSearchResults(
@ -75,7 +87,13 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({
return (
<ToolFallbackRoot open={open} onOpenChange={setOpen}>
<ToolFallbackTrigger
toolName={query ? `Searched "${query}"` : "Web Search"}
toolName={
isUrlFetch
? displayDomain ? `Read ${displayDomain}` : "Read page"
: query
? `Searched "${query}"`
: "Web Search"
}
status={status}
icon={GlobeIcon}
/>
@ -83,7 +101,12 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({
{isRunning ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<LoaderIcon className="size-3.5 animate-spin" />
<span>Searching for &ldquo;{query}&rdquo;&hellip;</span>
<span>
{isUrlFetch
? <>Reading {displayDomain || "page"}&hellip;</>
: <>Searching for &ldquo;{query}&rdquo;&hellip;</>
}
</span>
</div>
) : sources.length > 0 ? (
<div className="flex flex-wrap gap-1.5">