ui: Constants refactor (#26908)

* refactor: Constants

* refactor: Constants/Enums cleanup

* refactor: Constant objects instead of multiple single value constants

* refactor: Cleanup constants
This commit is contained in:
Aleksander Grygier
2026-08-13 06:53:56 +02:00
committed by GitHub
parent 8e7f22b67e
commit e21152dc96
209 changed files with 1101 additions and 1137 deletions
+13 -21
View File
@@ -1,17 +1,9 @@
import {
ATTACHMENT_SAVED_REGEX,
MARKDOWN_ATX_HEADING_REGEX,
MARKDOWN_BLOCKQUOTE_REGEX,
MARKDOWN_BOLD_REGEX,
MARKDOWN_CODE_FENCE_REGEX,
MARKDOWN_LINK_REGEX,
MARKDOWN_LIST_BULLET_REGEX,
MARKDOWN_LIST_NUMBERED_REGEX,
MARKDOWN_TABLE_SEPARATOR_REGEX,
MARKDOWN,
NEWLINE,
REASONING_TAGS,
SEARCH_SUMMARY_SEPARATOR,
SEARCH_SUMMARY_TOTAL_REGEX,
SEARCH_SUMMARY,
TOOL_RESULT_JSON_OPEN_REGEX
} from '$lib/constants';
import {
@@ -283,11 +275,11 @@ export function splitSearchSummaryList(
text: string,
captureTotal: (n: number) => void
): { lines: string[] } {
const separatorIndex = text.indexOf(SEARCH_SUMMARY_SEPARATOR);
const separatorIndex = text.indexOf(SEARCH_SUMMARY.SEPARATOR);
const matchesText = separatorIndex === -1 ? text : text.slice(0, separatorIndex);
const summaryText =
separatorIndex === -1 ? '' : text.slice(separatorIndex + SEARCH_SUMMARY_SEPARATOR.length);
const totalMatch = summaryText.match(SEARCH_SUMMARY_TOTAL_REGEX);
separatorIndex === -1 ? '' : text.slice(separatorIndex + SEARCH_SUMMARY.SEPARATOR.length);
const totalMatch = summaryText.match(SEARCH_SUMMARY.TOTAL_REGEX);
if (totalMatch) {
captureTotal(parseInt(totalMatch[1], 10));
@@ -412,31 +404,31 @@ export function classifyToolResult(content: string | undefined): ToolResultKind
*/
function looksLikeMarkdown(content: string): boolean {
// Code fences are unambiguous - triple backticks or tildes at line start.
if (MARKDOWN_CODE_FENCE_REGEX.test(content)) return true;
if (MARKDOWN.CODE_FENCE_REGEX.test(content)) return true;
const lines = content.split(NEWLINE);
for (const line of lines) {
if (MARKDOWN_ATX_HEADING_REGEX.test(line)) return true;
if (MARKDOWN.ATX_HEADING_REGEX.test(line)) return true;
if (MARKDOWN_BLOCKQUOTE_REGEX.test(line)) return true;
if (MARKDOWN.BLOCKQUOTE_REGEX.test(line)) return true;
if (MARKDOWN_LIST_BULLET_REGEX.test(line)) return true;
if (MARKDOWN.LIST_BULLET_REGEX.test(line)) return true;
if (MARKDOWN_LIST_NUMBERED_REGEX.test(line)) return true;
if (MARKDOWN.LIST_NUMBERED_REGEX.test(line)) return true;
}
// Inline structural markers anywhere in the body.
if (MARKDOWN_LINK_REGEX.test(content)) return true;
if (MARKDOWN.LINK_REGEX.test(content)) return true;
if (MARKDOWN_BOLD_REGEX.test(content)) return true;
if (MARKDOWN.BOLD_REGEX.test(content)) return true;
// Tables: a pipe-bearing header line followed by a separator row.
if (lines.length >= 2) {
const head = lines[0];
const sep = lines[1];
if (head.includes('|') && MARKDOWN_TABLE_SEPARATOR_REGEX.test(sep)) return true;
if (head.includes('|') && MARKDOWN.TABLE_SEPARATOR_REGEX.test(sep)) return true;
}
return false;
+1 -1
View File
@@ -1,6 +1,6 @@
import { getAuthHeaders, getJsonHeaders } from './api-headers';
import { base } from '$app/paths';
import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants/error';
import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';
import { UrlProtocol } from '$lib/enums';
/**
+8 -14
View File
@@ -1,11 +1,5 @@
import { redactValue } from './redact';
import {
AUTHORIZATION_HEADER,
BEARER_PREFIX,
CONTENT_TYPE_HEADER,
CORS_PROXY_HEADER_PREFIX,
REDACTED_HEADERS
} from '$lib/constants';
import { CORS_PROXY, HEADERS } from '$lib/constants';
import { MimeTypeApplication } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
@@ -17,7 +11,7 @@ export function getAuthHeaders(): Record<string, string> {
const currentConfig = config();
const apiKey = currentConfig.apiKey?.toString().trim();
return apiKey ? { [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey}` } : {};
return apiKey ? { [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey}` } : {};
}
/**
@@ -25,14 +19,14 @@ export function getAuthHeaders(): Record<string, string> {
*/
export function getJsonHeaders(): Record<string, string> {
return {
[CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON,
[HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON,
...getAuthHeaders()
};
}
/**
* Sanitize HTTP headers by redacting sensitive values.
* Known sensitive headers (from REDACTED_HEADERS) and any extra headers
* Known sensitive headers (from HEADERS.REDACTED) and any extra headers
* specified by the caller are fully redacted. Headers listed in
* `partialRedactHeaders` are partially redacted, showing only the
* specified number of trailing characters.
@@ -59,8 +53,8 @@ export function sanitizeHeaders(
for (const [key, value] of normalized.entries()) {
const normalizedKey = key.toLowerCase();
const unproxiedKey = normalizedKey.startsWith(CORS_PROXY_HEADER_PREFIX)
? normalizedKey.slice(CORS_PROXY_HEADER_PREFIX.length)
const unproxiedKey = normalizedKey.startsWith(CORS_PROXY.HEADER_PREFIX)
? normalizedKey.slice(CORS_PROXY.HEADER_PREFIX.length)
: normalizedKey;
const partialChars =
partialRedactHeaders?.get(normalizedKey) ?? partialRedactHeaders?.get(unproxiedKey);
@@ -68,8 +62,8 @@ export function sanitizeHeaders(
if (partialChars !== undefined) {
sanitized[key] = redactValue(value, partialChars);
} else if (
REDACTED_HEADERS.has(normalizedKey) ||
REDACTED_HEADERS.has(unproxiedKey) ||
HEADERS.REDACTED.has(normalizedKey) ||
HEADERS.REDACTED.has(unproxiedKey) ||
redactedHeaders.has(normalizedKey) ||
redactedHeaders.has(unproxiedKey)
) {
+3 -3
View File
@@ -1,7 +1,7 @@
import { error } from '@sveltejs/kit';
import { browser } from '$app/environment';
import { base } from '$app/paths';
import { AUTHORIZATION_HEADER, BEARER_PREFIX, CONTENT_TYPE_HEADER } from '$lib/constants';
import { HEADERS } from '$lib/constants';
import { MimeTypeApplication } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
@@ -18,14 +18,14 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
try {
const headers: Record<string, string> = {
[CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON
[HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON
};
// Probe /props even without a stored key: on a server started with
// --api-key the unauthenticated request returns 401 and surfaces the
// API key splash, which is the onboarding path for entering the key.
if (apiKey) {
headers[AUTHORIZATION_HEADER] = `${BEARER_PREFIX}${apiKey}`;
headers[HEADERS.AUTHORIZATION] = `${HEADERS.BEARER}${apiKey}`;
}
const response = await fetch(`${base}/props`, { headers });
+13
View File
@@ -0,0 +1,13 @@
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;
}
+5 -5
View File
@@ -1,4 +1,4 @@
import { DEFAULT_CACHE_MAX_ENTRIES, DEFAULT_CACHE_TTL_MS } from '$lib/constants';
import { CACHE } from '$lib/constants';
/**
* TTL Cache - Time-To-Live cache implementation for memory optimization
@@ -36,8 +36,8 @@ export class TTLCache<K extends string, V> {
private readonly onEvict?: (key: string, value: unknown) => void;
constructor(options: TTLCacheOptions = {}) {
this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
this.onEvict = options.onEvict;
}
@@ -217,8 +217,8 @@ export class ReactiveTTLMap<K extends string, V> {
private readonly maxEntries: number;
constructor(options: TTLCacheOptions = {}) {
this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
}
get(key: K): V | null {
+2 -3
View File
@@ -1,6 +1,5 @@
import { getJpegOrientationFromDataURL, isJpegMimeType } from './jpeg-orientation';
import { MEGAPIXELS_TO_PIXELS } from '$lib/constants/image-size';
import { BASE64_IMAGE_URI_REGEX } from '$lib/constants/uri-template';
import { BASE64_IMAGE_URI_REGEX, IMAGE } from '$lib/constants';
import { MimeTypeImage } from '$lib/enums';
/**
@@ -51,7 +50,7 @@ export function capImageDataURLSize(
const targetWidth = img.naturalWidth;
const targetHeight = img.naturalHeight;
const totalPixels = targetWidth * targetHeight;
const maxPixels = Math.floor(maxMegapixels * MEGAPIXELS_TO_PIXELS);
const maxPixels = Math.floor(maxMegapixels * IMAGE.MEGAPIXELS_TO_PIXELS);
if (maxPixels > 0 && totalPixels > maxPixels) {
const scaleFactor = Math.sqrt(maxPixels / totalPixels);
+35
View File
@@ -0,0 +1,35 @@
import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
import { ChatFormCommandAction } from '$lib/enums';
import type { ChatCommandsOptions, ChatFormCommand } from '$lib/types';
/**
* The slash commands surfaced by the `/` command picker, in display order.
*
* Availability is supplied as predicates rather than store imports: this
* module is re-exported through the `$lib/utils` barrel, and importing
* stores at module load would create a circular dependency (the stores
* themselves import from `$lib/utils`).
*/
export function getChatCommands(options: ChatCommandsOptions): ChatFormCommand[] {
return [
{
action: ChatFormCommandAction.PROMPT,
description: 'Insert an MCP prompt',
disabled: !options.hasPrompts(),
name: 'prompt'
},
{
action: ChatFormCommandAction.CWD,
description: SET_WORKING_DIRECTORY_LABEL,
disabled: !options.hasCwdTools(),
keywords: ['current working directory'],
name: 'cwd'
},
{
action: ChatFormCommandAction.MODEL,
description: 'Select model',
disabled: !options.showModelSelector,
name: 'model'
}
];
}
+11 -16
View File
@@ -1,14 +1,4 @@
import {
AMPERSAND_REGEX,
DEFAULT_LANGUAGE,
FENCE_PATTERN,
GT_REGEX,
LANG_PATTERN,
LT_REGEX,
NEWLINE,
TRIM_LEADING_PADDING_REGEX,
TRIM_TRAILING_PADDING_REGEX
} from '$lib/constants';
import { CODE_BLOCK, NEWLINE } from '$lib/constants';
import hljs from 'highlight.js';
export interface IncompleteCodeBlock {
@@ -81,11 +71,16 @@ export function splitGluedClosingCodeFences(markdown: string): string {
* so internal blank lines are still rendered as such.
*/
function trimCodePadding(code: string): string {
return code.replace(TRIM_LEADING_PADDING_REGEX, '').replace(TRIM_TRAILING_PADDING_REGEX, '');
return code
.replace(CODE_BLOCK.TRIM_LEADING_PADDING_REGEX, '')
.replace(CODE_BLOCK.TRIM_TRAILING_PADDING_REGEX, '');
}
function escapeCode(code: string): string {
return code.replace(AMPERSAND_REGEX, '&amp;').replace(LT_REGEX, '&lt;').replace(GT_REGEX, '&gt;');
return code
.replace(CODE_BLOCK.AMPERSAND_REGEX, '&amp;')
.replace(CODE_BLOCK.LT_REGEX, '&lt;')
.replace(CODE_BLOCK.GT_REGEX, '&gt;');
}
/** Bounded cache for highlightCode results. */
@@ -152,7 +147,7 @@ export { trimCodePadding };
export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock | null {
// Count all code fences in the markdown
// A code block is incomplete if there's an odd number of ``` fences
const fencePattern = new RegExp(FENCE_PATTERN.source, FENCE_PATTERN.flags);
const fencePattern = new RegExp(CODE_BLOCK.FENCE_PATTERN.source, CODE_BLOCK.FENCE_PATTERN.flags);
const fences: number[] = [];
let fenceMatch;
@@ -174,8 +169,8 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock
const openingIndex = fences[fences.length - 1];
const afterOpening = markdown.slice(openingIndex + 3);
// Extract language and code content
const langMatch = afterOpening.match(LANG_PATTERN);
const language = langMatch?.[1] || DEFAULT_LANGUAGE;
const langMatch = afterOpening.match(CODE_BLOCK.LANG_PATTERN);
const language = langMatch?.[1] || CODE_BLOCK.DEFAULT_LANGUAGE;
const codeStartIndex = openingIndex + 3 + (langMatch?.[0]?.length ?? 0);
const code = markdown.slice(codeStartIndex);
@@ -520,7 +520,7 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
* tokens, `<span data-mention-badge="true">` elements for badges,
* `<code data-code-token>` elements for code spans. The badge's class
* string + inline SVG are shared with the rehype plugin via
* `$lib/constants/mention-badge`.
* `$lib/constants`.
*/
export function buildFragment(tokens: ContentToken[]): DocumentFragment {
const fragment = document.createDocumentFragment();
+3 -7
View File
@@ -3,11 +3,7 @@
*/
import { base } from '$app/paths';
import {
CORS_PROXY_ENDPOINT,
CORS_PROXY_HEADER_PREFIX,
CORS_PROXY_URL_PARAM
} from '$lib/constants';
import { CORS_PROXY, CORS_PROXY_ENDPOINT } from '$lib/constants';
/**
* Build a proxied URL that routes through llama-server's CORS proxy.
@@ -18,7 +14,7 @@ export function buildProxiedUrl(targetUrl: string): URL {
const proxyPath = `${base}${CORS_PROXY_ENDPOINT}`;
const proxyUrl = new URL(proxyPath, window.location.origin);
proxyUrl.searchParams.set(CORS_PROXY_URL_PARAM, targetUrl);
proxyUrl.searchParams.set(CORS_PROXY.URL_PARAM, targetUrl);
return proxyUrl;
}
@@ -32,7 +28,7 @@ export function buildProxiedHeaders(headers: Record<string, string>): Record<str
const proxiedHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
proxiedHeaders[`${CORS_PROXY_HEADER_PREFIX}${key}`] = value;
proxiedHeaders[`${CORS_PROXY.HEADER_PREFIX}${key}`] = value;
}
return proxiedHeaders;
+4 -9
View File
@@ -12,12 +12,7 @@ import {
joinPath,
rankEntries
} from './working-directory';
import {
GLOB_WILDCARD,
PATH_NAV_MAX_DEPTH,
PATH_SEPARATOR,
WINDOWS_SEPARATOR
} from '$lib/constants';
import { GLOB, PATH_SEPARATOR, SEARCH } from '$lib/constants';
import { BuiltInTool, GlobSearchType } from '$lib/enums';
import { ToolsService } from '$lib/services/tools.service';
@@ -113,7 +108,7 @@ export async function runGlobSearchWithChildren(
options: GlobSearchChildOptions = {}
): Promise<GlobSearchChildResult> {
const {
childMaxDepth = PATH_NAV_MAX_DEPTH,
childMaxDepth = SEARCH.PATH_NAV_MAX_DEPTH,
descendOnTrailingSeparator = false,
type = GlobSearchType.ALL
} = options;
@@ -128,7 +123,7 @@ export async function runGlobSearchWithChildren(
if (last) {
const wantsDescend = descendOnTrailingSeparator
? query.endsWith(PATH_SEPARATOR) || query.endsWith(WINDOWS_SEPARATOR)
? query.endsWith(PATH_SEPARATOR) || query.endsWith(GLOB.WINDOWS_SEPARATOR)
: true;
const exact = ranked.find(
(e) => e.type === 'dir' && lastPathSegment(e.path).toLowerCase() === last.toLowerCase()
@@ -137,7 +132,7 @@ export async function runGlobSearchWithChildren(
if (wantsDescend && exact) {
const exactDir = joinPath(res.base, exact.path);
const childRes = await runGlobSearch(
{ include: GLOB_WILDCARD, maxDepth: childMaxDepth, path: exactDir, rankQuery: '' },
{ include: GLOB.WILDCARD, maxDepth: childMaxDepth, path: exactDir, rankQuery: '' },
type,
limit,
signal
+2 -2
View File
@@ -1,4 +1,4 @@
import { HEIC_JPEG_QUALITY } from '$lib/constants/image-size';
import { IMAGE } from '$lib/constants';
import { MimeTypeImage } from '$lib/enums';
// heic requires a relatively large decoder, in order to reduce primary bundle size
@@ -32,7 +32,7 @@ export async function heicFileToJpegDataURL(file: File | Blob): Promise<string>
const { heicTo } = await getHeicTo();
const jpegBlob = await heicTo({
blob: file,
quality: HEIC_JPEG_QUALITY,
quality: IMAGE.HEIC_JPEG_QUALITY,
type: MimeTypeImage.JPEG
});
+10 -1
View File
@@ -317,7 +317,16 @@ 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, type BuiltinToolUiEntry } from '$lib/constants/built-in-tools';
export { getBuiltinToolUi } from './built-in-tools';
export type { BuiltinToolUiEntry } from '$lib/types';
// Chat command picker
export { getChatCommands } from './chat-commands';
// Sandbox tool definition
// SANDBOX_TOOL_DEFINITION is deprecated; kept for backward compatibility.
export { buildSandboxToolDefinition, SANDBOX_TOOL_DEFINITION } from './sandbox-tool';
// Cryptography utilities
+11 -21
View File
@@ -1,14 +1,4 @@
import {
APP1_MARKER,
EXIF_ORIENTATION_TAG,
EXIF_SCAN_BYTE_LIMIT,
EXIF_SIGNATURE,
IFD_ENTRY_SIZE,
JPEG_SOI_MARKER,
SOS_MARKER,
TIFF_LITTLE_ENDIAN,
TIFF_MAGIC
} from '$lib/constants/jpeg-exif';
import { EXIF } from '$lib/constants';
import { MimeTypeImage } from '$lib/enums';
/**
@@ -28,7 +18,7 @@ export function getJpegOrientationFromDataURL(base64UrlJpeg: string): number {
}
// Keep the slice a multiple of 4 characters so atob accepts it
const charLimit = Math.ceil(EXIF_SCAN_BYTE_LIMIT / 3) * 4;
const charLimit = Math.ceil(EXIF.SCAN_BYTE_LIMIT / 3) * 4;
const slice = base64UrlJpeg.slice(payloadStart, payloadStart + charLimit);
const binary = atob(slice.slice(0, slice.length - (slice.length % 4)));
const bytes = new Uint8Array(binary.length);
@@ -49,7 +39,7 @@ export function getJpegOrientationFromDataURL(base64UrlJpeg: string): number {
* @returns The orientation value (1 to 8), or 1 when absent or malformed
*/
function findExifOrientation(view: DataView): number {
if (view.byteLength < 4 || view.getUint16(0) !== JPEG_SOI_MARKER) {
if (view.byteLength < 4 || view.getUint16(0) !== EXIF.JPEG_SOI_MARKER) {
return 1;
}
@@ -63,13 +53,13 @@ function findExifOrientation(view: DataView): number {
const marker = view.getUint8(offset + 1);
// Compressed image data starts here: no EXIF past this point
if (marker === SOS_MARKER) {
if (marker === EXIF.SOS_MARKER) {
return 1;
}
const segmentLength = view.getUint16(offset + 2);
if (marker === APP1_MARKER) {
if (marker === EXIF.APP1_MARKER) {
return parseExifOrientation(view, offset + 4, segmentLength);
}
@@ -92,7 +82,7 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
// The payload opens with the "Exif\0\0" signature
if (
start + 6 > end ||
view.getUint32(start) !== EXIF_SIGNATURE ||
view.getUint32(start) !== EXIF.EXIF_SIGNATURE ||
view.getUint16(start + 4) !== 0
) {
return 1;
@@ -104,9 +94,9 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
return 1;
}
const littleEndian = view.getUint16(tiff) === TIFF_LITTLE_ENDIAN;
const littleEndian = view.getUint16(tiff) === EXIF.TIFF_LITTLE_ENDIAN;
if (view.getUint16(tiff + 2, littleEndian) !== TIFF_MAGIC) {
if (view.getUint16(tiff + 2, littleEndian) !== EXIF.TIFF_MAGIC) {
return 1;
}
@@ -120,13 +110,13 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
// Scan IFD0 entries for the orientation tag
for (let i = 0; i < entryCount; i++) {
const entry = tiff + ifdOffset + 2 + i * IFD_ENTRY_SIZE;
const entry = tiff + ifdOffset + 2 + i * EXIF.IFD_ENTRY_SIZE;
if (entry + IFD_ENTRY_SIZE > end) {
if (entry + EXIF.IFD_ENTRY_SIZE > end) {
return 1;
}
if (view.getUint16(entry, littleEndian) === EXIF_ORIENTATION_TAG) {
if (view.getUint16(entry, littleEndian) === EXIF.ORIENTATION_TAG) {
const orientation = view.getUint16(entry + 8, littleEndian);
return orientation >= 1 && orientation <= 8 ? orientation : 1;
+20 -27
View File
@@ -15,23 +15,16 @@ import {
FILE_EXTENSION_REGEX,
IMAGE_FILE_EXTENSION_REGEX,
MCP_SERVER_ID_PREFIX,
MCP_SSE_ENDPOINT,
MCP_SSE_ENDPOINT_QUERY,
MCP_SSE_ENDPOINT_SLASH,
MCP_SSE,
MIME_TYPE_PREFIXES,
MIME_TYPE_SUBSTRINGS,
PATH_SEPARATOR,
PROTOCOL_PREFIX_REGEX,
RESOURCE_TEXT_CONTENT_SEPARATOR,
TEXT_FILE_EXTENSION_REGEX
TEXT_FILE_EXTENSION_REGEX,
URI_PATTERNS
} from '$lib/constants';
import {
MCPLogLevel,
MCPTransportType,
MimeTypeIncludes,
MimeTypePrefix,
MimeTypeText,
UriPattern,
UrlProtocol
} from '$lib/enums';
import { MCPLogLevel, MCPTransportType, MimeTypeText, UrlProtocol } from '$lib/enums';
import type { MCPResourceContent, MCPResourceInfo, MCPServerSettingsEntry } from '$lib/types';
import type { MimeTypeUnion } from '$lib/types/common';
import type { Component } from 'svelte';
@@ -51,9 +44,9 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType {
}
if (
normalized.endsWith(MCP_SSE_ENDPOINT) ||
normalized.endsWith(MCP_SSE_ENDPOINT_SLASH) ||
normalized.includes(MCP_SSE_ENDPOINT_QUERY)
normalized.endsWith(MCP_SSE.ENDPOINT) ||
normalized.endsWith(MCP_SSE.ENDPOINT_SLASH) ||
normalized.includes(MCP_SSE.ENDPOINT_QUERY)
) {
return MCPTransportType.SSE;
}
@@ -150,7 +143,7 @@ export function getMcpLogLevelClass(level: MCPLogLevel): string {
* @returns True if the MIME type starts with 'image/'
*/
export function isImageMimeType(mimeType?: MimeTypeUnion): boolean {
return mimeType?.startsWith(MimeTypePrefix.IMAGE) ?? false;
return mimeType?.startsWith(MIME_TYPE_PREFIXES.IMAGE) ?? false;
}
/**
@@ -213,9 +206,9 @@ export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean
const u = uri?.toLowerCase() || '';
return (
mime.includes(MimeTypeIncludes.JSON) ||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
mime.includes(MIME_TYPE_SUBSTRINGS.JSON) ||
mime.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT) ||
mime.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT) ||
CODE_FILE_EXTENSION_REGEX.test(u)
);
}
@@ -231,7 +224,7 @@ export function isImageResource(mimeType?: MimeTypeUnion, uri?: string): boolean
const mime = mimeType?.toLowerCase() || '';
const u = uri?.toLowerCase() || '';
return mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u);
return mime.startsWith(MIME_TYPE_PREFIXES.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u);
}
/**
@@ -245,24 +238,24 @@ export function getResourceIcon(mimeType?: MimeTypeUnion, uri?: string): Compone
const mime = mimeType?.toLowerCase() || '';
const u = uri?.toLowerCase() || '';
if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
if (mime.startsWith(MIME_TYPE_PREFIXES.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
return Image;
}
if (
mime.includes(MimeTypeIncludes.JSON) ||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
mime.includes(MIME_TYPE_SUBSTRINGS.JSON) ||
mime.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT) ||
mime.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT) ||
CODE_FILE_EXTENSION_REGEX.test(u)
) {
return Code;
}
if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
if (mime.includes(MIME_TYPE_PREFIXES.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
return FileText;
}
if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) {
if (u.includes(URI_PATTERNS.DATABASE_KEYWORD) || u.includes(URI_PATTERNS.DATABASE_SCHEME)) {
return Database;
}
+4 -3
View File
@@ -1,10 +1,11 @@
import { abbreviateHome, lastPathSegment } from './path-display';
import { DIRECTORY_PATH_SUFFIX, FILE_URI_PREFIX } from '$lib/constants';
import {
DIRECTORY_PATH_SUFFIX,
FILE_URI_PREFIX,
MENTION_BADGE_FILE_ICON_PATHS,
MENTION_BADGE_FOLDER_ICON_PATHS,
MENTION_LINK_SCAN_FLAGS
} from '$lib/constants/mention-badge';
} from '$lib/constants';
import { FileMentionEntryType } from '$lib/enums';
import type { FileMentionEntry } from '$lib/types';
@@ -14,7 +15,7 @@ export {
MENTION_BADGE_SVG_ATTRIBUTES,
MENTION_BADGE_FILE_ICON_PATHS,
MENTION_BADGE_FOLDER_ICON_PATHS
} from '$lib/constants/mention-badge';
} from '$lib/constants';
// `)` is allowed in a path only when not followed by whitespace or `[`,
// so macOS paths parse while adjacent badges still terminate the match.
+3 -3
View File
@@ -4,10 +4,10 @@ import {
CWD_LINK_REGEX,
FILE_URI_PREFIX,
HOME_TILDE,
HOME_TILDE_PREFIX
HOME_TILDE_PREFIX,
PATH_SEPARATOR,
TRAILING_SLASHES_REGEX
} from '$lib/constants';
import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
export function lastPathSegment(p: string): string {
const trimmed = p.replace(TRAILING_SLASHES_REGEX, '');
+52
View File
@@ -0,0 +1,52 @@
import {
SANDBOX_TIMEOUT_MS_DEFAULT,
SANDBOX_TIMEOUT_MS_MAX,
SANDBOX_TOOL_NAME
} from '$lib/constants';
import { JsonSchemaType, ToolCallType } from '$lib/enums';
import type { OpenAIToolDefinition } from '$lib/types';
const NERDAMER_DESCRIPTION = `
Symbolic/numeric math via \`nerdamer\`
nerdamer(expr,subs?,opts?)/nerdamer.func(...)→Expression Format via .text(fmt?) (fmt: 'decimals'|'fractions'|'scientific') eval via .evaluate(subs?)
nerdamer(expr,{x:2}) substitutes numeric via opts 'numer' or .evaluate()
simplify/expand/factor(expr) div/gcd/lcm(...) coeffs/partfrac(expr,var)
diff/integrate(expr,var) defint(expr,lo,hi,var?) sum/product(expr,var,lo,hi) limit(expr,var,pt)
solve(expr,var) solveEquations([eq1,eq2],[var1,var2])
polarform/rectform/arg/realpart/imagpart(z)
set/get Var/Constant(name,val?) setFunction(name,[params],body)
IMPORTANT:Identifier 'nerdamer' has already been declared, use it directly`;
/**
* Build the sandbox tool definition. When `includeSymbolicMath` is true,
* the description includes nerdamer API documentation; otherwise it
* describes a plain JavaScript sandbox.
*/
export function buildSandboxToolDefinition(includeSymbolicMath: boolean): OpenAIToolDefinition {
return {
function: {
description: includeSymbolicMath
? `Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.${NERDAMER_DESCRIPTION}`
: 'Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.',
name: SANDBOX_TOOL_NAME,
parameters: {
properties: {
code: {
description: 'JavaScript source to execute',
type: JsonSchemaType.STRING
},
timeout_ms: {
description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}`,
type: JsonSchemaType.NUMBER
}
},
required: ['code'],
type: JsonSchemaType.OBJECT
}
},
type: ToolCallType.FUNCTION
};
}
/** @deprecated Use {@link buildSandboxToolDefinition} instead. Kept for backward compatibility. */
export const SANDBOX_TOOL_DEFINITION = buildSandboxToolDefinition(true);
+5 -5
View File
@@ -1,4 +1,4 @@
import { SVG_MAX_BYTES, SVG_SANITIZE_CONFIG, SVG_TAG_PREFIX } from '$lib/constants';
import { SVG } from '$lib/constants';
import DOMPurify from 'dompurify';
/**
@@ -10,13 +10,13 @@ import DOMPurify from 'dompurify';
export function sanitizeSvg(source: string): string {
const trimmed = source.trim();
if (!trimmed || trimmed.length > SVG_MAX_BYTES) return '';
if (!trimmed || trimmed.length > SVG.MAX_BYTES) return '';
if (!trimmed.startsWith(SVG_TAG_PREFIX)) return '';
if (!trimmed.startsWith(SVG.TAG_PREFIX)) return '';
const clean = DOMPurify.sanitize(trimmed, SVG_SANITIZE_CONFIG) as unknown as string;
const clean = DOMPurify.sanitize(trimmed, SVG.SANITIZE_CONFIG) as unknown as string;
if (!clean || !clean.includes(SVG_TAG_PREFIX)) return '';
if (!clean || !clean.includes(SVG.TAG_PREFIX)) return '';
return clean;
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { CONVERSATION_ID_SEPARATOR } from '$lib/constants';
/**
* Build the conversation identity used by the server side replay buffer.
*
@@ -11,5 +13,5 @@ export function streamIdentity(conversationId: string, model?: string | null): s
if (!model) return conversationId;
return `${conversationId}::${model}`;
return `${conversationId}${CONVERSATION_ID_SEPARATOR}${model}`;
}
+20 -22
View File
@@ -2,8 +2,7 @@ import {
LEADING_SLASHES_REGEX,
TEMPLATE_EXPRESSION_REGEX,
URI_SCHEME_SEPARATOR,
URI_TEMPLATE_OPERATORS,
URI_TEMPLATE_SEPARATORS,
URI_TEMPLATE_SYMBOLS,
VARIABLE_EXPLODE_MODIFIER_REGEX,
VARIABLE_PREFIX_MODIFIER_REGEX
} from '../constants';
@@ -126,60 +125,59 @@ export function expandTemplate(template: string, values: Record<string, string>)
if (expandedParts.length === 0) return '';
switch (operator) {
case URI_TEMPLATE_OPERATORS.RESERVED:
case URI_TEMPLATE_SYMBOLS.RESERVED:
// Reserved expansion: no encoding
return expandedParts.join(URI_TEMPLATE_SEPARATORS.COMMA);
case URI_TEMPLATE_OPERATORS.FRAGMENT:
return expandedParts.join(URI_TEMPLATE_SYMBOLS.COMMA);
case URI_TEMPLATE_SYMBOLS.FRAGMENT:
// Fragment expansion
return (
URI_TEMPLATE_OPERATORS.FRAGMENT + expandedParts.join(URI_TEMPLATE_SEPARATORS.COMMA)
);
case URI_TEMPLATE_OPERATORS.PATH_SEGMENT:
return URI_TEMPLATE_SYMBOLS.FRAGMENT + expandedParts.join(URI_TEMPLATE_SYMBOLS.COMMA);
case URI_TEMPLATE_SYMBOLS.PATH_SEGMENT:
// Path segments
return URI_TEMPLATE_SEPARATORS.SLASH + expandedParts.join(URI_TEMPLATE_SEPARATORS.SLASH);
case URI_TEMPLATE_OPERATORS.LABEL:
// Label expansion
return (
URI_TEMPLATE_SEPARATORS.PERIOD + expandedParts.join(URI_TEMPLATE_SEPARATORS.PERIOD)
URI_TEMPLATE_SYMBOLS.PATH_SEGMENT +
expandedParts.join(URI_TEMPLATE_SYMBOLS.PATH_SEGMENT)
);
case URI_TEMPLATE_OPERATORS.PATH_PARAM:
case URI_TEMPLATE_SYMBOLS.LABEL:
// Label expansion
return URI_TEMPLATE_SYMBOLS.LABEL + expandedParts.join(URI_TEMPLATE_SYMBOLS.LABEL);
case URI_TEMPLATE_SYMBOLS.PATH_PARAM:
// Path-style parameters
return varNames
.filter((_: string, i: number) => expandedParts[i])
.map(
(name: string, i: number) =>
`${URI_TEMPLATE_SEPARATORS.SEMICOLON}${name}=${expandedParts[i]}`
`${URI_TEMPLATE_SYMBOLS.PATH_PARAM}${name}=${expandedParts[i]}`
)
.join('');
case URI_TEMPLATE_OPERATORS.FORM_QUERY:
case URI_TEMPLATE_SYMBOLS.FORM_QUERY:
// Form-style query
return (
URI_TEMPLATE_SEPARATORS.QUERY_PREFIX +
URI_TEMPLATE_SYMBOLS.FORM_QUERY +
varNames
.filter((_: string, i: number) => expandedParts[i])
.map(
(name: string, i: number) =>
`${encodeURIComponent(name)}=${encodeURIComponent(expandedParts[i])}`
)
.join(URI_TEMPLATE_SEPARATORS.QUERY_CONTINUATION)
.join(URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION)
);
case URI_TEMPLATE_OPERATORS.FORM_CONTINUATION:
case URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION:
// Form-style query continuation
return (
URI_TEMPLATE_SEPARATORS.QUERY_CONTINUATION +
URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION +
varNames
.filter((_: string, i: number) => expandedParts[i])
.map(
(name: string, i: number) =>
`${encodeURIComponent(name)}=${encodeURIComponent(expandedParts[i])}`
)
.join(URI_TEMPLATE_SEPARATORS.COMMA)
.join(URI_TEMPLATE_SYMBOLS.COMMA)
);
default:
// Simple string expansion (default operator)
return expandedParts
.map((v: string) => encodeURIComponent(v))
.join(URI_TEMPLATE_SEPARATORS.COMMA);
.join(URI_TEMPLATE_SYMBOLS.COMMA);
}
}
);
+15 -21
View File
@@ -7,20 +7,13 @@
import { lastPathSegment } from './path-display';
import {
DRIVE_PREFIX_REGEX,
DRIVE_ROOT_REGEX,
GLOB_RANGE_CLOSE,
GLOB_RANGE_OPEN,
GLOB_SPECIAL_CHARS,
GLOB_WILDCARD,
GLOB,
HOME_TILDE,
LEADING_SLASHES_REGEX,
PATH_NAV_MAX_DEPTH,
UNC_ROOT_REGEX,
WINDOWS_SEPARATOR
PATH_SEPARATOR,
SEARCH,
TRAILING_SLASHES_REGEX
} from '$lib/constants';
import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
export interface GlobEntry {
path: string;
@@ -37,17 +30,18 @@ export interface PathQuery {
* backslash is left alone: it is a legal filename character on POSIX.
*/
function toPosixSeparators(query: string): string {
if (!DRIVE_PREFIX_REGEX.test(query) && !query.startsWith(WINDOWS_SEPARATOR)) return query;
if (!GLOB.DRIVE_PREFIX_REGEX.test(query) && !query.startsWith(GLOB.WINDOWS_SEPARATOR))
return query;
return query.split(WINDOWS_SEPARATOR).join(PATH_SEPARATOR);
return query.split(GLOB.WINDOWS_SEPARATOR).join(PATH_SEPARATOR);
}
export function rootPrefixLength(path: string): number {
const unc = path.match(UNC_ROOT_REGEX);
const unc = path.match(GLOB.UNC_ROOT_REGEX);
if (unc) return unc[0].length;
const drive = path.match(DRIVE_ROOT_REGEX);
const drive = path.match(GLOB.DRIVE_ROOT_REGEX);
if (drive) return drive[0].length;
@@ -83,20 +77,20 @@ export function splitPathQuery(query: string): PathQuery | null {
}
export function buildCaseInsensitiveGlob(query: string): string {
let out = GLOB_WILDCARD;
let out = GLOB.WILDCARD;
for (const c of query) {
const lo = c.toLowerCase();
const up = c.toUpperCase();
if (lo !== up) out += GLOB_RANGE_OPEN + lo + up + GLOB_RANGE_CLOSE;
if (lo !== up) out += GLOB.RANGE_OPEN + lo + up + GLOB.RANGE_CLOSE;
// glob metacharacters are escaped into a literal character class so a
// query like "a*b" matches a literal '*' instead of becoming "ab"
else if (GLOB_SPECIAL_CHARS.includes(c)) out += GLOB_RANGE_OPEN + c + GLOB_RANGE_CLOSE;
else if (GLOB.SPECIAL_CHARS.includes(c)) out += GLOB.RANGE_OPEN + c + GLOB.RANGE_CLOSE;
else out += c;
}
return out + GLOB_WILDCARD;
return out + GLOB.WILDCARD;
}
export interface GlobSearchArgs {
@@ -120,9 +114,9 @@ export function buildGlobSearchArgs(
const include = pathQuery
? pathQuery.last
? buildCaseInsensitiveGlob(pathQuery.last)
: GLOB_WILDCARD
: GLOB.WILDCARD
: buildCaseInsensitiveGlob(query);
const maxDepth = pathQuery ? PATH_NAV_MAX_DEPTH : searchDepth;
const maxDepth = pathQuery ? SEARCH.PATH_NAV_MAX_DEPTH : searchDepth;
return { include, last: pathQuery?.last, maxDepth, path, rankQuery: pathQuery?.last ?? query };
}