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:
@@ -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,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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -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, '&').replace(LT_REGEX, '<').replace(GT_REGEX, '>');
|
||||
return code
|
||||
.replace(CODE_BLOCK.AMPERSAND_REGEX, '&')
|
||||
.replace(CODE_BLOCK.LT_REGEX, '<')
|
||||
.replace(CODE_BLOCK.GT_REGEX, '>');
|
||||
}
|
||||
|
||||
/** 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,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user