ui: Refactor Built-In Tools naming (Server/Browser) (#27271)

* server: rename built-in tools to server tools

* ui: rename built-in tools to server/browser tools
This commit is contained in:
Aleksander Grygier
2026-08-17 22:23:22 +02:00
committed by GitHub
parent 058df671b2
commit 0021a77de0
49 changed files with 315 additions and 286 deletions
@@ -35,7 +35,7 @@
<span>
Run llama-server with <code>{CLI_FLAGS.TOOLS}</code> flag to enable
<strong>Built-in Tools</strong>.
<strong>Server Tools</strong>.
</span>
</span>
@@ -62,7 +62,7 @@
// it, the picker still opens for manual entry but explains why search is
// unavailable instead of firing searches that would only fail. Browse is
// hidden too: it resolves the picked folder name through the same tool.
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.FILE_GLOB_SEARCH));
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.SERVER_FILE_GLOB_SEARCH));
const fileSearchEnabled = $derived(
fileSearchKey !== null && toolsStore.isToolEnabled(fileSearchKey)
);
@@ -212,7 +212,7 @@
// so the caller fails visibly instead of committing a bare leaf name.
async function resolveNativeName(name: string): Promise<string | null> {
try {
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
const res = await ToolsService.executeToolRaw(BuiltInTool.SERVER_FILE_GLOB_SEARCH, {
include: buildCaseInsensitiveGlob(name),
limit: SEARCH.NATIVE_LIMIT,
max_depth: SEARCH.NATIVE_MAX_DEPTH,
@@ -51,7 +51,7 @@
// When the server does not expose file_glob_search (started without
// --tools) or the user disabled it, the picker still opens but explains
// why instead of firing searches that would only fail.
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.FILE_GLOB_SEARCH));
const fileSearchKey = $derived(toolsStore.getPermissionKey(BuiltInTool.SERVER_FILE_GLOB_SEARCH));
const fileSearchEnabled = $derived(
fileSearchKey !== null && toolsStore.isToolEnabled(fileSearchKey)
);
@@ -35,19 +35,19 @@
{#if isSearchCall}
<ChatMessageToolCallBlockSearchResults {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.GET_DATETIME}
{:else if section.toolName === BuiltInTool.BROWSER_GET_DATETIME}
<ChatMessageToolCallBlockGetDatetime {section} {isStreaming} />
{:else if section.toolName === BuiltInTool.GET_INFO}
{:else if section.toolName === BuiltInTool.SERVER_GET_INFO}
<ChatMessageToolCallBlockGetInfo {section} {isStreaming} />
{:else if section.toolName === BuiltInTool.READ_FILE}
{:else if section.toolName === BuiltInTool.SERVER_READ_FILE}
<ChatMessageToolCallBlockReadFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.READ_MEDIA}
{:else if section.toolName === BuiltInTool.BROWSER_READ_MEDIA}
<ChatMessageToolCallBlockReadMedia {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.EDIT_FILE}
{:else if section.toolName === BuiltInTool.SERVER_EDIT_FILE}
<ChatMessageToolCallBlockEditFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.WRITE_FILE}
{:else if section.toolName === BuiltInTool.SERVER_WRITE_FILE}
<ChatMessageToolCallBlockWriteFile {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.EXEC_SHELL_COMMAND}
{:else if section.toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND}
<ChatMessageToolCallBlockExecShellCommand
{section}
{open}
@@ -56,11 +56,11 @@
{attachments}
{onToggle}
/>
{:else if section.toolName === BuiltInTool.FILE_GLOB_SEARCH}
{:else if section.toolName === BuiltInTool.SERVER_FILE_GLOB_SEARCH}
<ChatMessageToolCallBlockFileGlobSearch {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.GREP_SEARCH}
{:else if section.toolName === BuiltInTool.SERVER_GREP_SEARCH}
<ChatMessageToolCallBlockGrepSearch {section} {open} {isStreaming} {onToggle} />
{:else if section.toolName === BuiltInTool.RUN_JAVASCRIPT}
{:else if section.toolName === BuiltInTool.BROWSER_RUN_JAVASCRIPT}
<ChatMessageToolCallBlockRunJavascript {section} {open} {isStreaming} {onToggle} />
{:else}
<ChatMessageToolCallBlockDefault {section} {open} {isStreaming} {attachments} {onToggle} />
@@ -12,7 +12,7 @@
import {
classifyToolResult,
formatJsonPretty,
getBuiltinToolUi,
getToolUi,
parseToolResultWithMedia
} from '$lib/utils';
import { createBase64DataUrl } from '$lib/utils/data-url';
@@ -27,7 +27,7 @@
let { attachments, isStreaming, onToggle, open, section }: Props = $props();
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? '');
const outputKind = $derived(classifyToolResult(section.toolResult));
const parsedLines: ToolResultLine[] = $derived(
section.toolResult ? parseToolResultWithMedia(section.toolResult, attachments) : []
@@ -6,7 +6,7 @@
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
import { FileTypeText } from '$lib/enums';
import type { AgenticSection } from '$lib/types';
import { getBuiltinToolUi } from '$lib/utils';
import { getToolUi } from '$lib/utils';
interface Props {
section: AgenticSection;
@@ -18,7 +18,7 @@
let { isStreaming, onToggle, open, section }: Props = $props();
const runJsMeta = $derived(parseRunJavascriptMeta(section));
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? '');
</script>
<ToolCallBlock {section} {open} {isStreaming} meta={runJsMeta} {title} {onToggle}>
@@ -14,8 +14,8 @@
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
import { AgenticSectionType } from '$lib/enums';
import { mcpStore } from '$lib/stores';
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/types';
import { getBuiltinToolUi } from '$lib/utils';
import type { AgenticSection, ToolUiEntry } from '$lib/types';
import { getToolUi } from '$lib/utils';
import type { Component, Snippet } from 'svelte';
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
@@ -82,7 +82,7 @@
const showSpinner = $derived(isPending || (isStreamingCall && isStreaming) || extraLiveStreaming);
const isCodeStreaming = $derived(isStreaming && (isPending || isStreamingCall));
const toolUi: BuiltinToolUiEntry | null = $derived(getBuiltinToolUi(section.toolName));
const toolUi: ToolUiEntry | null = $derived(getToolUi(section.toolName));
const toolIcon: Component = $derived(
spinIconWhenActive && showSpinner ? Loader2 : (toolUi?.icon ?? Wrench)
);
@@ -24,7 +24,7 @@ export type EditFileMeta = {
};
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_EDIT_FILE, section, { partial: true });
if (!args) return null;
@@ -14,7 +14,7 @@ export type ExecShellCommandMeta = {
};
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
const args = parseToolArgs(BuiltInTool.SERVER_EXEC_SHELL_COMMAND, section);
if (!args) return null;
@@ -19,7 +19,7 @@ export type FileGlobSearchMeta = {
};
export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
const args = parseToolArgs(BuiltInTool.SERVER_FILE_GLOB_SEARCH, section);
if (!args) return null;
@@ -28,7 +28,7 @@ export type GrepSearchMeta = {
};
export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
const args = parseToolArgs(BuiltInTool.SERVER_GREP_SEARCH, section);
if (!args) return null;
@@ -16,7 +16,7 @@ export type ReadFileMeta = {
};
export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_READ_FILE, section, { partial: true });
if (!args) return null;
@@ -16,7 +16,7 @@ export type RunJavascriptMeta = {
};
export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
const args = parseToolArgs(BuiltInTool.BROWSER_RUN_JAVASCRIPT, section);
if (!args) return null;
@@ -20,7 +20,7 @@ export type WriteFileMeta = {
};
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
const args = parseToolArgs(BuiltInTool.SERVER_WRITE_FILE, section, { partial: true });
if (!args) return null;
@@ -61,8 +61,8 @@
{:else}
{@const source = toolsStore.getToolSource(toolName)}
{@const providerName =
source === ToolSource.BUILTIN
? TOOL_SERVER_LABELS[ToolSource.BUILTIN]
source === ToolSource.SERVER
? TOOL_SERVER_LABELS[ToolSource.SERVER]
: source === ToolSource.CUSTOM
? TOOL_SERVER_LABELS[ToolSource.CUSTOM]
: 'MCP Tools'}
@@ -278,7 +278,7 @@ export { default as ChatFormInput } from './ChatForm/ChatFormInput/ChatFormInput
/**
* Working directory selector for agent mode. Renders a chip below the chat
* form; clicking it opens a popover with a directory picker backed by the
* server's `file_glob_search` built-in tool (POST /tools). The picked
* server's `file_glob_search` server tool (POST /tools). The picked
* directory is exposed via `bind:directory`; changing it records a
* synthetic "Set working directory to ..." user message into chat history
* and is enforced on tool calls via the `x-tool-cwd` request header.
@@ -380,7 +380,7 @@ export { default as ChatFormPickerListItemSkeleton } from './ChatForm/ChatFormPi
/**
* `@`-triggered file/folder mention picker. Resolves `@<query>` in the chat
* input to a filesystem match via the server's `file_glob_search` built-in
* input to a filesystem match via the server's `file_glob_search` server tool
* tool, scoped to the conversation cwd (or server home when unset).
* Selection splices a `[name](file:///<abs path>)` link into the input.
*/
@@ -6,7 +6,7 @@
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ToolSource } from '$lib/enums/tools.enums';
import { mcpStore, permissionsStore, toolsStore } from '$lib/stores';
import { getBuiltinToolUi } from '$lib/utils';
import { getToolUi } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
let expandedGroups = new SvelteSet<string>();
@@ -69,12 +69,12 @@
{#each group.tools as entry (entry.key)}
{@const toolName = entry.definition.function.name}
{@const builtinUi =
entry.source === ToolSource.BUILTIN || entry.source === ToolSource.FRONTEND
? getBuiltinToolUi(toolName)
{@const toolUi =
entry.source === ToolSource.SERVER || entry.source === ToolSource.BROWSER
? getToolUi(toolName)
: null}
{@const displayLabel = builtinUi?.label ?? toolName}
{@const IconComponent = builtinUi?.icon ?? null}
{@const displayLabel = toolUi?.label ?? toolName}
{@const IconComponent = toolUi?.icon ?? null}
{@const isEnabled = toolsStore.isToolEnabled(entry.key)}
{@const permissionKey = entry.key}
{@const isAlwaysAllowed = permissionsStore.hasTool(permissionKey)}
@@ -69,7 +69,7 @@ export { default as SettingsChatFields } from './SettingsChat/SettingsChatFields
/**
* **SettingsChatToolsTab** - Tools configuration tab for chat settings
*
* Displays available tools grouped by source (built-in, MCP, custom) with
* Displays available tools grouped by source (server, browser, MCP, custom) with
* toggles to enable/disable individual tools and tool groups. Shows MCP
* server favicons and permission management controls.
*/
+3 -1
View File
@@ -2,7 +2,9 @@ import { CLI_FLAGS } from './cli-flags.constants';
import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const BROWSER_INFO_TOOL_NAME = BuiltInTool.GET_INFO;
// get_info is served by the server, but the browser falls back to this
// implementation when the server does not provide it - same wire name.
export const BROWSER_INFO_TOOL_NAME = BuiltInTool.SERVER_GET_INFO;
/** UA token to OS name, first match wins - Android and iOS UAs also carry the Linux / Mac OS X tokens */
export const BROWSER_INFO_OS_UA_PATTERNS: readonly [RegExp, string][] = [
@@ -1,52 +0,0 @@
// Registry of built-in and frontend (browser) tools whose renderer
// shows a recognizable icon and friendly label inline in the chat UI.
//
// To add a new built-in tool, add an entry to BUILTIN_TOOL_UI. To give a
// tool a custom title or body renderer, add a dedicated component under
// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
// (see ChatMessageToolCallBlockGetDatetime and
// ChatMessageToolCallBlockSearchResults for prior art).
import {
Braces,
Clock,
Eye,
FilePen,
FilePlus,
FileSearch,
FileText,
Info,
SearchCode,
Terminal
} from '@lucide/svelte';
import { BuiltInTool, ToolSource } from '$lib/enums';
import type { BuiltinToolUiEntry } from '$lib/types';
export const BUILTIN_TOOL_UI: Readonly<Record<BuiltInTool, BuiltinToolUiEntry>> = {
[BuiltInTool.EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.BUILTIN },
[BuiltInTool.EXEC_SHELL_COMMAND]: {
icon: Terminal,
label: 'Run command',
source: ToolSource.BUILTIN
},
[BuiltInTool.FILE_GLOB_SEARCH]: {
icon: FileSearch,
label: 'Search files',
source: ToolSource.BUILTIN
},
[BuiltInTool.GET_DATETIME]: { icon: Clock, label: 'Current time', source: ToolSource.FRONTEND },
[BuiltInTool.GET_INFO]: { icon: Info, label: 'Runtime info', source: ToolSource.BUILTIN },
[BuiltInTool.GREP_SEARCH]: {
icon: SearchCode,
label: 'Search in files',
source: ToolSource.BUILTIN
},
[BuiltInTool.READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.BUILTIN },
[BuiltInTool.READ_MEDIA]: { icon: Eye, label: 'Read media', source: ToolSource.FRONTEND },
[BuiltInTool.RUN_JAVASCRIPT]: {
icon: Braces,
label: 'Run JavaScript',
source: ToolSource.FRONTEND
},
[BuiltInTool.WRITE_FILE]: { icon: FilePlus, label: 'Write file', source: ToolSource.BUILTIN }
} as const;
+1 -1
View File
@@ -1,7 +1,7 @@
import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const GET_DATETIME_TOOL_NAME = BuiltInTool.GET_DATETIME;
export const GET_DATETIME_TOOL_NAME = BuiltInTool.BROWSER_GET_DATETIME;
export function buildGetDatetimeToolDefinition(): OpenAIToolDefinition {
return {
+1 -1
View File
@@ -15,7 +15,7 @@ export * from './context-gauge-popup.constants';
export * from './conversation-import.constants';
export * from './binary-detection.constants';
export * from './content-detection.constants';
export * from './built-in-tools.constants';
export * from './tool-ui.constants';
export * from './cache.constants';
export * from './chat-form.constants';
export * from './cli-flags.constants';
+1 -1
View File
@@ -7,7 +7,7 @@ import {
} from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
export const READ_MEDIA_TOOL_NAME = BuiltInTool.READ_MEDIA;
export const READ_MEDIA_TOOL_NAME = BuiltInTool.BROWSER_READ_MEDIA;
// header lines of the tool result, parsed back by the read_media renderer
export const PREFIX_FILE = 'File: ';
@@ -1,6 +1,6 @@
import { BuiltInTool } from '$lib/enums';
export const SANDBOX_TOOL_NAME = BuiltInTool.RUN_JAVASCRIPT;
export const SANDBOX_TOOL_NAME = BuiltInTool.BROWSER_RUN_JAVASCRIPT;
export const SANDBOX_TIMEOUT_MS_DEFAULT = 10000;
@@ -0,0 +1,60 @@
// Registry of server and browser tools whose renderer
// shows a recognizable icon and friendly label inline in the chat UI.
//
// To add a new tool, add an entry to TOOL_UI. To give a
// tool a custom title or body renderer, add a dedicated component under
// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
// (see ChatMessageToolCallBlockGetDatetime and
// ChatMessageToolCallBlockSearchResults for prior art).
import {
Braces,
Clock,
Eye,
FilePen,
FilePlus,
FileSearch,
FileText,
Info,
SearchCode,
Terminal
} from '@lucide/svelte';
import { BuiltInTool, ToolSource } from '$lib/enums';
import type { ToolUiEntry } from '$lib/types';
export const TOOL_UI: Readonly<Record<BuiltInTool, ToolUiEntry>> = {
[BuiltInTool.BROWSER_GET_DATETIME]: {
icon: Clock,
label: 'Current time',
source: ToolSource.BROWSER
},
[BuiltInTool.BROWSER_READ_MEDIA]: { icon: Eye, label: 'Read media', source: ToolSource.BROWSER },
[BuiltInTool.BROWSER_RUN_JAVASCRIPT]: {
icon: Braces,
label: 'Run JavaScript',
source: ToolSource.BROWSER
},
[BuiltInTool.SERVER_EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.SERVER },
[BuiltInTool.SERVER_EXEC_SHELL_COMMAND]: {
icon: Terminal,
label: 'Run command',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_FILE_GLOB_SEARCH]: {
icon: FileSearch,
label: 'Search files',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_GET_INFO]: { icon: Info, label: 'Runtime info', source: ToolSource.SERVER },
[BuiltInTool.SERVER_GREP_SEARCH]: {
icon: SearchCode,
label: 'Search in files',
source: ToolSource.SERVER
},
[BuiltInTool.SERVER_READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.SERVER },
[BuiltInTool.SERVER_WRITE_FILE]: {
icon: FilePlus,
label: 'Write file',
source: ToolSource.SERVER
}
} as const;
+4 -4
View File
@@ -18,15 +18,15 @@ export const UI_DATA_ATTRS = {
} as const;
export const TOOL_GROUP_LABELS = {
[ToolSource.BUILTIN]: 'Built-in',
[ToolSource.BROWSER]: 'Browser',
[ToolSource.CUSTOM]: 'JSON Schema',
[ToolSource.FRONTEND]: 'Browser'
[ToolSource.SERVER]: 'Server'
} as const;
export const TOOL_SERVER_LABELS = {
[ToolSource.BUILTIN]: 'Built-in Tools',
[ToolSource.BROWSER]: 'Browser Tools',
[ToolSource.CUSTOM]: 'Custom Tools',
[ToolSource.FRONTEND]: 'Browser Tools'
[ToolSource.SERVER]: 'Server Tools'
} as const;
export const TOOLTIP_DELAY_DURATION = 500;
+24 -18
View File
@@ -1,8 +1,8 @@
export enum ToolSource {
BUILTIN = 'builtin',
MCP = 'mcp',
BROWSER = 'browser',
CUSTOM = 'custom',
FRONTEND = 'frontend'
MCP = 'mcp',
SERVER = 'server'
}
export enum ToolPermissionDecision {
@@ -28,22 +28,28 @@ export enum GlobSearchType {
}
/**
* Wire-format identifiers for built-in and frontend tools. The string
* Wire-format identifiers for server and browser tools. The string
* value matches what the model emits in tool call names, so comparing
* against `BuiltInTool.READ_FILE` is equivalent to comparing against the
* raw `'read_file'` literal - the enum just keeps the two in lock-step
* and gives TypeScript a single source of truth for autocomplete / rename
* support.
* against `BuiltInTool.SERVER_READ_FILE` is equivalent to comparing
* against the raw `'read_file'` literal - the enum just keeps the two in
* lock-step and gives TypeScript a single source of truth for autocomplete
* / rename support.
*
* The `SERVER_` / `BROWSER_` prefixes mirror the tool's primary source
* (llama-server vs llama-ui). `get_info` is the exception: it is served by
* the server, but llama-ui falls back to a browser implementation when the
* server does not provide it, so it can surface under both categories in
* the UI while keeping a single wire name.
*/
export enum BuiltInTool {
READ_FILE = 'read_file',
READ_MEDIA = 'read_media',
EDIT_FILE = 'edit_file',
WRITE_FILE = 'write_file',
GET_DATETIME = 'get_datetime',
GET_INFO = 'get_info',
FILE_GLOB_SEARCH = 'file_glob_search',
GREP_SEARCH = 'grep_search',
EXEC_SHELL_COMMAND = 'exec_shell_command',
RUN_JAVASCRIPT = 'run_javascript'
BROWSER_GET_DATETIME = 'get_datetime',
BROWSER_READ_MEDIA = 'read_media',
BROWSER_RUN_JAVASCRIPT = 'run_javascript',
SERVER_EDIT_FILE = 'edit_file',
SERVER_EXEC_SHELL_COMMAND = 'exec_shell_command',
SERVER_FILE_GLOB_SEARCH = 'file_glob_search',
SERVER_GET_INFO = 'get_info',
SERVER_GREP_SEARCH = 'grep_search',
SERVER_READ_FILE = 'read_file',
SERVER_WRITE_FILE = 'write_file'
}
@@ -46,13 +46,13 @@ export function useToolsPanel(): UseToolsPanelReturn {
// Tools endpoint is unreachable (404) — server started without --tools
if (toolsStore.isToolsEndpointUnreachable) {
return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
return `To enable Server Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
}
// Other errors — return null so UI shows "Failed to load tools"
if (toolsStore.error) return null;
return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
return `To enable Server Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
});
function isGroupChecked(group: ToolGroup): boolean {
@@ -95,8 +95,8 @@ export function useToolsPanel(): UseToolsPanelReturn {
}
function handleOpen(): void {
if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
toolsStore.fetchBuiltinTools();
if (toolsStore.serverTools.length === 0 && !toolsStore.loading) {
toolsStore.fetchServerTools();
}
mcpStore.runHealthChecksForServers(mcpStore.getServers().filter((s) => s.enabled));
+3 -3
View File
@@ -262,9 +262,9 @@ export { ParameterSyncService } from './parameter-sync.service';
export { MCPService } from './mcp.service';
/**
* **SandboxService** - Frontend JavaScript execution in a browser sandbox
* **SandboxService** - Browser JavaScript execution in a browser sandbox
*
* Stateless executor for the run_javascript frontend tool. Model generated
* Stateless executor for the run_javascript browser tool. Model generated
* code runs in a Web Worker spawned inside a sandboxed iframe with an opaque
* origin: no access to the app origin, its storage or its API, and outgoing
* requests carry a null origin. The code never touches a main thread, so the
@@ -274,7 +274,7 @@ export { MCPService } from './mcp.service';
* **Architecture & Relationships:**
* - **SandboxService** (this class): Stateless sandbox execution
* - **toolsStore**: Exposes the tool definition when the sandbox is enabled
* - **agenticStore**: Dispatches ToolSource.FRONTEND calls here
* - **agenticStore**: Dispatches ToolSource.BROWSER calls here
*
* @see buildSandboxToolDefinition in utils/sandbox-tool - tool schema sent to the LLM
* @see agenticStore in stores/agentic.svelte.ts - tool dispatch
@@ -28,15 +28,15 @@ function fileExtension(path: string): string {
}
/**
* **ReadMediaService** - frontend executor for the `read_media` tool
* **ReadMediaService** - browser executor for the `read_media` tool
*
* The tool is synthetic: no such tool exists on the server. It reads the file
* through the built-in `read_file` tool with the `base64` response type, then
* through the server `read_file` tool with the `base64` response type, then
* turns the bytes into a data URI line. The agentic store lifts that line into
* an image or audio attachment on the tool result message, which is what makes
* the model perceive the file instead of reading a wall of base64.
*
* Living in the frontend is what lets it exist only for models that can
* Living in the browser is what lets it exist only for models that can
* actually use the result - the server has no idea which model is selected.
*
* @see buildReadMediaToolDefinition in constants/read-media.ts - tool schema sent to the LLM
@@ -82,7 +82,7 @@ export class ReadMediaService {
}
const raw = await ToolsService.executeToolRaw(
BuiltInTool.READ_FILE,
BuiltInTool.SERVER_READ_FILE,
{ path },
signal,
cwd,
+2 -2
View File
@@ -68,7 +68,7 @@ function formatReply(reply: SandboxReply): ToolExecutionResult {
export class SandboxService {
/**
* Execute a frontend sandbox tool call and return its output.
* Execute a browser sandbox tool call and return its output.
* One disposable iframe per execution, removed on completion,
* timeout or abort. Removing the iframe terminates the worker
* at the browser level, so runaway code cannot outlive it.
@@ -79,7 +79,7 @@ export class SandboxService {
signal?: AbortSignal
): Promise<ToolExecutionResult> {
if (toolName !== SANDBOX_TOOL_NAME) {
return { content: `Unknown frontend tool: ${toolName}`, isError: true };
return { content: `Unknown browser tool: ${toolName}`, isError: true };
}
const code = typeof params.code === 'string' ? params.code : '';
+7 -7
View File
@@ -1,23 +1,23 @@
import { base } from '$app/paths';
import { API_TOOLS, HEADERS } from '$lib/constants';
import { ToolResponseField } from '$lib/enums';
import type { ServerBuiltinToolInfo, ToolExecutionResult } from '$lib/types';
import type { ServerToolInfo, ToolExecutionResult } from '$lib/types';
import { apiFetch } from '$lib/utils';
import { getJsonHeaders } from '$lib/utils/api-headers';
import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
export class ToolsService {
/**
* Fetch the list of built-in tools from the server.
* Fetch the list of server tools from the server.
*
* @returns Array of tool definitions in OpenAI-compatible format
*/
static async list(): Promise<ServerBuiltinToolInfo[]> {
return apiFetch<ServerBuiltinToolInfo[]>(API_TOOLS.LIST);
static async list(): Promise<ServerToolInfo[]> {
return apiFetch<ServerToolInfo[]>(API_TOOLS.LIST);
}
/**
* Execute a built-in tool on the server.
* Execute a server tool on the server.
*
* @param cwd - Working directory for the tool call, sent as the
* x-tool-cwd request header. The server resolves relative paths
@@ -48,7 +48,7 @@ export class ToolsService {
}
/**
* Execute a built-in tool and return the raw JSON response. Unlike
* Execute a server tool and return the raw JSON response. Unlike
* executeTool, this preserves structured fields (e.g. file_glob_search's
* `entries` and `base`) that the flattened ToolExecutionResult drops.
*
@@ -77,7 +77,7 @@ export class ToolsService {
}
/**
* Stream a built-in tool's output chunks from the server. The server
* Stream a server tool's output chunks from the server. The server
* `POST /tools` endpoint with `{stream: true}` emits `data: {"chunk": "..."}`
* events followed by a terminal `data: {"done": true}` (optionally with
* `error`). Yields the chunk string for each partial event.
+12 -12
View File
@@ -326,8 +326,8 @@ class AgenticStore {
const maxTurns = Number(settings.agenticMaxTurns) || DEFAULT_AGENTIC_CONFIG.maxTurns;
const hasTools =
mcpStore.hasEnabledServers(perChatOverrides) ||
toolsStore.builtinTools.length > 0 ||
toolsStore.frontendTools.length > 0 ||
toolsStore.serverTools.length > 0 ||
toolsStore.browserTools.length > 0 ||
toolsStore.customTools.length > 0;
return {
@@ -455,9 +455,9 @@ class AgenticStore {
this._continueResolvers.delete(conversationId);
this._steeringMessages.delete(conversationId);
// Ensure built-in tools are fetched before checking if agentic is enabled
if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
await toolsStore.fetchBuiltinTools();
// Ensure server tools are fetched before checking if agentic is enabled
if (toolsStore.serverTools.length === 0 && !toolsStore.loading) {
await toolsStore.fetchServerTools();
}
const agenticConfig = this.getConfig(settingsStore.config, perChatOverrides);
@@ -906,8 +906,8 @@ class AgenticStore {
} else {
try {
if (
toolSource === ToolSource.BUILTIN &&
toolName === BuiltInTool.EXEC_SHELL_COMMAND &&
toolSource === ToolSource.SERVER &&
toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND &&
createToolResultMessage &&
updateToolResultMessage
) {
@@ -938,7 +938,7 @@ class AgenticStore {
}
}
result = accumulated;
} else if (toolSource === ToolSource.BUILTIN) {
} else if (toolSource === ToolSource.SERVER) {
const args = this.parseToolArguments(toolCall.function.arguments);
const cwd = conversationsStore.activeConversation?.cwd;
const executionResult = await ToolsService.executeTool(toolName, args, signal, cwd);
@@ -946,16 +946,16 @@ class AgenticStore {
result = executionResult.content;
if (executionResult.isError) toolSuccess = false;
} else if (toolSource === ToolSource.FRONTEND) {
} else if (toolSource === ToolSource.BROWSER) {
const args = this.parseToolArguments(toolCall.function.arguments);
let executionResult: ToolExecutionResult;
if (toolName === BuiltInTool.GET_DATETIME) {
if (toolName === BuiltInTool.BROWSER_GET_DATETIME) {
executionResult = executeGetDatetimeTool();
} else if (toolName === BuiltInTool.GET_INFO) {
} else if (toolName === BuiltInTool.SERVER_GET_INFO) {
executionResult = executeBrowserInfoTool();
} else if (toolName === BuiltInTool.READ_MEDIA) {
} else if (toolName === BuiltInTool.BROWSER_READ_MEDIA) {
executionResult = await ReadMediaService.executeTool(
args,
{
+37 -37
View File
@@ -28,11 +28,11 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
/** Stable selection identity for a tool, shared by the disabled set and the permission store */
class ToolsStore {
private _builtinTools = $state<OpenAIToolDefinition[]>([]);
private _serverTools = $state<OpenAIToolDefinition[]>([]);
private _loading = $state(false);
private _error = $state<string | null>(null);
private _disabledTools = $state(new SvelteSet<string>());
// builtin tools that resolve their paths against the working directory,
// server tools that resolve their paths against the working directory,
// as declared by the server in its `/tools` listing
private _cwdAwareTools = $state(new SvelteSet<string>());
private _toolsEndpointUnreachable = $state(false);
@@ -58,7 +58,7 @@ class ToolsStore {
console.error('[ToolsStore] Failed to load disabled tools from localStorage:', err);
}
this.fetchBuiltinTools();
this.fetchServerTools();
}
private persistDisabledTools(): void {
@@ -78,10 +78,10 @@ class ToolsStore {
return serverId ? `mcp-${serverId}:${name}` : `mcp:${name}`;
case ToolSource.CUSTOM:
return `custom:${name}`;
case ToolSource.FRONTEND:
return `frontend:${name}`;
case ToolSource.BROWSER:
return `browser:${name}`;
default:
return `builtin:${name}`;
return `server:${name}`;
}
}
@@ -164,8 +164,8 @@ class ToolsStore {
};
}
get builtinTools(): OpenAIToolDefinition[] {
return this._builtinTools;
get serverTools(): OpenAIToolDefinition[] {
return this._serverTools;
}
get serverHome(): string | null {
@@ -176,7 +176,7 @@ class ToolsStore {
return this.mcpEntries().map((e) => e.definition);
}
get frontendTools(): OpenAIToolDefinition[] {
get browserTools(): OpenAIToolDefinition[] {
const tools: OpenAIToolDefinition[] = [buildGetDatetimeToolDefinition()];
if (settingsStore.config.jsSandboxEnabled) {
@@ -188,25 +188,25 @@ class ToolsStore {
if (readMedia) tools.push(readMedia);
// provide browser's get_info tool if server doesn't provide one
if (!this.hasBuiltinTool(BuiltInTool.GET_INFO)) {
if (!this.hasServerTool(BuiltInTool.SERVER_GET_INFO)) {
tools.push(buildBrowserInfoToolDefinition());
}
return tools;
}
private hasBuiltinTool(name: BuiltInTool): boolean {
return this._builtinTools.some((def) => def.function.name === name);
private hasServerTool(name: BuiltInTool): boolean {
return this._serverTools.some((def) => def.function.name === name);
}
/**
* `read_media` runs in the frontend on top of the server's `read_file`, so it
* `read_media` runs in the browser on top of the server's `read_file`, so it
* exists only when that tool is served and the active model can perceive the
* bytes. The server cannot make this call - it does not know which model the
* conversation uses.
*/
private readMediaTool(): OpenAIToolDefinition | null {
if (!this.hasBuiltinTool(BuiltInTool.READ_FILE)) return null;
if (!this.hasServerTool(BuiltInTool.SERVER_READ_FILE)) return null;
const model = modelsStore.selectedModelName ?? modelsStore.models[0]?.model ?? '';
@@ -304,23 +304,23 @@ class ToolsStore {
entries.push(entry);
};
for (const def of this._builtinTools) {
for (const def of this._serverTools) {
const name = def.function.name;
push({
definition: def,
key: this.toolKey(ToolSource.BUILTIN, name),
source: ToolSource.BUILTIN
key: this.toolKey(ToolSource.SERVER, name),
source: ToolSource.SERVER
});
}
for (const def of this.frontendTools) {
for (const def of this.browserTools) {
const name = def.function.name;
push({
definition: def,
key: this.toolKey(ToolSource.FRONTEND, name),
source: ToolSource.FRONTEND
key: this.toolKey(ToolSource.BROWSER, name),
source: ToolSource.BROWSER
});
}
@@ -384,17 +384,17 @@ class ToolsStore {
return entry.serverName ?? '';
case ToolSource.CUSTOM:
return TOOL_GROUP_LABELS[ToolSource.CUSTOM];
case ToolSource.FRONTEND:
return TOOL_GROUP_LABELS[ToolSource.FRONTEND];
case ToolSource.BROWSER:
return TOOL_GROUP_LABELS[ToolSource.BROWSER];
default:
return TOOL_GROUP_LABELS[ToolSource.BUILTIN];
return TOOL_GROUP_LABELS[ToolSource.SERVER];
}
}
/**
* Enabled tool definitions for sending to the LLM.
* MCP tool schemas are normalized here so the wire payload is consistent
* across all four sources (built-in, frontend/sandbox, MCP, custom JSON).
* across all four sources (server, browser/sandbox, MCP, custom JSON).
* The API identifies tools by name, so a name is sent at most once.
*/
getEnabledToolsForLLM(): OpenAIToolDefinition[] {
@@ -417,8 +417,8 @@ class ToolsStore {
result.push(def);
};
for (const def of this._builtinTools) take(def);
for (const def of this.frontendTools) take(def);
for (const def of this._serverTools) take(def);
for (const def of this.browserTools) take(def);
// mcpEntries() over mcpStore directly so wire shape stays normalized and aligned with the tools UI.
for (const entry of this.mcpEntries()) take(entry.definition);
for (const def of this.customTools) take(def);
@@ -542,11 +542,11 @@ class ToolsStore {
if (entry.serverName) return mcpStore.getServerDisplayName(entry.serverName);
if (entry.source === ToolSource.BUILTIN) return TOOL_SERVER_LABELS[ToolSource.BUILTIN];
if (entry.source === ToolSource.SERVER) return TOOL_SERVER_LABELS[ToolSource.SERVER];
if (entry.source === ToolSource.CUSTOM) return TOOL_SERVER_LABELS[ToolSource.CUSTOM];
if (entry.source === ToolSource.FRONTEND) return TOOL_SERVER_LABELS[ToolSource.FRONTEND];
if (entry.source === ToolSource.BROWSER) return TOOL_SERVER_LABELS[ToolSource.BROWSER];
return '';
}
@@ -556,27 +556,27 @@ class ToolsStore {
return this.findEntryByName(toolName)?.key ?? null;
}
/** Check if there are any enabled tools available (builtin, MCP, or custom) */
/** Check if there are any enabled tools available (server, MCP, or custom) */
get hasEnabledTools(): boolean {
return this.getEnabledToolsForLLM().length > 0;
}
/**
* Check if a working directory is worth setting: at least one builtin tool
* Check if a working directory is worth setting: at least one server tool
* that reads it is both served and left enabled by the user.
*/
get hasEnabledCwdTools(): boolean {
return this._builtinTools.some((def) => {
return this._serverTools.some((def) => {
const name = def.function.name;
return (
this._cwdAwareTools.has(name) &&
!this._disabledTools.has(this.toolKey(ToolSource.BUILTIN, name))
!this._disabledTools.has(this.toolKey(ToolSource.SERVER, name))
);
});
}
async fetchBuiltinTools(): Promise<void> {
async fetchServerTools(): Promise<void> {
if (this._loading) return;
this._loading = true;
@@ -586,7 +586,7 @@ class ToolsStore {
try {
const toolInfos = await ToolsService.list();
this._builtinTools = toolInfos.map((info) => info.definition);
this._serverTools = toolInfos.map((info) => info.definition);
this._cwdAwareTools = new SvelteSet(
toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool)
);
@@ -599,9 +599,9 @@ class ToolsStore {
// TODO: check status code instead of relying on message
if (errorMessage.includes('this feature is disabled')) {
this._toolsEndpointUnreachable = true;
console.info('[ToolsStore] Built-in tools are disabled on the server');
console.info('[ToolsStore] Server tools are disabled on the server');
} else {
console.error('[ToolsStore] Failed to fetch built-in tools:', err);
console.error('[ToolsStore] Failed to fetch server tools:', err);
}
} finally {
this._loading = false;
@@ -618,7 +618,7 @@ class ToolsStore {
if (this._serverHome !== undefined) return this._serverHome;
try {
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
const res = await ToolsService.executeToolRaw(BuiltInTool.SERVER_FILE_GLOB_SEARCH, {
limit: 1,
max_depth: 1,
path: HOME_TILDE,
+2 -2
View File
@@ -147,7 +147,7 @@ export type {
ServerStatus,
ToolCallParams,
ToolExecutionResult,
ServerBuiltinToolInfo,
ServerToolInfo,
Tool,
Prompt,
GetPromptResult,
@@ -208,7 +208,7 @@ export type {
export type { DesktopIconStripItem } from './navigation';
// Tools types
export type { ToolEntry, ToolGroup, BuiltinToolUiEntry } from './tools';
export type { ToolEntry, ToolGroup, ToolUiEntry } from './tools';
// Reasoning
export type { ReasoningEffortLevel } from './reasoning';
+2 -2
View File
@@ -285,10 +285,10 @@ export interface ToolExecutionResult {
isError: boolean;
}
export interface ServerBuiltinToolInfo {
export interface ServerToolInfo {
display_name: string;
tool: string;
type: ToolSource.BUILTIN;
type: ToolSource.SERVER;
permissions: {
write: boolean;
};
+4 -4
View File
@@ -3,12 +3,12 @@ import type { ToolSource } from '$lib/enums';
import type { Component } from 'svelte';
/**
* UI metadata for a built-in or frontend tool, keyed by its `BuiltInTool` id.
* UI metadata for a server or browser tool, keyed by its `BuiltInTool` id.
*/
export interface BuiltinToolUiEntry {
export interface ToolUiEntry {
icon: Component;
label: string;
source: ToolSource.BUILTIN | ToolSource.FRONTEND;
source: ToolSource.SERVER | ToolSource.BROWSER;
}
export interface ToolEntry {
@@ -17,7 +17,7 @@ export interface ToolEntry {
serverName?: string;
/** For MCP tools, the server ID (used for permission keys) */
serverId?: string;
/** Stable selection identity: builtin:name, mcp-<serverId>:name, mcp:name, custom:name */
/** Stable selection identity: server:name, mcp-<serverId>:name, mcp:name, custom:name */
key: string;
definition: OpenAIToolDefinition;
}
-13
View File
@@ -1,13 +0,0 @@
import { BUILTIN_TOOL_UI } from '$lib/constants';
import type { BuiltinToolUiEntry } from '$lib/types';
/**
* Resolve the UI metadata (label + icon) for a built-in tool by its name.
* Falls back to null for unknown or non-built-in tools so callers can render
* a generic chrome instead.
*/
export function getBuiltinToolUi(toolName: string | undefined): BuiltinToolUiEntry | null {
if (!toolName) return null;
return (BUILTIN_TOOL_UI as Record<string, BuiltinToolUiEntry>)[toolName] ?? null;
}
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* Frontend executor for the `get_datetime` tool. It runs in the browser, so it
* Browser executor for the `get_datetime` tool. It runs in the browser, so it
* reports the user's own clock and time zone instead of the server's UTC time -
* a chat about "tomorrow" means the user's tomorrow, not the host's.
*
+1 -1
View File
@@ -42,7 +42,7 @@ export async function runGlobSearch(
}
const res = await ToolsService.executeToolRaw(
BuiltInTool.FILE_GLOB_SEARCH,
BuiltInTool.SERVER_FILE_GLOB_SEARCH,
{ include: args.include, limit, max_depth: args.maxDepth, path: args.path, type },
signal
);
+4 -4
View File
@@ -127,7 +127,7 @@ export { sanitizeKeyValuePairKey, sanitizeKeyValuePairValue } from './sanitize';
// Image error fallback utilities
export { getImageErrorFallbackHtml } from './image-error-fallback';
// SSE-with-JSON stream iterator (used by built-in tool streaming, decoupled
// SSE-with-JSON stream iterator (used by server tool streaming, decoupled
// from chat.service.ts which embeds its own SSE parser for resume support)
export { parseSseJsonStream } from './sse';
@@ -310,7 +310,7 @@ export {
withAbortSignal
} from './abort';
// Tool-call meta utilities. Parsers for each built-in tool live next to
// Tool-call meta utilities. Parsers for each server tool live next to
// their renderer family under
// `src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/`.
// This module only carries the helpers that genuinely cross tool
@@ -321,7 +321,7 @@ export { tryParseToolResultObject } from './tool-call-meta';
// Per-tool UI metadata (label + icon) used by the tool-call chrome.
// Re-exported through $lib/utils so renderer components can read the
// label without depending on $lib/constants directly.
export { getBuiltinToolUi } from './built-in-tools';
export { getToolUi } from './tool-ui';
// Chat command picker
@@ -331,7 +331,7 @@ export { getChatCommands } from './chat-commands';
// SANDBOX_TOOL_DEFINITION is deprecated; kept for backward compatibility.
export { buildSandboxToolDefinition, SANDBOX_TOOL_DEFINITION } from './sandbox-tool';
// Frontend `get_datetime` executor (the browser clock, not the server's)
// Browser `get_datetime` executor (the browser clock, not the server's)
export { executeGetDatetimeTool } from './get-datetime';
// Browser fallback for the server's get_info tool
+13
View File
@@ -0,0 +1,13 @@
import { TOOL_UI } from '$lib/constants';
import type { ToolUiEntry } from '$lib/types';
/**
* Resolve the UI metadata (label + icon) for a server or browser tool by its
* name. Falls back to null for unknown tools so callers can render a generic
* chrome instead.
*/
export function getToolUi(toolName: string | undefined): ToolUiEntry | null {
if (!toolName) return null;
return (TOOL_UI as Record<string, ToolUiEntry>)[toolName] ?? null;
}