* feat: Add shimmer text animation for processing state indicators * feat: Redesign CollapsibleContentBlock component with improved UX * feat: Add conditional setting display support with dependsOn field * feat: Add showAgenticTurnStats setting for per-turn statistics * feat: Update ChatMessageAgenticContent with improved UI and new features * feat: Enhance file read tool UI/UX * feat: Refine styling of collapsible content and code preview blocks * feat: add terminal variant to CollapsibleContentBlock * feat: add built-in tools UI registry * feat: extract ChatMessageReasoningBlock and ChatMessageToolCallBlock * refactor: simplify ChatMessageAgenticContent to use extracted blocks * fix: correct markdown content block margin spacing * fix: reorganize SettingsChatFields layout and reset button positioning * fix: use direct map access in agentic store session methods * refactor: remove reasoning preview/throttle system from CollapsibleContentBlock * feat: add auto-scroll to reasoning block and remove showThoughtInProgress * feat: add ChatMessageToolCallDateTime component and support for new tool types * feat: improve auto-scroll reliability in reasoning block with RAF coalescing and MutationObserver * feat: show MCP server favicon for tools without a built-in icon * feat: add search-results parsing utilities and tests * feat: add ChatMessageToolCallSearchResults component * feat: integrate search results rendering into ChatMessageAgenticContent * feat: display tool call input alongside output in ChatMessageToolCallBlock * style: use muted foreground color in reasoning block content * chore: Format * feat: Refine reasoning block layout and make pending thoughts display configurable * feat: Stream tool call code blocks with auto-scroll and handle partial JSON * feat: add streaming permission gate infrastructure * feat: wire permission gate into the agentic loop * fix: bail out on abort and skip already-approved tool calls * fix: clear partial tool calls on abort and savePartialResponse * test: cover partial tool call cleanup end-to-end * refactor: Remove streaming permission gate logic * fix: Correct autoscroll and streaming gates for tool calls and reasoning blocks * refactor: Chat Message Assistant componentization * fix: Show health metadata for disabled MCP servers and promote connections on enable * fix: Inherit global enabled state for missing MCP per-chat overrides * refactor: Cleanup * refactor: Split ChatMessageToolCallBlock into dedicated components * feat: Add live streaming and auto-scroll for tool execution output * feat: Add line numbers and change markers to file edit diffs * chore: Formatting * feat: Add type definitions and utilities for recommended MCP servers * feat: Add recommended MCP servers configuration and storage key * feat: Add McpServerCardCompact component for recommended servers * feat: Add recommended servers section to Add New Server dialog * feat: Update McpServerForm to support authorization requirements * feat: Add select-none classes for text selection prevention * feat: Add recommended MCP server icon assets * refactor: Store dismissed MCP recommendations as a boolean flag * feat: Render tool results as JSON or Markdown based on detected content type * feat: UI improvement * feat: Render search block early and update heading to show execution state * fix: Prevent non-web-search tools from triggering the search UI block * refactor: Cleanup * refactor: Extract hardcoded icon size classes into shared constants * refactor: Extract hardcoded tool result separator into a shared constant * refactor: Tool Calls UI/logic * refactor: Cleanup * refactor: Cleanup * refactor: Cleanup
113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import {
|
|
TRAILING_SLASHES_REGEX,
|
|
TWO_PART_PUBLIC_SUFFIXES,
|
|
WILDCARD_PUBLIC_SUFFIXES
|
|
} from '$lib/constants';
|
|
import { UrlProtocol } from '$lib/enums';
|
|
|
|
/**
|
|
* Check whether a hostname looks like an IPv4 or IPv6 address.
|
|
*/
|
|
function isIpAddress(hostname: string): boolean {
|
|
if (hostname.includes(':')) return true;
|
|
|
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Extract the registrable root domain from a URL.
|
|
*
|
|
* @example
|
|
* 'mcp.example.com' -> 'example.com'
|
|
* 'www.example.co.uk' -> 'example.co.uk'
|
|
* 'bar.foo.nom.br' -> 'bar.foo.nom.br'
|
|
* '192.168.1.1' -> null
|
|
* 'localhost' -> null
|
|
*/
|
|
export function extractRootDomain(url: URL): string | null {
|
|
const hostname = url.hostname.toLowerCase();
|
|
if (!hostname || isIpAddress(hostname)) return null;
|
|
|
|
const parts = hostname.split('.');
|
|
|
|
if (parts.length < 2) return null;
|
|
|
|
if (parts.length >= 3) {
|
|
const suffix2 = `${parts[parts.length - 2]}.${parts[parts.length - 1]}`;
|
|
|
|
if (TWO_PART_PUBLIC_SUFFIXES.has(suffix2)) {
|
|
return parts.slice(-3).join('.');
|
|
}
|
|
}
|
|
|
|
for (let i = 2; i <= parts.length; i++) {
|
|
const candidate = parts.slice(-i).join('.');
|
|
|
|
if (WILDCARD_PUBLIC_SUFFIXES.has(candidate)) {
|
|
if (parts.length === i + 1) {
|
|
return hostname;
|
|
}
|
|
|
|
return parts.slice(-(i + 2)).join('.');
|
|
}
|
|
}
|
|
|
|
return parts.slice(-2).join('.');
|
|
}
|
|
|
|
/**
|
|
* Sanitize an external URL string for safe use in an `<a href>`.
|
|
* Only allows http: and https: schemes. Returns `null` for anything else.
|
|
*/
|
|
export function sanitizeExternalUrl(raw: string): string | null {
|
|
try {
|
|
const url = new URL(raw);
|
|
|
|
if (url.protocol !== UrlProtocol.HTTP && url.protocol !== UrlProtocol.HTTPS) {
|
|
return null;
|
|
}
|
|
|
|
return url.href;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Canonicalize a server URL for "is this the same server?" checks across
|
|
* the user's settings and the recommended-server list. Lowercases scheme
|
|
* and host, drops the port entirely, and strips any trailing slashes off
|
|
* the path so a stored `https://api.example.com:8443/mcp/` matches the
|
|
* recommended `https://api.example.com/mcp`. Falls back to a cheap
|
|
* trim+lowercase+strip pass when the input isn't a parseable URL.
|
|
*
|
|
* Query strings are preserved deliberately - if the user entered one,
|
|
* it's part of their endpoint. The port is always stripped because the
|
|
* underlying `URL` parser is asymmetric (it auto-drops HTTPS default
|
|
* :443 but keeps HTTP default :80), so a half-hearted "drop default
|
|
* ports" policy never matches consistently across schemes.
|
|
*/
|
|
export function canonicalizeServerUrl(raw: string): string {
|
|
const trimmed = raw.trim();
|
|
|
|
try {
|
|
const parsed = new URL(trimmed);
|
|
const pathname = parsed.pathname.replace(TRAILING_SLASHES_REGEX, '');
|
|
|
|
// Aggressive: drop the port unconditionally. We only use this for
|
|
// equality checks between user-typed URLs and a hard-coded list of
|
|
// recommendations, where the port can never carry distinguishing
|
|
// information we care about (a different port = a different server,
|
|
// but two URLs that differ only in `:80` vs no-port are clearly the
|
|
// same intent). Lowercasing the hostname matches HTTP/HTTPS
|
|
// case-insensitivity - the URL parser does NOT lowercase it.
|
|
const host = parsed.hostname.toLowerCase();
|
|
|
|
return `${parsed.protocol}//${host}${pathname}${parsed.search}`;
|
|
} catch {
|
|
return trimmed.toLowerCase().replace(TRAILING_SLASHES_REGEX, '');
|
|
}
|
|
}
|