From 5b7c0615f31bf9611ba9ce2b6a28c30ff6744d42 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Fri, 3 Apr 2026 14:03:27 +0200 Subject: [PATCH] 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 --- studio/backend/core/inference/llama_cpp.py | 16 +++++++++-- .../assistant-ui/tool-ui-web-search.tsx | 27 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index d752500bf4..4da62e23c3 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -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] diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx index d3a86846de..da4c0cbbb9 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx @@ -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 ( @@ -83,7 +101,12 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({ {isRunning ? (
- Searching for “{query}”… + + {isUrlFetch + ? <>Reading {displayDomain || "page"}… + : <>Searching for “{query}”… + } +
) : sources.length > 0 ? (