import { writeThemeFavicons } from './scripts/favicon-colorize';
-import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
+import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa.constants';
import { defineConfig } from '@vite-pwa/assets-generator/config';
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
PWA_ASSET_GENERATOR,
PWA_GENERATOR_DEVICES,
THEME_COLORS
-} from './src/lib/constants/pwa';
+} from './src/lib/constants/pwa.constants';
import { SplashOrientation } from './src/lib/enums/splash.enums';
import {
combinePresetAndAppleSplashScreens,
-import { BUILD_CONFIG } from '../src/lib/constants/pwa';
+import { BUILD_CONFIG } from '../src/lib/constants/pwa.constants';
import { existsSync, writeFileSync } from 'node:fs';
import { resolve } from 'path';
import type { Plugin } from 'vite';
-import { BUILD_CONFIG } from '../src/lib/constants/pwa';
+import { BUILD_CONFIG } from '../src/lib/constants/pwa.constants';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'path';
import type { Plugin } from 'vite';
-import { NEWLINE, TAB } from '../src/lib/constants/code';
-import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
+import {
+ APPLE_DEVICES,
+ BUILD_CONFIG,
+ REGEX_PATTERNS,
+ SPLASH_LINK
+} from '../src/lib/constants/pwa.constants';
+import { NEWLINE, TAB } from '../src/lib/constants/special-characters.constants';
import { SplashOrientation } from '../src/lib/enums/splash.enums';
import type { SplashDimensions } from '../src/lib/types';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
<script lang="ts">
import ActionIcon from './ActionIcon.svelte';
import { Copy } from '@lucide/svelte';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { copyToClipboard } from '$lib/utils';
export let ariaLabel: string = 'Copy to clipboard';
<script lang="ts">
import { Music, Video, X } from '@lucide/svelte';
import { ActionIcon } from '$lib/components/app';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { AttachmentType } from '$lib/enums';
import {
formatFileSize,
import { SyntaxHighlightedCode } from '$lib/components/app';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { PdfViewMode } from '$lib/enums';
import type { ChatAttachmentDisplayItem } from '$lib/types';
import { getLanguageFromFilename } from '$lib/utils';
<script lang="ts">
import { FileText, Music, Video } from '@lucide/svelte';
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface PreviewItem {
id: string;
import { Plus } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ATTACHMENT_TOOLTIP_TEXT, ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
disabled?: boolean;
import {
ATTACHMENT_FILE_ITEMS,
ATTACHMENT_TOOLTIP_TEXT,
+ ICON_CLASS_DEFAULT,
TOOLTIP_DELAY_DURATION
} from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
interface Props {
import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { Switch } from '$lib/components/ui/switch';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
- import { ROUTES } from '$lib/constants/routes';
+ import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
const reasoning = useReasoningMenu();
import * as Sheet from '$lib/components/ui/sheet';
import { Switch } from '$lib/components/ui/switch';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
- import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import {
+ ATTACHMENT_FILE_ITEMS,
+ ICON_CLASS_DEFAULT,
+ TOOLTIP_DELAY_DURATION
+ } from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { AttachmentAction } from '$lib/enums/attachment.enums';
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
import * as Collapsible from '$lib/components/ui/collapsible';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { CLI_FLAGS } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { CLI_FLAGS, ICON_CLASS_DEFAULT } from '$lib/constants';
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { toolsStore } from '$lib/stores/tools.svelte';
import { Mic, Square } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
class?: string;
ChatFormContextGauge
} from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
- import { ROUTES } from '$lib/constants/routes';
+ import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
import { FileTypeCategory, MessageRole } from '$lib/enums';
import { ChatService } from '$lib/services';
import {
<script lang="ts">
- import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
+ import { CODE_BLOCK } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { isMobile } from '$lib/stores/viewport.svelte';
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
const prefix = open[0];
const language = open[1].trim().split(/\s+/)[0] ?? '';
const content = segment.slice(prefix.length, -3);
- const leading = content.match(TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
- const trailing = content.match(TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
+ const leading = content.match(CODE_BLOCK.TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
+ const trailing = content.match(CODE_BLOCK.TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
const core = content.slice(leading.length, content.length - trailing.length);
// autoDetect off: re-guessing the language on every keystroke
// costs ~38ms a call and flickers while typing
import HighlightedMatch from '$lib/components/app/forms/HighlightedMatch.svelte';
import * as Popover from '$lib/components/ui/popover';
import * as Tooltip from '$lib/components/ui/tooltip';
- import {
- FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
- HOME_TILDE,
- SEARCH_DEBOUNCE_MS
- } from '$lib/constants';
+ import { FILE_GLOB_SEARCH_PICKERS, HOME_TILDE, SEARCH } from '$lib/constants';
import { BuiltInTool, FileMentionEntryType, GlobSearchType, KeyboardKey } from '$lib/enums';
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
const searchDepth = $derived.by(() => {
const n = Number(config().mentionSearchMaxDepth);
- return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH;
+ return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS.DEFAULT_SEARCH_DEPTH;
});
const home = $derived(toolsStore.serverHome);
const search = useDebouncedSearch({
canRun: () => isOpen && fileSearchEnabled,
- debounceMs: SEARCH_DEBOUNCE_MS,
+ debounceMs: SEARCH.DEBOUNCE_MS,
getQuery: () => trimmedQuery,
run: async (query, signal, isCurrent) => {
try {
import { FolderOpen } from '@lucide/svelte';
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
import * as Popover from '$lib/components/ui/popover';
- import {
- DEFAULT_MOBILE_BREAKPOINT,
- HOME_TILDE,
- MAX_RESULTS_SHOWN,
- NATIVE_LIMIT,
- NATIVE_MAX_DEPTH,
- SEARCH_DEBOUNCE_MS,
- SEARCH_LIMIT,
- SEARCH_MAX_DEPTH
- } from '$lib/constants';
+ import { DEFAULT_MOBILE_BREAKPOINT, HOME_TILDE, SEARCH } from '$lib/constants';
import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
// children too, so path navigation does not require a trailing slash.
const search = useDebouncedSearch({
canRun: () => isOpen && fileSearchEnabled,
- debounceMs: SEARCH_DEBOUNCE_MS,
+ debounceMs: SEARCH.DEBOUNCE_MS,
getQuery: () => query.trim(),
run: async (q, signal, isCurrent) => {
const trimmed = q.trim();
const res = await runGlobSearchWithChildren(
trimmed,
homeBase ?? HOME_TILDE,
- SEARCH_MAX_DEPTH,
- SEARCH_LIMIT,
+ SEARCH.MAX_DEPTH,
+ SEARCH.LIMIT,
signal,
{ type: GlobSearchType.DIR }
);
}
searchScope = res.exactDir ?? res.args.path;
- queryResults = res.entries.map((e) => e.path).slice(0, MAX_RESULTS_SHOWN);
+ queryResults = res.entries.map((e) => e.path).slice(0, SEARCH.MAX_RESULTS_SHOWN);
if (queryResults.length > 0) {
nav.reset(0);
try {
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
include: buildCaseInsensitiveGlob(name),
- limit: NATIVE_LIMIT,
- max_depth: NATIVE_MAX_DEPTH,
+ limit: SEARCH.NATIVE_LIMIT,
+ max_depth: SEARCH.NATIVE_MAX_DEPTH,
path: homeBase ?? HOME_TILDE,
type: GlobSearchType.DIR
});
ChatMessageSystem,
ChatMessageUser
} from '$lib/components/app/chat';
- import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
- import { REASONING_TAGS } from '$lib/constants/agentic';
- import { ROUTES } from '$lib/constants/routes';
+ import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
import { DatabaseService } from '$lib/services/database.service';
import { Loader2 } from '@lucide/svelte';
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
- import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
import { AttachmentType, FileTypeText, MimeTypeAudio, ToolResultKind } from '$lib/enums';
import type { DatabaseMessageExtra } from '$lib/types';
import {
type AgenticSection,
classifyToolResult,
formatJsonPretty,
+ getBuiltinToolUi,
parseToolResultWithMedia,
type ToolResultLine
} from '$lib/utils';
import ToolCallBlock from './ToolCallBlock.svelte';
import { AlertTriangle, Check, Loader2, XCircle } from '@lucide/svelte';
import { CollapsibleTerminalBlock } from '$lib/components/app';
- import { SETTINGS_KEYS } from '$lib/constants';
- import { TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+ import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
import { config } from '$lib/stores/settings.svelte';
import { toolsStore } from '$lib/stores/tools.svelte';
import type { DatabaseMessageExtra } from '$lib/types';
import { parseReadFileMeta } from './parsers/read-file';
import ToolCallBlock from './ToolCallBlock.svelte';
import { SyntaxHighlightedCode } from '$lib/components/app';
- import { DEFAULT_LANGUAGE, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
+ import { CODE_BLOCK, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
import { type AgenticSection } from '$lib/utils';
interface Props {
{#if section.toolResult}
<SyntaxHighlightedCode
code={section.toolResult}
- language={readFileMeta?.language ?? DEFAULT_LANGUAGE}
+ language={readFileMeta?.language ?? CODE_BLOCK.DEFAULT_LANGUAGE}
maxHeight={MAX_HEIGHT_CODE_BLOCK}
/>
{:else}
<script lang="ts">
import { parseReadMediaMeta } from './parsers/read-media';
import ToolCallBlock from './ToolCallBlock.svelte';
- import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic';
+ import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic.constants';
import { AttachmentType, MimeTypeAudio } from '$lib/enums';
import type { DatabaseMessageExtraAudioFile, DatabaseMessageExtraImageFile } from '$lib/types';
import { type AgenticSection } from '$lib/utils';
import { Globe, Loader2 } from '@lucide/svelte';
import { CollapsibleContentBlock } from '$lib/components/app';
import * as HoverCard from '$lib/components/ui/hover-card';
- import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
import { AgenticSectionType } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
import { Loader2, Wrench } from '@lucide/svelte';
import { CollapsibleContentBlock } from '$lib/components/app';
- import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
- import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
import { AgenticSectionType } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
- import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
+ import { type AgenticSection, type BuiltinToolUiEntry, getBuiltinToolUi } from '$lib/utils';
import type { Component, Snippet } from 'svelte';
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
// can render incrementally as the file path streams in.
import { parseToolArgs } from './_shared';
-import {
- DEFAULT_LANGUAGE,
- FILE_PATH_SEPARATOR_REGEX,
- TEXT_LANGUAGE_PREFIX_REGEX
-} from '$lib/constants';
+import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, getFileTypeByExtension } from '$lib/utils';
}
const fileType = getFileTypeByExtension(fileName);
- const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
+ const language = fileType
+ ? fileType.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '')
+ : CODE_BLOCK.DEFAULT_LANGUAGE;
return { fileName, language, lineRange };
}
-import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants/code';
+import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants';
import {
PREFIX_FILE,
PREFIX_MIME,
// result blob.
import { parseToolArgs } from './_shared';
-import {
- DEFAULT_LANGUAGE,
- FILE_PATH_SEPARATOR_REGEX,
- TEXT_LANGUAGE_PREFIX_REGEX
-} from '$lib/constants';
+import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
const content = typeof args.content === 'string' ? args.content : '';
const language =
- getFileTypeByExtension(rawPath)?.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') ?? DEFAULT_LANGUAGE;
+ getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
+ CODE_BLOCK.DEFAULT_LANGUAGE;
const resultObj = tryParseToolResultObject(section.toolResult);
const bytesWritten =
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
<script lang="ts">
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { Component, Snippet } from 'svelte';
interface Props {
<script lang="ts">
import { Lightbulb } from '@lucide/svelte';
import { CollapsibleContentBlock, MarkdownContent } from '$lib/components/app';
- import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+ import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
import { AgenticSectionType } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
import type { DatabaseMessageExtra } from '$lib/types';
<script lang="ts">
import { ArrowDown } from '@lucide/svelte';
import ActionIcon from '$lib/components/app/actions/ActionIcon.svelte';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
let { onclick }: { onclick: (e?: MouseEvent) => void } = $props();
</script>
<script lang="ts">
import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
import * as Alert from '$lib/components/ui/alert';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
let hasError = $derived(!!serverError());
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import { cn } from '$lib/components/ui/utils';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { Snippet } from 'svelte';
import type { Component } from 'svelte';
} from '$lib/components/app';
import {
BOOL_TRUE_STRING,
- CODE_BLOCK_HEADER_CLASS,
+ CODE_BLOCK_CLASS,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
MERMAID_SYNTAX_ATTR,
MERMAID_WRAPPER_CLASS,
SETTINGS_KEYS,
- SVG_BLOCK_CLASS,
- SVG_INLINE_SHADOW_STYLE,
- SVG_LANGUAGE,
- SVG_RENDERED_ATTR,
- SVG_SOURCE_ATTR,
- SVG_TAG_PREFIX,
- SVG_WRAPPER_CLASS,
- TOGGLE_SOURCE_BTN_CLASS,
- XML_LANGUAGE
+ SVG,
+ TOGGLE_SOURCE_BTN_CLASS
} from '$lib/constants';
import { ColorMode, UrlProtocol } from '$lib/enums';
import { FileTypeText } from '$lib/enums/files.enums';
if (!block) return null;
- if (block.language === SVG_LANGUAGE) return block.code;
+ if (block.language === SVG.LANGUAGE) return block.code;
- if (block.language === XML_LANGUAGE && block.code.trimStart().startsWith(SVG_TAG_PREFIX))
+ if (block.language === SVG.XML_LANGUAGE && block.code.trimStart().startsWith(SVG.TAG_PREFIX))
return block.code;
return null;
// Mount the streaming svg into its shadow host on every chunk so it renders live
$effect(() => {
- if (streamingSvgHost) mountSvgShadow(streamingSvgHost, liveSvgHtml, SVG_INLINE_SHADOW_STYLE);
+ if (streamingSvgHost) mountSvgShadow(streamingSvgHost, liveSvgHtml, SVG.INLINE_SHADOW_STYLE);
});
let streamingCodeScrollContainer = $state<HTMLDivElement>();
event.preventDefault();
event.stopPropagation();
- const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`);
+ const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG.WRAPPER_CLASS}`);
if (!wrapper) return;
}
// Check if clicking on copy or preview button in svg block
- const svgCopyBtn = target.closest(`.${SVG_WRAPPER_CLASS} .copy-code-btn`);
- const svgPreviewBtn = target.closest(`.${SVG_WRAPPER_CLASS} .preview-code-btn`);
+ const svgCopyBtn = target.closest(`.${SVG.WRAPPER_CLASS} .copy-code-btn`);
+ const svgPreviewBtn = target.closest(`.${SVG.WRAPPER_CLASS} .preview-code-btn`);
if (svgCopyBtn || svgPreviewBtn) {
- const wrapper = target.closest(`.${SVG_WRAPPER_CLASS}`);
+ const wrapper = target.closest(`.${SVG.WRAPPER_CLASS}`);
if (!wrapper) return;
const preElement = wrapper.querySelector<HTMLElement>(
- `pre.${SVG_BLOCK_CLASS}[${SVG_SOURCE_ATTR}]`
+ `pre.${SVG.BLOCK_CLASS}[${SVG.SOURCE_ATTR}]`
);
if (!preElement) return;
event.preventDefault();
event.stopPropagation();
try {
- await copyToClipboard(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
+ await copyToClipboard(preElement.getAttribute(SVG.SOURCE_ATTR) ?? '');
} catch (error) {
console.error('Failed to copy svg source:', error);
}
if (svgPreviewBtn) {
event.preventDefault();
event.stopPropagation();
- mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
+ mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG.SOURCE_ATTR) ?? '');
svgPreviewLive = false;
mermaidPreviewOpen = true;
// A click on the header chrome targets the action buttons, never the
// diagram. Guard so a header click can not fall through to the click to
// zoom branches below, whatever the scroll position or stacking.
- if (target.closest(`.${CODE_BLOCK_HEADER_CLASS}`)) return;
+ if (target.closest(`.${CODE_BLOCK_CLASS.HEADER}`)) return;
// Open preview when clicking the svg block itself. A final block carries its
// source, a streaming block does not and is mirrored live into the dialog.
- const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`);
+ const svgEl = target.closest(`.${SVG.BLOCK_CLASS}`);
if (svgEl) {
- const source = svgEl.getAttribute(SVG_SOURCE_ATTR);
+ const source = svgEl.getAttribute(SVG.SOURCE_ATTR);
if (source !== null) {
mermaidPreviewSvgHtml = sanitizeSvg(source);
if (!containerRef) return;
const nodes = containerRef.querySelectorAll<HTMLElement>(
- `pre.${SVG_BLOCK_CLASS}:not([${SVG_RENDERED_ATTR}])`
+ `pre.${SVG.BLOCK_CLASS}:not([${SVG.RENDERED_ATTR}])`
);
if (nodes.length === 0) return;
nodes.forEach((node) => {
- node.setAttribute(SVG_RENDERED_ATTR, 'true');
+ node.setAttribute(SVG.RENDERED_ATTR, 'true');
- const source = node.getAttribute(SVG_SOURCE_ATTR) ?? node.textContent ?? '';
+ const source = node.getAttribute(SVG.SOURCE_ATTR) ?? node.textContent ?? '';
const clean = sanitizeSvg(source);
if (clean) {
const host = document.createElement('div');
node.appendChild(host);
- mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE);
+ mountSvgShadow(host, clean, SVG.INLINE_SHADOW_STYLE);
}
});
}
</div>
{#if liveSvgHtml}
<div class="svg-scroll-container">
- <div class={SVG_BLOCK_CLASS}>
+ <div class={SVG.BLOCK_CLASS}>
<div bind:this={streamingSvgHost}></div>
</div>
</div>
*/
import {
- CODE_BLOCK_ACTIONS_CLASS,
- CODE_BLOCK_HEADER_CLASS,
- CODE_BLOCK_SCROLL_CONTAINER_CLASS,
+ CODE_BLOCK_CLASS,
CODE_ICON_SVG,
- CODE_LANGUAGE_CLASS,
- COPY_CODE_BTN_CLASS,
COPY_ICON_SVG,
DIAGRAM_SOURCE_CLASS,
- PREVIEW_CODE_BTN_CLASS,
PREVIEW_ICON_SVG,
- RELATIVE_CLASS,
TOGGLE_SOURCE_BTN_CLASS
} from '$lib/constants';
import type { Element, ElementContent } from 'hast';
* Creates a copy button element.
*/
export function createCopyButton(id: string, idAttribute: string, title: string = 'Copy'): Element {
- return createButton(COPY_CODE_BTN_CLASS, title, COPY_ICON_SVG, id, idAttribute);
+ return createButton(CODE_BLOCK_CLASS.COPY_BTN, title, COPY_ICON_SVG, id, idAttribute);
}
/**
idAttribute: string,
title: string = 'Preview'
): Element {
- return createButton(PREVIEW_CODE_BTN_CLASS, title, PREVIEW_ICON_SVG, id, idAttribute);
+ return createButton(CODE_BLOCK_CLASS.PREVIEW_BTN, title, PREVIEW_ICON_SVG, id, idAttribute);
}
/**
type: 'element'
}
],
- properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
+ properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_CLASS.SCROLL_CONTAINER] },
tagName: 'div',
type: 'element'
};
id: string,
idAttribute: string,
actions: Element[],
- languageClassName: string = CODE_LANGUAGE_CLASS
+ languageClassName: string = CODE_BLOCK_CLASS.LANGUAGE
): Element {
return {
children: [
},
{
children: actions,
- properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
+ properties: { className: [CODE_BLOCK_CLASS.ACTIONS] },
tagName: 'div',
type: 'element'
}
],
- properties: { className: [CODE_BLOCK_HEADER_CLASS] },
+ properties: { className: [CODE_BLOCK_CLASS.HEADER] },
tagName: 'div',
type: 'element'
};
return {
children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren],
properties: {
- className: [wrapperClass, RELATIVE_CLASS],
+ className: [wrapperClass, CODE_BLOCK_CLASS.RELATIVE],
...additionalAttributes
} as Element['properties'],
tagName: 'div',
createWrapper,
generateBlockId
} from './code-block-utils';
-import { CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_BLOCK_WRAPPER_CLASS } from '$lib/constants';
+import { CODE_BLOCK_CLASS } from '$lib/constants';
import type { Element, ElementContent, Root } from 'hast';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
const wrapper = createWrapper(
header,
node,
- CODE_BLOCK_WRAPPER_CLASS,
- CODE_BLOCK_SCROLL_CONTAINER_CLASS
+ CODE_BLOCK_CLASS.WRAPPER,
+ CODE_BLOCK_CLASS.SCROLL_CONTAINER
);
// Replace pre with wrapper in parent
generateBlockId
} from './code-block-utils';
import type { DiagramPreData } from './pre-transform';
-import {
- DIAGRAM_VIEW_MODE_ATTR,
- DIAGRAM_VIEW_RENDERED,
- SVG_BLOCK_CLASS,
- SVG_ID_ATTR,
- SVG_LANGUAGE,
- SVG_SCROLL_CONTAINER_CLASS,
- SVG_SOURCE_ATTR,
- SVG_WRAPPER_CLASS
-} from '$lib/constants';
+import { DIAGRAM_VIEW_MODE_ATTR, DIAGRAM_VIEW_RENDERED, SVG } from '$lib/constants';
import type { Element, ElementContent, Root } from 'hast';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
if (!Array.isArray(className)) return;
- const isSvg = className.some((cls) => typeof cls === 'string' && cls === SVG_BLOCK_CLASS);
+ const isSvg = className.some((cls) => typeof cls === 'string' && cls === SVG.BLOCK_CLASS);
if (!isSvg) return;
- const svgId = generateBlockId(SVG_LANGUAGE, 'idxSvgBlock');
+ const svgId = generateBlockId(SVG.LANGUAGE, 'idxSvgBlock');
// Extract the svg source (text content of the pre element)
const svgSource = node.children
.map((child) => {
// Store the svg source in data attribute for copy and render
node.properties = {
...node.properties,
- [SVG_ID_ATTR]: svgId,
- [SVG_SOURCE_ATTR]: svgSource
+ [SVG.ID_ATTR]: svgId,
+ [SVG.SOURCE_ATTR]: svgSource
};
const actions = [
- createCopyButton(svgId, SVG_ID_ATTR, 'Copy svg source'),
- createToggleSourceButton(svgId, SVG_ID_ATTR, 'Toggle svg source'),
- createPreviewButton(svgId, SVG_ID_ATTR, 'Preview svg')
+ createCopyButton(svgId, SVG.ID_ATTR, 'Copy svg source'),
+ createToggleSourceButton(svgId, SVG.ID_ATTR, 'Toggle svg source'),
+ createPreviewButton(svgId, SVG.ID_ATTR, 'Preview svg')
];
- const header = createBlockHeader(SVG_LANGUAGE, svgId, SVG_ID_ATTR, actions);
+ const header = createBlockHeader(SVG.LANGUAGE, svgId, SVG.ID_ATTR, actions);
const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
- const sourceView = createSourceView(preservedCode, svgSource, SVG_LANGUAGE);
+ const sourceView = createSourceView(preservedCode, svgSource, SVG.LANGUAGE);
const wrapper = createWrapper(
header,
node,
- SVG_WRAPPER_CLASS,
- SVG_SCROLL_CONTAINER_CLASS,
+ SVG.WRAPPER_CLASS,
+ SVG.SCROLL_CONTAINER_CLASS,
{
[DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED,
- [SVG_ID_ATTR]: svgId
+ [SVG.ID_ATTR]: svgId
},
[sourceView]
);
/**
* Rehype plugin that rewrites `file://` markdown anchors into the inline
* mention chip, sharing the class string with the contenteditable
- * tokenizer via `$lib/constants/mention-badge`.
+ * tokenizer via `$lib/constants`.
*
* The chip is presentational: `file://` navigation is blocked from
* http(s) pages, so the anchor becomes a plain `<span>` (no link role,
import { createPreTransform } from './pre-transform';
-import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_TAG_PREFIX, XML_LANGUAGE } from '$lib/constants';
+import { SVG } from '$lib/constants';
/**
* Converts svg code blocks to <pre class="svg-block"> for client-side rendering.
* svg inside an xml fence.
*/
export const rehypeSvgPre = createPreTransform(
- [SVG_LANGUAGE, XML_LANGUAGE],
- SVG_BLOCK_CLASS,
- (text) => text.startsWith(SVG_TAG_PREFIX)
+ [SVG.LANGUAGE, SVG.XML_LANGUAGE],
+ SVG.BLOCK_CLASS,
+ (text) => text.startsWith(SVG.TAG_PREFIX)
);
<script lang="ts">
import MermaidPreviewControls from './MermaidPreviewControls.svelte';
- import { SVG_DIALOG_SHADOW_STYLE } from '$lib/constants';
+ import { SVG } from '$lib/constants';
import { mountSvgShadow } from '$lib/utils/svg-shadow';
interface Props {
// Re-mount on every svgHtml change so a live streaming svg keeps rendering while zoomed
$effect(() => {
- if (svgHost) mountSvgShadow(svgHost, svgHtml, SVG_DIALOG_SHADOW_STYLE);
+ if (svgHost) mountSvgShadow(svgHost, svgHtml, SVG.DIALOG_SHADOW_STYLE);
});
// Zoom and pan state
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import ZoomInIcon from '@lucide/svelte/icons/zoom-in';
import ZoomOutIcon from '@lucide/svelte/icons/zoom-out';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
scale: number;
<script lang="ts">
import { browser } from '$app/environment';
- import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+ import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { highlightCode } from '$lib/utils';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import { ActionIconCopyToClipboard, SyntaxHighlightedCode } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
- import { DEFAULT_RESOURCE_FILENAME } from '$lib/constants';
- import { MimeTypeIncludes, MimeTypeText } from '$lib/enums';
+ import { DEFAULT_RESOURCE_FILENAME, MIME_TYPE_SUBSTRINGS } from '$lib/constants';
+ import { MimeTypeText } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
import {
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
function getLanguage(): string {
- if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return MimeTypeIncludes.JSON;
+ if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.JSON)) return MIME_TYPE_SUBSTRINGS.JSON;
- if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return MimeTypeIncludes.JAVASCRIPT;
+ if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT))
+ return MIME_TYPE_SUBSTRINGS.JAVASCRIPT;
- if (extra.mimeType?.includes(MimeTypeIncludes.TYPESCRIPT)) return MimeTypeIncludes.TYPESCRIPT;
+ if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT))
+ return MIME_TYPE_SUBSTRINGS.TYPESCRIPT;
const name = extra.name || extra.uri || '';
} from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import {
- BEARER_PREFIX,
BOOL_FALSE_STRING,
BOOL_TRUE_STRING,
DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY,
+ HEADERS,
MCP_SERVER_ID_PREFIX,
- RECOMMENDED_MCP_SERVERS,
- REDACTED_HEADERS
+ RECOMMENDED_MCP_SERVERS
} from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { conversationsStore } from '$lib/stores/conversations.svelte';
let bearerTokenFilled = $derived.by(() => {
const pairs = parseHeadersToArray(newServerHeaders);
- const bearerPrefix = BEARER_PREFIX.toLowerCase();
+ const bearerPrefix = HEADERS.BEARER.toLowerCase();
const bearer = pairs.find(
(p) =>
- REDACTED_HEADERS.has(p.key.trim().toLowerCase()) &&
+ HEADERS.REDACTED.has(p.key.trim().toLowerCase()) &&
p.value.trim().toLowerCase().startsWith(bearerPrefix)
);
import { goto } from '$app/navigation';
import { page } from '$app/state';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
- import { URL_PARAMS } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT, URL_PARAMS } from '$lib/constants';
interface Props {
open: boolean;
<script lang="ts">
import { Search, X } from '@lucide/svelte';
import { Input } from '$lib/components/ui/input';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
autofocus?: boolean;
<script lang="ts">
import McpLogo from './McpLogo.svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT, MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { AlertCircle, Download, FileText, Loader2 } from '@lucide/svelte';
import { ActionIconCopyToClipboard } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { MimeTypeApplication, MimeTypeText } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { MCPResourceContent, MCPResourceInfo } from '$lib/types';
import { Loader2, RefreshCw } from '@lucide/svelte';
import { SearchInput } from '$lib/components/app/forms';
import { Button } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
isLoading: boolean;
import { McpServerIdentity } from '$lib/components/app/mcp';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Collapsible from '$lib/components/ui/collapsible';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
import { getDisplayName, getResourceIcon } from '$lib/utils';
} from '$lib/components/app/mcp';
import * as Card from '$lib/components/ui/card';
import { Skeleton } from '$lib/components/ui/skeleton';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { HealthCheckStatus } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { HealthCheckState, MCPServerSettingsEntry } from '$lib/types';
import { KeyValuePairs } from '$lib/components/app';
import { Input } from '$lib/components/ui/input';
import { Switch } from '$lib/components/ui/switch';
- import {
- AUTHORIZATION_HEADER,
- BEARER_PREFIX,
- CLI_FLAGS,
- MCP_SERVER_URL_PLACEHOLDER,
- REDACTED_HEADERS
- } from '$lib/constants';
+ import { CLI_FLAGS, HEADERS, MCP_SERVER_URL_PLACEHOLDER } from '$lib/constants';
import { UrlProtocol } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { KeyValuePair } from '$lib/types';
// carry a Bearer scheme. Anything else (e.g. Basic, raw tokens) stays in the
// KV section so the user can still edit those values verbatim.
const matchesAuthorizationKey = (key: string): boolean =>
- REDACTED_HEADERS.has(key.trim().toLowerCase());
+ HEADERS.REDACTED.has(key.trim().toLowerCase());
const isBearerScheme = (value: string): boolean =>
- value.trim().toLowerCase().startsWith(BEARER_PREFIX.toLowerCase());
+ value.trim().toLowerCase().startsWith(HEADERS.BEARER.toLowerCase());
const ownedByBearerUi = (p: KeyValuePair): boolean =>
matchesAuthorizationKey(p.key) && isBearerScheme(p.value);
if (!auth) return '';
- return auth.value.trim().slice(BEARER_PREFIX.length).trim();
+ return auth.value.trim().slice(HEADERS.BEARER.length).trim();
});
$effect(() => {
const trimmed = token.trim();
if (trimmed) {
- filtered.push({ key: AUTHORIZATION_HEADER, value: `${BEARER_PREFIX}${trimmed}` });
+ filtered.push({ key: HEADERS.AUTHORIZATION, value: `${HEADERS.BEARER}${trimmed}` });
}
updateHeaderPairs(filtered);
<script lang="ts">
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { Snippet } from 'svelte';
interface Props {
RotateCw
} from '@lucide/svelte';
import { ActionIcon, ModelId } from '$lib/components/app';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ServerModelStatus } from '$lib/enums';
import { modelsStore, routerModels } from '$lib/stores/models.svelte';
import type { ModelOption } from '$lib/types/models';
import { ActionIcon, KeyboardShortcutInfo, SearchInput } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import {
+ ICON_CLASS_DEFAULT,
ICON_STRIP_TRANSITION_DELAY_MULTIPLIER,
ICON_STRIP_TRANSITION_DURATION,
ROUTES,
SIDEBAR_ACTIONS_ITEMS
} from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { TooltipSide } from '$lib/enums';
import { isMobile } from '$lib/stores/viewport.svelte';
import type { Component } from 'svelte';
import { TruncatedText } from '$lib/components/app';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { FORK_TREE_DEPTH_PADDING } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { FORK_TREE_DEPTH_PADDING, ICON_CLASS_DEFAULT } from '$lib/constants';
import { RouterService } from '$lib/services/router.service';
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
- import { AUTHORIZATION_HEADER, BEARER_PREFIX, SETTINGS_KEYS } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
- import { ROUTES } from '$lib/constants/routes';
+ import { HEADERS, ICON_CLASS_DEFAULT, ROUTES, SETTINGS_KEYS } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
import { serverLoading, serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
// Test the API key by making a real request to the server
const response = await fetch(`${base}/props`, {
headers: {
- [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKeyInput.trim()}`,
- 'Content-Type': 'application/json'
+ 'Content-Type': 'application/json',
+ [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKeyInput.trim()}`
}
});
import { AlertTriangle, Server } from '@lucide/svelte';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { singleModelName } from '$lib/stores/models.svelte';
import { serverError, serverLoading, serverProps } from '$lib/stores/server.svelte';
import * as RadioGroup from '$lib/components/ui/radio-group';
import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea';
- import { SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT, SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants';
import { SettingsFieldType } from '$lib/enums/settings.enums';
import { modelsStore, propsCacheVersion, selectedModelName } from '$lib/stores/models.svelte';
import { serverStore } from '$lib/stores/server.svelte';
<script lang="ts">
import { Button, type ButtonVariant } from '$lib/components/ui/button';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { Component } from 'svelte';
let {
import { McpServerIdentity, TruncatedText } from '$lib/components/app';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Collapsible from '$lib/components/ui/collapsible';
- import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { ToolSource } from '$lib/enums/tools.enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { permissionsStore } from '$lib/stores/permissions.svelte';
import { toolsStore } from '$lib/stores/tools.svelte';
+ import { getBuiltinToolUi } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
let expandedGroups = new SvelteSet<string>();
<script lang="ts">
import { Settings } from '@lucide/svelte';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
interface Props {
<script lang="ts">
import { ChevronLeft, ChevronRight, Settings } from '@lucide/svelte';
- import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+ import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte';
import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
import { onMount, tick } from 'svelte';
<script lang="ts">
- import { APP_NAME } from '$lib/constants';
- import { APPLE_META_TAGS, MEDIA_QUERIES, THEME_COLORS } from '$lib/constants/pwa';
+ import { APP_NAME, APPLE_META_TAGS, MEDIA_QUERIES, THEME_COLORS } from '$lib/constants';
let { appName = APP_NAME } = $props();
</script>
--- /dev/null
+import type { AgenticConfig } from '$lib/types/agentic';
+
+export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
+
+// JSON detection: trimmed content opens with an object or array literal.
+export const TOOL_RESULT_JSON_OPEN_REGEX = /^[[{]/;
+
+// Search-summary wire format used by file-glob and grep tools:
+// <matches>
+// ---
+// Total matches: N
+export const SEARCH_SUMMARY = {
+ SEPARATOR: '---\n',
+ TOTAL_REGEX: /Total matches:\s*(\d+)/
+} as const;
+
+// Separator rendered between stats in the tool-result footer (e.g. between a
+// result message and the byte/edit count). Plain ASCII spaces bracket a hyphen
+// so the whole " - " sits on one visual line even when the surrounding text
+// wraps mid-paragraph.
+export const RESULT_STAT_SEPARATOR = ' - ';
+
+export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
+ enabled: true,
+ maxTurns: 100
+} as const;
+
+export const REASONING_TAGS = {
+ END: '</think>',
+ START: '<think>'
+} as const;
+
+/**
+ * @deprecated Legacy marker tags - only used for migration of old stored messages.
+ * New messages use structured fields (reasoningContent, toolCalls, toolCallId).
+ */
+export const LEGACY_AGENTIC_TAGS = {
+ TAG_SUFFIX: '>>>',
+ TOOL_ARGS_END: '<<<TOOL_ARGS_END>>>',
+ TOOL_ARGS_START: '<<<TOOL_ARGS_START>>>',
+ TOOL_CALL_END: '<<<AGENTIC_TOOL_CALL_END>>>',
+ TOOL_CALL_START: '<<<AGENTIC_TOOL_CALL_START>>>',
+ TOOL_NAME_PREFIX: '<<<TOOL_NAME:'
+} as const;
+
+/**
+ * @deprecated Legacy reasoning tags - only used for migration of old stored messages.
+ * New messages use the dedicated reasoningContent field.
+ */
+export const LEGACY_REASONING_TAGS = {
+ END: '<<<reasoning_content_end>>>',
+ START: '<<<reasoning_content_start>>>'
+} as const;
+
+/**
+ * @deprecated Legacy regex patterns - only used for migration of old stored messages.
+ */
+export const LEGACY_AGENTIC_REGEX = {
+ AGENTIC_TOOL_CALL_BLOCK: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*?<<<AGENTIC_TOOL_CALL_END>>>/g,
+ AGENTIC_TOOL_CALL_OPEN: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*$/,
+ COMPLETED_TOOL_CALL:
+ /<<<AGENTIC_TOOL_CALL_START>>>\n<<<TOOL_NAME:(.+?)>>>\n<<<TOOL_ARGS_START>>>([\s\S]*?)<<<TOOL_ARGS_END>>>([\s\S]*?)<<<AGENTIC_TOOL_CALL_END>>>/g,
+ HAS_LEGACY_MARKERS: /<<<(?:AGENTIC_TOOL_CALL_START|reasoning_content_start)>>>/,
+ REASONING_BLOCK: /<<<reasoning_content_start>>>[\s\S]*?<<<reasoning_content_end>>>/g,
+ REASONING_EXTRACT: /<<<reasoning_content_start>>>([\s\S]*?)<<<reasoning_content_end>>>/,
+ REASONING_OPEN: /<<<reasoning_content_start>>>[\s\S]*$/
+} as const;
+++ /dev/null
-import type { AgenticConfig } from '$lib/types/agentic';
-
-export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
-
-// JSON detection: trimmed content opens with an object or array literal.
-export const TOOL_RESULT_JSON_OPEN_REGEX = /^[[{]/;
-
-// Markdown structural markers used by `looksLikeMarkdown`. Inline / line-level.
-export const MARKDOWN_CODE_FENCE_REGEX = /^(```|~~~)/m;
-export const MARKDOWN_ATX_HEADING_REGEX = /^#{1,6}\s+\S/;
-export const MARKDOWN_BLOCKQUOTE_REGEX = /^>\s+\S/;
-export const MARKDOWN_LIST_BULLET_REGEX = /^\s*[-*+]\s+\S/;
-export const MARKDOWN_LIST_NUMBERED_REGEX = /^\s*\d+[.)]\s+\S/;
-export const MARKDOWN_LINK_REGEX = /\[[^\]\n]+\]\([^)\s]+\)/;
-export const MARKDOWN_BOLD_REGEX = /\*\*[^*\n]+\*\*|__[^_\n]+__/;
-export const MARKDOWN_TABLE_SEPARATOR_REGEX = /^\s*\|?[\s:|-]+\|?\s*$/;
-
-// Search-summary wire format used by file-glob and grep tools:
-// <matches>
-// ---
-// Total matches: N
-export const SEARCH_SUMMARY_SEPARATOR = '---\n';
-export const SEARCH_SUMMARY_TOTAL_REGEX = /Total matches:\s*(\d+)/;
-
-// Separator rendered between stats in the tool-result footer (e.g. between a
-// result message and the byte/edit count). Plain ASCII spaces bracket a hyphen
-// so the whole " - " sits on one visual line even when the surrounding text
-// wraps mid-paragraph.
-export const RESULT_STAT_SEPARATOR = ' - ';
-
-export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
- enabled: true,
- maxTurns: 100
-} as const;
-
-export const REASONING_TAGS = {
- END: '</think>',
- START: '<think>'
-} as const;
-
-/**
- * @deprecated Legacy marker tags - only used for migration of old stored messages.
- * New messages use structured fields (reasoningContent, toolCalls, toolCallId).
- */
-export const LEGACY_AGENTIC_TAGS = {
- TAG_SUFFIX: '>>>',
- TOOL_ARGS_END: '<<<TOOL_ARGS_END>>>',
- TOOL_ARGS_START: '<<<TOOL_ARGS_START>>>',
- TOOL_CALL_END: '<<<AGENTIC_TOOL_CALL_END>>>',
- TOOL_CALL_START: '<<<AGENTIC_TOOL_CALL_START>>>',
- TOOL_NAME_PREFIX: '<<<TOOL_NAME:'
-} as const;
-
-/**
- * @deprecated Legacy reasoning tags - only used for migration of old stored messages.
- * New messages use the dedicated reasoningContent field.
- */
-export const LEGACY_REASONING_TAGS = {
- END: '<<<reasoning_content_end>>>',
- START: '<<<reasoning_content_start>>>'
-} as const;
-
-/**
- * @deprecated Legacy regex patterns - only used for migration of old stored messages.
- */
-export const LEGACY_AGENTIC_REGEX = {
- AGENTIC_TOOL_CALL_BLOCK: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*?<<<AGENTIC_TOOL_CALL_END>>>/g,
- AGENTIC_TOOL_CALL_OPEN: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*$/,
- COMPLETED_TOOL_CALL:
- /<<<AGENTIC_TOOL_CALL_START>>>\n<<<TOOL_NAME:(.+?)>>>\n<<<TOOL_ARGS_START>>>([\s\S]*?)<<<TOOL_ARGS_END>>>([\s\S]*?)<<<AGENTIC_TOOL_CALL_END>>>/g,
- HAS_LEGACY_MARKERS: /<<<(?:AGENTIC_TOOL_CALL_START|reasoning_content_start)>>>/,
- REASONING_BLOCK: /<<<reasoning_content_start>>>[\s\S]*?<<<reasoning_content_end>>>/g,
- REASONING_EXTRACT: /<<<reasoning_content_start>>>([\s\S]*?)<<<reasoning_content_end>>>/,
- REASONING_OPEN: /<<<reasoning_content_start>>>[\s\S]*$/
-} as const;
--- /dev/null
+export const API_MODELS = {
+ LIST: '/v1/models',
+ LOAD: '/models/load',
+ SSE: '/models/sse',
+ UNLOAD: '/models/unload'
+};
+
+// chat completion routes, the control route drives realtime inference (e.g. end reasoning)
+export const API_CHAT = {
+ COMPLETIONS: './v1/chat/completions',
+ CONTROL: './v1/chat/completions/control'
+};
+
+// slot introspection, requires the --slots flag on the server
+export const API_SLOTS = {
+ LIST: './slots'
+};
+
+export const API_TOOLS = {
+ EXECUTE: '/tools',
+ LIST: '/tools'
+};
+
+// resumable stream routes, the conv::model identity travels as the conv_id query param
+// because model names can contain slashes that a path segment cannot carry
+// resume retry cadence while the owning model is still loading (server answers 503)
+export const STREAM_RESUME_RETRY_MS = 2000;
+
+export const API_STREAM = {
+ BASE: './v1/stream',
+ LOOKUP: './v1/streams/lookup'
+};
+
+/** CORS proxy endpoint path */
+export const CORS_PROXY_ENDPOINT = '/cors-proxy';
+++ /dev/null
-export const API_MODELS = {
- LIST: '/v1/models',
- LOAD: '/models/load',
- SSE: '/models/sse',
- UNLOAD: '/models/unload'
-};
-
-// chat completion routes, the control route drives realtime inference (e.g. end reasoning)
-export const API_CHAT = {
- COMPLETIONS: './v1/chat/completions',
- CONTROL: './v1/chat/completions/control'
-};
-
-// slot introspection, requires the --slots flag on the server
-export const API_SLOTS = {
- LIST: './slots'
-};
-
-export const API_TOOLS = {
- EXECUTE: '/tools',
- LIST: '/tools'
-};
-
-// resumable stream routes, the conv::model identity travels as the conv_id query param
-// because model names can contain slashes that a path segment cannot carry
-// resume retry cadence while the owning model is still loading (server answers 503)
-export const STREAM_RESUME_RETRY_MS = 2000;
-
-export const API_STREAM = {
- BASE: './v1/stream',
- LOOKUP: './v1/streams/lookup'
-};
-
-/** CORS proxy endpoint path */
-export const CORS_PROXY_ENDPOINT = '/cors-proxy';
--- /dev/null
+export const APP_NAME = import.meta.env?.VITE_PUBLIC_APP_NAME || 'llama-ui';
+++ /dev/null
-export const APP_NAME = import.meta.env?.VITE_PUBLIC_APP_NAME || 'llama-ui';
+++ /dev/null
-export const ATTACHMENT_LABEL_FILE = 'File';
-export const ATTACHMENT_LABEL_PDF_FILE = 'PDF File';
-export const ATTACHMENT_LABEL_MCP_PROMPT = 'MCP Prompt';
-export const ATTACHMENT_LABEL_MCP_RESOURCE = 'MCP Resource';
--- /dev/null
+import { FolderOpen, MessageSquare, Zap } from '@lucide/svelte';
+import { FILE_TYPE_ICONS } from '$lib/constants';
+import {
+ AttachmentAction,
+ AttachmentItemEnabledWhen,
+ AttachmentItemVisibleWhen,
+ AttachmentMenuItemId
+} from '$lib/enums';
+import type { AttachmentMenuItem } from '$lib/types';
+
+/**
+ * File attachment menu items shown in both the desktop dropdown and mobile sheet.
+ * The "Tools" submenu is handled separately by each component.
+ */
+export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [
+ {
+ action: AttachmentAction.FILE_UPLOAD,
+ class: 'images-button',
+ disabledTooltip: 'Image processing requires a vision model',
+ enabledWhen: AttachmentItemEnabledWhen.HAS_VISION_MODALITY,
+ icon: FILE_TYPE_ICONS.image,
+ id: AttachmentMenuItemId.IMAGES,
+ label: 'Images'
+ },
+ {
+ action: AttachmentAction.FILE_UPLOAD,
+ class: 'audio-button',
+ disabledTooltip: 'Audio files processing requires an audio model',
+ enabledWhen: AttachmentItemEnabledWhen.HAS_AUDIO_MODALITY,
+ icon: FILE_TYPE_ICONS.audio,
+ id: AttachmentMenuItemId.AUDIO,
+ label: 'Audio Files'
+ },
+ {
+ action: AttachmentAction.FILE_UPLOAD,
+ class: 'video-button',
+ disabledTooltip: 'Video files processing requires a video model',
+ enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
+ icon: FILE_TYPE_ICONS.video,
+ id: AttachmentMenuItemId.VIDEO,
+ label: 'Video Files'
+ },
+ {
+ action: AttachmentAction.FILE_UPLOAD,
+ enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+ icon: FILE_TYPE_ICONS.text,
+ id: AttachmentMenuItemId.TEXT,
+ label: 'Text Files'
+ },
+ {
+ action: AttachmentAction.FILE_UPLOAD,
+ disabledTooltip: 'PDFs will be converted to text. Image-based PDFs may not work properly.',
+ enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+ hasEnabledTooltip: true,
+ icon: FILE_TYPE_ICONS.pdf,
+ id: AttachmentMenuItemId.PDF,
+ label: 'PDF Files'
+ }
+];
+
+export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [];
+
+export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [
+ {
+ action: AttachmentAction.SYSTEM_PROMPT_CLICK,
+ enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+ hasEnabledTooltip: true,
+ icon: MessageSquare,
+ id: AttachmentMenuItemId.SYSTEM_MESSAGE,
+ label: 'System Message'
+ },
+ {
+ action: AttachmentAction.MCP_PROMPT_CLICK,
+ enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+ icon: Zap,
+ id: AttachmentMenuItemId.MCP_PROMPT,
+ label: 'MCP Prompt',
+ visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_PROMPTS_SUPPORT
+ }
+];
+
+export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [
+ {
+ action: AttachmentAction.MCP_RESOURCES_CLICK,
+ enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+ icon: FolderOpen,
+ id: AttachmentMenuItemId.MCP_RESOURCES,
+ label: 'MCP Resources',
+ visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_RESOURCES_SUPPORT
+ }
+];
+
+export const ATTACHMENT_TOOLTIP_TEXT = 'Add files, prompts, tools or MCP Servers';
+++ /dev/null
-import { FolderOpen, MessageSquare, Zap } from '@lucide/svelte';
-import { FILE_TYPE_ICONS } from '$lib/constants/icons';
-import {
- AttachmentAction,
- AttachmentItemEnabledWhen,
- AttachmentItemVisibleWhen,
- AttachmentMenuItemId
-} from '$lib/enums';
-import type { Component } from 'svelte';
-
-export interface AttachmentMenuItem {
- /** Unique identifier for the item */
- id: AttachmentMenuItemId;
- /** Display label */
- label: string;
- /** Lucide icon component */
- icon: Component;
- /** Extra CSS class applied to the item (e.g. for test selectors) */
- class?: string;
- /** Whether the item requires a specific modality to be enabled */
- enabledWhen?: AttachmentItemEnabledWhen;
- /** Tooltip shown when the item is disabled */
- disabledTooltip?: string;
- /** Callback key on the Props interface to invoke when clicked */
- action: AttachmentAction;
- /** Whether the item is only shown when a specific capability is present */
- visibleWhen?: AttachmentItemVisibleWhen;
- /** Whether this item has a tooltip even when enabled (uses dynamic text) */
- hasEnabledTooltip?: boolean;
-}
-
-/**
- * File attachment menu items shown in both the desktop dropdown and mobile sheet.
- * The "Tools" submenu is handled separately by each component.
- */
-export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [
- {
- action: AttachmentAction.FILE_UPLOAD,
- class: 'images-button',
- disabledTooltip: 'Image processing requires a vision model',
- enabledWhen: AttachmentItemEnabledWhen.HAS_VISION_MODALITY,
- icon: FILE_TYPE_ICONS.image,
- id: AttachmentMenuItemId.IMAGES,
- label: 'Images'
- },
- {
- action: AttachmentAction.FILE_UPLOAD,
- class: 'audio-button',
- disabledTooltip: 'Audio files processing requires an audio model',
- enabledWhen: AttachmentItemEnabledWhen.HAS_AUDIO_MODALITY,
- icon: FILE_TYPE_ICONS.audio,
- id: AttachmentMenuItemId.AUDIO,
- label: 'Audio Files'
- },
- {
- action: AttachmentAction.FILE_UPLOAD,
- class: 'video-button',
- disabledTooltip: 'Video files processing requires a video model',
- enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
- icon: FILE_TYPE_ICONS.video,
- id: AttachmentMenuItemId.VIDEO,
- label: 'Video Files'
- },
- {
- action: AttachmentAction.FILE_UPLOAD,
- enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
- icon: FILE_TYPE_ICONS.text,
- id: AttachmentMenuItemId.TEXT,
- label: 'Text Files'
- },
- {
- action: AttachmentAction.FILE_UPLOAD,
- disabledTooltip: 'PDFs will be converted to text. Image-based PDFs may not work properly.',
- enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
- hasEnabledTooltip: true,
- icon: FILE_TYPE_ICONS.pdf,
- id: AttachmentMenuItemId.PDF,
- label: 'PDF Files'
- }
-];
-
-export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [];
-
-export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [
- {
- action: AttachmentAction.SYSTEM_PROMPT_CLICK,
- enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
- hasEnabledTooltip: true,
- icon: MessageSquare,
- id: AttachmentMenuItemId.SYSTEM_MESSAGE,
- label: 'System Message'
- },
- {
- action: AttachmentAction.MCP_PROMPT_CLICK,
- enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
- icon: Zap,
- id: AttachmentMenuItemId.MCP_PROMPT,
- label: 'MCP Prompt',
- visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_PROMPTS_SUPPORT
- }
-];
-
-export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [
- {
- action: AttachmentAction.MCP_RESOURCES_CLICK,
- enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
- icon: FolderOpen,
- id: AttachmentMenuItemId.MCP_RESOURCES,
- label: 'MCP Resources',
- visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_RESOURCES_SUPPORT
- }
-];
-
-export const ATTACHMENT_TOOLTIP_TEXT = 'Add files, prompts, tools or MCP Servers';
--- /dev/null
+export const AUTO_SCROLL_INTERVAL = 100;
+// Conversation landing: the page keeps growing after the first bottom pin
+// without DOM mutations (content-visibility size realizations, syntax
+// highlight passes), so the pin repeats every frame until the height holds
+// for this many consecutive frames, bounded by the time cap below.
+export const LANDING_STABLE_FRAMES = 10;
+export const LANDING_SETTLE_MAX_MS = 1000;
+// Chat main view: tight threshold because scroll-here events come from
+// discrete assistant-message appends.
+export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
+// Reasoning block: stickier because reasoning fires many small
+// incremental DOM writes that easily drift a few pixels off bottom.
+export const REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
+// Syntax-highlighted code: stickier than the chat main view because line
+// wrap reflows while the highlight.js pass settles can drift a few pixels
+// off bottom.
+export const SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX = 32;
+// Streaming tool output (e.g. exec_shell_command): shell commands produce
+// lots of small line writes and the exit-code line appended at the tail
+// past the last user-visible frame is what triggers DOM drift, so use a
+// threshold generous enough to capture that tail flush.
+export const TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
+++ /dev/null
-export const AUTO_SCROLL_INTERVAL = 100;
-// Conversation landing: the page keeps growing after the first bottom pin
-// without DOM mutations (content-visibility size realizations, syntax
-// highlight passes), so the pin repeats every frame until the height holds
-// for this many consecutive frames, bounded by the time cap below.
-export const LANDING_STABLE_FRAMES = 10;
-export const LANDING_SETTLE_MAX_MS = 1000;
-// Chat main view: tight threshold because scroll-here events come from
-// discrete assistant-message appends.
-export const AUTO_SCROLL_AT_BOTTOM_THRESHOLD = 10;
-// Reasoning block: stickier because reasoning fires many small
-// incremental DOM writes that easily drift a few pixels off bottom.
-export const REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
-// Syntax-highlighted code: stickier than the chat main view because line
-// wrap reflows while the highlight.js pass settles can drift a few pixels
-// off bottom.
-export const SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX = 32;
-// Streaming tool output (e.g. exec_shell_command): shell commands produce
-// lots of small line writes and the exit-code line appended at the tail
-// past the last user-visible frame is what triggers DOM drift, so use a
-// threshold generous enough to capture that tail flush.
-export const TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX = 64;
--- /dev/null
+import type { BinaryDetectionOptions } from '$lib/types';
+
+export const DEFAULT_BINARY_DETECTION_OPTIONS: BinaryDetectionOptions = {
+ maxAbsoluteNullBytes: 2,
+ prefixLength: 1024 * 10, // Check the first 10KB of the string
+ suspiciousCharThresholdRatio: 0.15 // Allow up to 15% suspicious chars
+};
+++ /dev/null
-import type { BinaryDetectionOptions } from '$lib/types';
-
-export const DEFAULT_BINARY_DETECTION_OPTIONS: BinaryDetectionOptions = {
- maxAbsoluteNullBytes: 2,
- prefixLength: 1024 * 10, // Check the first 10KB of the string
- suspiciousCharThresholdRatio: 0.15 // Allow up to 15% suspicious chars
-};
--- /dev/null
+// 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.BUILTIN },
+ [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;
+++ /dev/null
-// 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 { Component } from 'svelte';
-
-export interface BuiltinToolUiEntry {
- icon: Component;
- label: string;
- source: ToolSource.BUILTIN | ToolSource.FRONTEND;
-}
-
-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.BUILTIN },
- [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;
-
-export function getBuiltinToolUi(toolName: string | undefined): BuiltinToolUiEntry | null {
- if (!toolName) return null;
-
- return (BUILTIN_TOOL_UI as Record<string, BuiltinToolUiEntry>)[toolName] ?? null;
-}
--- /dev/null
+/**
+ * Cache configuration constants
+ */
+
+/**
+ * Default cache limits when no per-cache overrides are given.
+ */
+export const CACHE = {
+ /** Default maximum number of entries in a cache */
+ DEFAULT_MAX_ENTRIES: 100,
+ /** Default TTL (Time-To-Live) for cache entries in milliseconds (5 minutes) */
+ DEFAULT_TTL_MS: 5 * 60 * 1000
+} as const;
+
+/**
+ * TTL and size for the model props cache.
+ * Props don't change frequently, so we can cache them longer.
+ */
+export const MODEL_PROPS_CACHE = {
+ /** Maximum number of model props to cache */
+ MAX_ENTRIES: 50,
+ /** TTL for model props cache entries in milliseconds (10 minutes) */
+ TTL_MS: 10 * 60 * 1000
+} as const;
+
+/**
+ * TTL and size for the MCP resource cache.
+ */
+export const MCP_RESOURCE_CACHE = {
+ /** Maximum number of MCP resources to cache */
+ MAX_ENTRIES: 50,
+ /** TTL for MCP resource cache entries in milliseconds (5 minutes) */
+ TTL_MS: 5 * 60 * 1000
+} as const;
+
+/**
+ * Limits for pruning inactive conversation states held in memory.
+ */
+export const INACTIVE_CONVERSATION = {
+ /** Maximum age (in ms) for inactive conversation states before cleanup (30 minutes) */
+ MAX_AGE_MS: 30 * 60 * 1000,
+ /** Maximum number of inactive conversation states to keep in memory */
+ MAX_STATES: 10
+} as const;
+++ /dev/null
-/**
- * Cache configuration constants
- */
-
-/**
- * Default TTL (Time-To-Live) for cache entries in milliseconds
- * @default 5 minutes
- */
-export const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;
-
-/**
- * Default maximum number of entries in a cache
- * @default 100
- */
-export const DEFAULT_CACHE_MAX_ENTRIES = 100;
-
-/**
- * TTL for model props cache in milliseconds
- * Props don't change frequently, so we can cache them longer
- * @default 10 minutes
- */
-export const MODEL_PROPS_CACHE_TTL_MS = 10 * 60 * 1000;
-
-/**
- * Maximum number of model props to cache
- * @default 50
- */
-export const MODEL_PROPS_CACHE_MAX_ENTRIES = 50;
-
-/**
- * Maximum number of MCP resources to cache
- * @default 50
- */
-export const MCP_RESOURCE_CACHE_MAX_ENTRIES = 50;
-
-/**
- * TTL for MCP resource cache entries in milliseconds
- * @default 5 minutes
- */
-export const MCP_RESOURCE_CACHE_TTL_MS = 5 * 60 * 1000;
-
-/**
- * Maximum number of inactive conversation states to keep in memory
- * States for conversations beyond this limit will be cleaned up
- * @default 10
- */
-export const MAX_INACTIVE_CONVERSATION_STATES = 10;
-
-/**
- * Maximum age (in ms) for inactive conversation states before cleanup
- * States older than this will be removed during cleanup
- * @default 30 minutes
- */
-export const INACTIVE_CONVERSATION_STATE_MAX_AGE_MS = 30 * 60 * 1000;
+++ /dev/null
-import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants/working-directory';
-import { ChatFormCommandAction } from '$lib/enums';
-import type { ChatFormCommand } from '$lib/types';
-
-interface ChatCommandsOptions {
- /** Gates `/model`. */
- showModelSelector: boolean;
- /** Gates `/prompt`. */
- hasPrompts: () => boolean;
- /** Gates `/cwd`. */
- hasCwdTools: () => boolean;
-}
-
-/**
- * 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/constants` barrel, and importing
- * stores at module load would create a circular dependency (the stores
- * themselves import from `$lib/constants`).
- */
-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'
- }
- ];
-}
--- /dev/null
+export const INITIAL_FILE_SIZE = 0;
+export const PROMPT_CONTENT_SEPARATOR = '\n\n';
+export const CLIPBOARD_CONTENT_QUOTE_PREFIX = '"';
+export const PROMPT_TRIGGER_PREFIX = '/';
+export const NEW_CHAT_DRAFT_KEY = '__new_chat__';
+++ /dev/null
-export const INITIAL_FILE_SIZE = 0;
-export const PROMPT_CONTENT_SEPARATOR = '\n\n';
-export const CLIPBOARD_CONTENT_QUOTE_PREFIX = '"';
-export const PROMPT_TRIGGER_PREFIX = '/';
-export const NEW_CHAT_DRAFT_KEY = '__new_chat__';
--- /dev/null
+export const CLI_FLAGS = {
+ API_KEY: '--api-key',
+ MCP_PROXY: '--ui-mcp-proxy',
+ SLOTS: '--slots',
+ TOOLS: '--tools'
+} as const;
+++ /dev/null
-export const CLI_FLAGS = {
- API_KEY: '--api-key',
- MCP_PROXY: '--ui-mcp-proxy',
- SLOTS: '--slots',
- TOOLS: '--tools'
-} as const;
--- /dev/null
+// Constants for the markdown code-block renderer: language/fence handling and CSS classes.
+
+/** Parsing and escaping helpers for the markdown code-block renderer. */
+export const CODE_BLOCK = {
+ AMPERSAND_REGEX: /&/g,
+ /** Language fallback used when no language is specified. */
+ DEFAULT_LANGUAGE: 'text',
+ /** Matches opening/closing markdown code fences. */
+ FENCE_PATTERN: /^```|\n```/g,
+ GT_REGEX: />/g,
+ /** Matches the language specifier at the start of a code fence. */
+ LANG_PATTERN: /^(\w*)\n?/,
+ LT_REGEX: /</g,
+
+ // Matches the `text:` prefix that file-type identifiers use to denote a
+ // plain-text language (e.g. `text:typescript`). Used by tool-call renderers
+ // to recover the underlying highlight.js language.
+ TEXT_LANGUAGE_PREFIX_REGEX: /^text:/,
+ // Whitespace-only empty lines (between start of string and first non-empty line).
+ // Used by trimCodePadding to drop leading/trailing phantom blank rows from LLM
+ // payload wrappers without touching internal blank lines.
+ TRIM_LEADING_PADDING_REGEX: /^(?:[ \t]*\n)+/,
+
+ TRIM_TRAILING_PADDING_REGEX: /(?:\n[ \t]*)+$/
+} as const;
+
+// Matches either Unix or Windows path separators so `String.split(REGEX)` can
+// recover the trailing file-name segment from either `/foo/bar.txt` or
+// `C:\foo\bar.txt`. Used wherever a parameter accepts a user-supplied path.
+export const FILE_PATH_SEPARATOR_REGEX = /[\\/]/;
+
+// Separates a file name from its extension, e.g. the '.' in `cover.png`.
+export const FILE_EXTENSION_SEPARATOR = '.';
+
+// Matches the `text:` prefix that file-type identifiers use to denote a
+// plain-text language (e.g. `text:typescript`). Used by tool-call renderers
+// to recover the underlying highlight.js language.
+export const TEXT_LANGUAGE_PREFIX_REGEX = /^text:/;
+
+/** CSS classes applied by the markdown code-block renderer. */
+export const CODE_BLOCK_CLASS = {
+ ACTIONS: 'code-block-actions',
+ COPY_BTN: 'copy-code-btn',
+ HEADER: 'code-block-header',
+ LANGUAGE: 'code-language',
+ PREVIEW_BTN: 'preview-code-btn',
+ RELATIVE: 'relative',
+ SCROLL_CONTAINER: 'code-block-scroll-container',
+ WRAPPER: 'code-block-wrapper'
+} as const;
+++ /dev/null
-export const CODE_BLOCK_SCROLL_CONTAINER_CLASS = 'code-block-scroll-container';
-export const CODE_BLOCK_WRAPPER_CLASS = 'code-block-wrapper';
-export const CODE_BLOCK_HEADER_CLASS = 'code-block-header';
-export const CODE_BLOCK_ACTIONS_CLASS = 'code-block-actions';
-export const CODE_LANGUAGE_CLASS = 'code-language';
-export const COPY_CODE_BTN_CLASS = 'copy-code-btn';
-export const PREVIEW_CODE_BTN_CLASS = 'preview-code-btn';
-export const RELATIVE_CLASS = 'relative';
+++ /dev/null
-export const NEWLINE = '\n';
-export const TAB = '\t';
-export const DEFAULT_LANGUAGE = 'text';
-export const LANG_PATTERN = /^(\w*)\n?/;
-export const AMPERSAND_REGEX = /&/g;
-export const LT_REGEX = /</g;
-export const GT_REGEX = />/g;
-export const FENCE_PATTERN = /^```|\n```/g;
-
-// Whitespace-only empty lines (between start of string and first non-empty line).
-// Used by trimCodePadding to drop leading/trailing phantom blank rows from LLM
-// payload wrappers without touching internal blank lines.
-export const TRIM_LEADING_PADDING_REGEX = /^(?:[ \t]*\n)+/;
-export const TRIM_TRAILING_PADDING_REGEX = /(?:\n[ \t]*)+$/;
-
-// Matches either Unix or Windows path separators so `String.split(REGEX)` can
-// recover the trailing file-name segment from either `/foo/bar.txt` or
-// `C:\foo\bar.txt`. Used wherever a parameter accepts a user-supplied path.
-export const FILE_PATH_SEPARATOR_REGEX = /[\\/]/;
-
-// Separates a file name from its extension, e.g. the '.' in `cover.png`.
-export const FILE_EXTENSION_SEPARATOR = '.';
-
-// Matches the `text:` prefix that file-type identifiers use to denote a
-// plain-text language (e.g. `text:typescript`). Used by tool-call renderers
-// to recover the underlying highlight.js language.
-export const TEXT_LANGUAGE_PREFIX_REGEX = /^text:/;
--- /dev/null
+/**
+ * String patterns for detecting content kind from MIME types and URIs.
+ * Used with startsWith/includes checks, not as discriminated values.
+ */
+
+export const MIME_TYPE_PREFIXES = {
+ IMAGE: 'image/',
+ TEXT: 'text'
+} as const;
+
+export const MIME_TYPE_SUBSTRINGS = {
+ JAVASCRIPT: 'javascript',
+ JSON: 'json',
+ TYPESCRIPT: 'typescript'
+} as const;
+
+export const URI_PATTERNS = {
+ DATABASE_KEYWORD: 'database',
+ DATABASE_SCHEME: 'db://'
+} as const;
--- /dev/null
+// Half of the card width, matching the w-64 class on the card.
+export const CONTEXT_GAUGE_CARD_HALF_WIDTH_PX = 128;
+// Minimum distance kept between the card and the form edges.
+export const CONTEXT_GAUGE_EDGE_MARGIN_PX = 8;
+// Gap between the top of the dial and the bottom edge of the card.
+export const CONTEXT_GAUGE_DIAL_GAP_PX = 8;
+// Grace delay before closing, letting the pointer travel from dial to card.
+export const CONTEXT_GAUGE_CLOSE_GRACE_MS = 150;
+++ /dev/null
-// Half of the card width, matching the w-64 class on the card.
-export const CONTEXT_GAUGE_CARD_HALF_WIDTH_PX = 128;
-// Minimum distance kept between the card and the form edges.
-export const CONTEXT_GAUGE_EDGE_MARGIN_PX = 8;
-// Gap between the top of the dial and the bottom edge of the card.
-export const CONTEXT_GAUGE_DIAL_GAP_PX = 8;
-// Grace delay before closing, letting the pointer travel from dial to card.
-export const CONTEXT_GAUGE_CLOSE_GRACE_MS = 150;
--- /dev/null
+export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
+export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
+export const CONTEXT_KEY_CHAT_SETTINGS_CONFIG = 'chat-settings-config';
+++ /dev/null
-export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit';
-export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions';
-export const CONTEXT_KEY_CHAT_SETTINGS_CONFIG = 'chat-settings-config';
--- /dev/null
+// actions accepted by the realtime inference control endpoint (API_CHAT.CONTROL)
+// kept separate from the endpoint paths since these are protocol level verbs
+export const CONTROL_ACTION = {
+ END_REASONING: 'reasoning_end'
+} as const;
+++ /dev/null
-// actions accepted by the realtime inference control endpoint (API_CHAT.CONTROL)
-// kept separate from the endpoint paths since these are protocol level verbs
-export const CONTROL_ACTION = {
- END_REASONING: 'reasoning_end'
-} as const;
-
-export type ControlAction = (typeof CONTROL_ACTION)[keyof typeof CONTROL_ACTION];
--- /dev/null
+// First bytes of every ZIP local file header ("PK"). Import detects an archive
+// from these bytes rather than from the filename, which the OS may not preserve.
+export const ZIP_MAGIC = [0x50, 0x4b];
+++ /dev/null
-// First bytes of every ZIP local file header ("PK"). Import detects an archive
-// from these bytes rather than from the filename, which the OS may not preserve.
-export const ZIP_MAGIC = [0x50, 0x4b];
--- /dev/null
+export const BOX_BORDER =
+ 'border border-border/30 focus-within:border-border dark:border-border/20 dark:focus-within:border-border';
+
+export const INPUT_CLASSES = `
+ bg-muted/60 dark:bg-muted/75
+ ${BOX_BORDER}
+ shadow-sm
+ outline-none
+ text-foreground
+`;
+
+export const PANEL_CLASSES = `
+ bg-background
+ border border-border/30 dark:border-border/20
+ shadow-sm backdrop-blur-lg!
+ rounded-t-lg!
+`;
+
+export const CHAT_FORM_POPOVER_MAX_HEIGHT = 'max-h-80';
+export const DIALOG_SUBMENU_CONTENT = 'w-60';
+
+/** Selects the focused chat-form input (either renderer) to restore focus after model actions. */
+export const CHAT_INPUT_FOCUS_SELECTOR =
+ '[data-slot="input-area"] textarea, [data-slot="input-area"] [contenteditable="true"]';
+
+/** Default Tailwind size class for inline icon components (lucide, etc.). */
+export const ICON_CLASS_DEFAULT = 'h-4 w-4';
+
+/** Icon size + spinning animation; used for live-streaming tool indicators. */
+export const ICON_CLASS_SPIN = 'h-4 w-4 animate-spin';
+++ /dev/null
-export const BOX_BORDER =
- 'border border-border/30 focus-within:border-border dark:border-border/20 dark:focus-within:border-border';
-
-export const INPUT_CLASSES = `
- bg-muted/60 dark:bg-muted/75
- ${BOX_BORDER}
- shadow-sm
- outline-none
- text-foreground
-`;
-
-export const PANEL_CLASSES = `
- bg-background
- border border-border/30 dark:border-border/20
- shadow-sm backdrop-blur-lg!
- rounded-t-lg!
-`;
-
-export const CHAT_FORM_POPOVER_MAX_HEIGHT = 'max-h-80';
-export const DIALOG_SUBMENU_CONTENT = 'w-60';
-
-/** Selects the focused chat-form input (either renderer) to restore focus after model actions. */
-export const CHAT_INPUT_FOCUS_SELECTOR =
- '[data-slot="input-area"] textarea, [data-slot="input-area"] [contenteditable="true"]';
-
-/** Default Tailwind size class for inline icon components (lucide, etc.). */
-export const ICON_CLASS_DEFAULT = 'h-4 w-4';
-
-/** Icon size + spinning animation; used for live-streaming tool indicators. */
-export const ICON_CLASS_SPIN = 'h-4 w-4 animate-spin';
--- /dev/null
+/**
+ * Database-related constants (IndexedDB, Dexie).
+ *
+ * Centralized to ensure consistency across the app and simplify future
+ * naming changes.
+ */
+
+import { STORAGE_APP_NAME } from './storage.constants';
+
+/** IndexedDB database name */
+export const DB_NAME = STORAGE_APP_NAME;
+
+/** IndexedDB store / table names */
+export const IDXDB_TABLES = {
+ conversations: 'conversations',
+ messages: 'messages'
+} as const;
+
+/** IndexedDB store schemas */
+export const IDXDB_STORE_SCHEMAS = {
+ conversations: 'id, lastModified, currNode, name',
+ messages: 'id, convId, type, role, timestamp, parent, children'
+} as const;
+
+/** Combined Dexie stores definition — keys are table names, values are schemas */
+export const IDXDB_STORES = {
+ [IDXDB_TABLES.conversations]: IDXDB_STORE_SCHEMAS.conversations,
+ [IDXDB_TABLES.messages]: IDXDB_STORE_SCHEMAS.messages
+} as const;
+++ /dev/null
-/**
- * Database-related constants (IndexedDB, Dexie).
- *
- * Centralized to ensure consistency across the app and simplify future
- * naming changes.
- */
-
-import { STORAGE_APP_NAME } from './storage';
-
-/** IndexedDB database name */
-export const DB_NAME = STORAGE_APP_NAME;
-
-/** IndexedDB store / table names */
-export const IDXDB_TABLES = {
- conversations: 'conversations',
- messages: 'messages'
-} as const;
-
-/** IndexedDB store schemas */
-export const IDXDB_STORE_SCHEMAS = {
- conversations: 'id, lastModified, currNode, name',
- messages: 'id, convId, type, role, timestamp, parent, children'
-} as const;
-
-/** Combined Dexie stores definition — keys are table names, values are schemas */
-export const IDXDB_STORES = {
- [IDXDB_TABLES.conversations]: IDXDB_STORE_SCHEMAS.conversations,
- [IDXDB_TABLES.messages]: IDXDB_STORE_SCHEMAS.messages
-} as const;
--- /dev/null
+// Shared constants for diagram blocks (mermaid and svg) that toggle between a
+// rendered view and a source view. The wrapper carries the active mode, css
+// drives the visibility, the click handler only flips the attribute.
+
+export const DIAGRAM_VIEW_MODE_ATTR = 'data-view-mode';
+export const DIAGRAM_VIEW_RENDERED = 'rendered';
+export const DIAGRAM_VIEW_SOURCE = 'source';
+export const DIAGRAM_SOURCE_CLASS = 'diagram-source';
+export const TOGGLE_SOURCE_BTN_CLASS = 'toggle-source-btn';
+++ /dev/null
-// Shared constants for diagram blocks (mermaid and svg) that toggle between a
-// rendered view and a source view. The wrapper carries the active mode, css
-// drives the visibility, the click handler only flips the attribute.
-
-export const DIAGRAM_VIEW_MODE_ATTR = 'data-view-mode';
-export const DIAGRAM_VIEW_RENDERED = 'rendered';
-export const DIAGRAM_VIEW_SOURCE = 'source';
-export const DIAGRAM_SOURCE_CLASS = 'diagram-source';
-export const TOGGLE_SOURCE_BTN_CLASS = 'toggle-source-btn';
--- /dev/null
+export const ERROR_MESSAGES = {
+ HTTP: {
+ ACCESS_DENIED: 'Access denied',
+ GENERIC: 'Request failed',
+ INTERNAL_ERROR: 'Server error - check server logs',
+ NOT_FOUND: 'Not found',
+ TEMPORARILY_UNAVAILABLE: 'Server temporarily unavailable'
+ },
+ NETWORK: {
+ GENERIC: 'Failed to connect to server',
+ NXDOMAIN: 'Server not found - check server address',
+ REFUSED: 'Connection refused - server may be offline',
+ TIMEOUT: 'Request timed out',
+ UNREACHABLE: 'Server is not running or unreachable'
+ }
+};
+
+export const HTTP_CODE_TO_STRING: Record<string, string> = {
+ 401: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
+ 403: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
+ 500: ERROR_MESSAGES.HTTP.INTERNAL_ERROR,
+ 503: ERROR_MESSAGES.HTTP.TEMPORARILY_UNAVAILABLE
+};
+++ /dev/null
-export const ERROR_MESSAGES = {
- HTTP: {
- ACCESS_DENIED: 'Access denied',
- GENERIC: 'Request failed',
- INTERNAL_ERROR: 'Server error - check server logs',
- NOT_FOUND: 'Not found',
- TEMPORARILY_UNAVAILABLE: 'Server temporarily unavailable'
- },
- NETWORK: {
- GENERIC: 'Failed to connect to server',
- NXDOMAIN: 'Server not found - check server address',
- REFUSED: 'Connection refused - server may be offline',
- TIMEOUT: 'Request timed out',
- UNREACHABLE: 'Server is not running or unreachable'
- }
-};
-
-export const HTTP_CODE_TO_STRING: Record<string, string> = {
- 401: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
- 403: ERROR_MESSAGES.HTTP.ACCESS_DENIED,
- 500: ERROR_MESSAGES.HTTP.INTERNAL_ERROR,
- 503: ERROR_MESSAGES.HTTP.TEMPORARILY_UNAVAILABLE
-};
+++ /dev/null
-export const VIEWPORT_GUTTER = 8;
-export const MENU_OFFSET = 6;
--- /dev/null
+export const MS_PER_SECOND = 1000;
+export const SECONDS_PER_MINUTE = 60;
+export const SECONDS_PER_HOUR = 3600;
+export const SHORT_DURATION_THRESHOLD = 1;
+export const MEDIUM_DURATION_THRESHOLD = 10;
+
+/** Default display value when no performance time is available */
+export const DEFAULT_PERFORMANCE_TIME = '0s';
+++ /dev/null
-export const MS_PER_SECOND = 1000;
-export const SECONDS_PER_MINUTE = 60;
-export const SECONDS_PER_HOUR = 3600;
-export const SHORT_DURATION_THRESHOLD = 1;
-export const MEDIUM_DURATION_THRESHOLD = 10;
-
-/** Default display value when no performance time is available */
-export const DEFAULT_PERFORMANCE_TIME = '0s';
--- /dev/null
+/** HTTP header handling for API and MCP requests. */
+export const HEADERS = {
+ /** Canonical casing for the Authorization header (RFC 7235) */
+ AUTHORIZATION: 'Authorization',
+ /** Bearer scheme prefix used for Authorization headers (RFC 6750) */
+ BEARER: 'Bearer ',
+ /** Content-Type HTTP header name */
+ CONTENT_TYPE: 'Content-Type',
+ /** Partial-redaction rules for MCP headers: header name -> visible trailing chars */
+ PARTIAL_REDACT: new Map<string, number>([['mcp-session-id', 5]]),
+
+ /** Header names whose values should be redacted in diagnostic logs */
+ REDACTED: new Set([
+ 'authorization',
+ 'api-key',
+ 'cookie',
+ 'mcp-session-id',
+ 'proxy-authorization',
+ 'set-cookie',
+ 'x-auth-token',
+ 'x-api-key'
+ ]),
+
+ /** Header carrying the stream-session identity (conversation id, optionally with a model suffix) */
+ X_CONVERSATION_ID_HEADER: 'X-Conversation-Id',
+
+ /** Header asking the server to encode a tool's output differently, e.g. read_file returning base64. */
+ X_RESP_TYPE_HEADER: 'x-resp-type',
+
+ /** Header carrying the working directory a tool call runs in; the model cannot override it */
+ X_TOOL_CWD_HEADER: 'x-tool-cwd'
+};
+
+/** `X_RESP_TYPE_HEADER` value that makes read_file return raw bytes as base64 instead of text. */
+export const RESP_TYPE_BASE64 = 'base64';
--- /dev/null
+/**
+ * Icon mappings for file types and model modalities
+ * Centralized configuration to ensure consistent icon usage across the app
+ */
+
+import {
+ Eye as VisionIcon,
+ File as FileIcon,
+ FileText as FileTextIcon,
+ Image as ImageIcon,
+ Mic as AudioIcon,
+ Video as VideoIcon
+} from '@lucide/svelte';
+import { FileTypeCategory, ModelModality } from '$lib/enums';
+
+export const FILE_TYPE_ICONS = {
+ [FileTypeCategory.AUDIO]: AudioIcon,
+ [FileTypeCategory.IMAGE]: ImageIcon,
+ [FileTypeCategory.PDF]: FileIcon,
+ [FileTypeCategory.TEXT]: FileTextIcon,
+ [FileTypeCategory.VIDEO]: VideoIcon
+} as const;
+
+export const DEFAULT_FILE_ICON = FileIcon;
+
+export const MODALITY_ICONS = {
+ [ModelModality.AUDIO]: AudioIcon,
+ [ModelModality.VIDEO]: VideoIcon,
+ [ModelModality.VISION]: VisionIcon
+} as const;
+
+export const MODALITY_LABELS = {
+ [ModelModality.AUDIO]: 'Audio',
+ [ModelModality.VIDEO]: 'Video',
+ [ModelModality.VISION]: 'Vision'
+} as const;
+
+// Shared SVG icon strings for copy and preview buttons
+export const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
+
+export const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
+
+export const CODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code lucide-code-icon"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>`;
+++ /dev/null
-/**
- * Icon mappings for file types and model modalities
- * Centralized configuration to ensure consistent icon usage across the app
- */
-
-import {
- Eye as VisionIcon,
- File as FileIcon,
- FileText as FileTextIcon,
- Image as ImageIcon,
- Mic as AudioIcon,
- Video as VideoIcon
-} from '@lucide/svelte';
-import { FileTypeCategory, ModelModality } from '$lib/enums';
-
-export const FILE_TYPE_ICONS = {
- [FileTypeCategory.AUDIO]: AudioIcon,
- [FileTypeCategory.IMAGE]: ImageIcon,
- [FileTypeCategory.PDF]: FileIcon,
- [FileTypeCategory.TEXT]: FileTextIcon,
- [FileTypeCategory.VIDEO]: VideoIcon
-} as const;
-
-export const DEFAULT_FILE_ICON = FileIcon;
-
-export const MODALITY_ICONS = {
- [ModelModality.AUDIO]: AudioIcon,
- [ModelModality.VIDEO]: VideoIcon,
- [ModelModality.VISION]: VisionIcon
-} as const;
-
-export const MODALITY_LABELS = {
- [ModelModality.AUDIO]: 'Audio',
- [ModelModality.VIDEO]: 'Video',
- [ModelModality.VISION]: 'Vision'
-} as const;
-
-// Shared SVG icon strings for copy and preview buttons
-export const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
-
-export const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
-
-export const CODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code lucide-code-icon"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>`;
+++ /dev/null
-export const MEGAPIXELS_TO_PIXELS = 1_000_000;
-
-export const HEIC_JPEG_QUALITY = 0.85;
--- /dev/null
+/** Image handling constants */
+
+export const IMAGE = {
+ /** JPEG quality used when transcoding HEIC images. */
+ HEIC_JPEG_QUALITY: 0.85,
+ /** Unit conversion: pixels per megapixel. */
+ MEGAPIXELS_TO_PIXELS: 1_000_000
+} as const;
+
+/**
+ * JPEG and EXIF binary format constants for orientation parsing.
+ */
+export const EXIF = {
+ /** APP1 segment marker byte, carries the EXIF payload */
+ APP1_MARKER: 0xe1,
+ /** "Exif" signature opening the APP1 payload, big endian uint32 */
+ EXIF_SIGNATURE: 0x45786966,
+ /** Size in bytes of one IFD directory entry */
+ IFD_ENTRY_SIZE: 12,
+ /** JPEG start of image marker */
+ JPEG_SOI_MARKER: 0xffd8,
+ /** EXIF tag id holding the orientation value */
+ ORIENTATION_TAG: 0x0112,
+ /** Bytes of file prefix to scan, the APP1 EXIF segment sits near the start */
+ SCAN_BYTE_LIMIT: 128 * 1024,
+ /** Start of scan marker byte, compressed data begins and no EXIF follows */
+ SOS_MARKER: 0xda,
+ /** TIFF byte order mark for little endian ("II") */
+ TIFF_LITTLE_ENDIAN: 0x4949,
+ /** TIFF magic number following the byte order mark */
+ TIFF_MAGIC: 42
+} as const;
// Central constants export file
// All constants should be imported from '$lib/constants'
-export * from './agentic';
-export * from './api-endpoints';
-export * from './app';
-export * from './attachment-labels';
-export * from './database';
-export * from './reasoning-effort';
-export * from './reasoning-effort-tokens';
-export * from './recommended-mcp-servers';
-export * from './storage';
-export * from './attachment-menu';
-export * from './auto-scroll';
-export * from './context-gauge-popup';
-export * from './conversation-import';
-export * from './binary-detection';
-export * from './built-in-tools';
-export * from './cache';
-export * from './chat-form';
-export * from './chat-commands';
-export * from './cli-flags';
-export * from './code-blocks';
-export * from './icons';
-export * from './code';
-export * from './context-keys';
-export * from './control-actions';
-export * from './css-classes';
-export * from './floating-ui-constraints';
-export * from './formatters';
-export * from './key-value-pairs';
-export * from './icons';
-export * from './latex-protection';
-export * from './literal-html';
-export * from './markdown';
-export * from './mermaid-blocks';
-export * from './svg-blocks';
-export * from './diagram-blocks';
-export * from './max-bundle-size';
-export * from './mcp';
-export * from './mcp-form';
-export * from './mcp-resource';
-export * from './mention-badge';
-export * from './message-export';
-export * from './path-display';
-export * from './model-id';
-export * from './model-loading';
-export * from './sse';
-export * from './precision';
-export * from './processing-info';
-export * from './pwa';
+export * from './agentic.constants';
+export * from './api-endpoints.constants';
+export * from './app.constants';
+export * from './database.constants';
+export * from './reasoning-effort.constants';
+export * from './recommended-mcp-servers.constants';
+export * from './storage.constants';
+export * from './icons.constants';
+export * from './attachment-menu.constants';
+export * from './auto-scroll.constants';
+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 './cache.constants';
+export * from './chat-form.constants';
+export * from './cli-flags.constants';
+export * from './code-block.constants';
+export * from './context-keys.constants';
+export * from './control-actions.constants';
+export * from './css-classes.constants';
+export * from './formatters.constants';
+export * from './headers.constants';
+export * from './key-value-pairs.constants';
+export * from './latex-protection.constants';
+export * from './literal-html.constants';
+export * from './markdown.constants';
+export * from './mermaid-blocks.constants';
+export * from './svg-blocks.constants';
+export * from './diagram-blocks.constants';
+export * from './max-bundle-size.constants';
+export * from './error.constants';
+export * from './image.constants';
+export * from './mcp.constants';
+export * from './mcp-form.constants';
+export * from './mcp-resource.constants';
+export * from './mention-badge.constants';
+export * from './message-export.constants';
+export * from './path-display.constants';
+export * from './model-id.constants';
+export * from './model-loading.constants';
+export * from './precision.constants';
+export * from './pwa.constants';
+export * from './routes.constants';
+export * from './sandbox.constants';
+export * from './settings-keys.constants';
+export * from './settings-registry.constants';
+export * from './special-characters.constants';
+export * from './stream.constants';
+export * from './supported-file-types.constants';
+export * from './table-html-restorer.constants';
+export * from './title-generation.constants';
+export * from './ui.constants';
+export * from './uri-template.constants';
+export * from './url.constants';
+export * from './working-directory.constants';
export * from './read-media';
-export * from './routes';
-export * from './sandbox';
-export * from './settings-keys';
-export * from './settings-registry';
-export * from './stream';
-export * from './supported-file-types';
-export * from './table-html-restorer';
-export * from './title-generation';
-export * from './tools';
-export * from './tooltip-config';
-export * from './ui';
-export * from './uri-template';
-export * from './url';
-export * from './viewport';
-export * from './working-directory';
+++ /dev/null
-/**
- * JPEG and EXIF binary format constants for orientation parsing.
- */
-
-/** Bytes of file prefix to scan, the APP1 EXIF segment sits near the start */
-export const EXIF_SCAN_BYTE_LIMIT = 128 * 1024;
-
-/** JPEG start of image marker */
-export const JPEG_SOI_MARKER = 0xffd8;
-
-/** APP1 segment marker byte, carries the EXIF payload */
-export const APP1_MARKER = 0xe1;
-
-/** Start of scan marker byte, compressed data begins and no EXIF follows */
-export const SOS_MARKER = 0xda;
-
-/** "Exif" signature opening the APP1 payload, big endian uint32 */
-export const EXIF_SIGNATURE = 0x45786966;
-
-/** TIFF byte order mark for little endian ("II") */
-export const TIFF_LITTLE_ENDIAN = 0x4949;
-
-/** TIFF magic number following the byte order mark */
-export const TIFF_MAGIC = 42;
-
-/** EXIF tag id holding the orientation value */
-export const EXIF_ORIENTATION_TAG = 0x0112;
-
-/** Size in bytes of one IFD directory entry */
-export const IFD_ENTRY_SIZE = 12;
--- /dev/null
+/**
+ * Key-value pair form constraints and sanitization patterns.
+ *
+ * Both regexes target characters dangerous in HTTP-header / env-var contexts:
+ * \x00 – null byte (injection)
+ * \x0A (\n) – LF (HTTP header injection / response splitting)
+ * \x0D (\r) – CR (HTTP header injection / response splitting)
+ * \x01–\x08, \x0B–\x0C, \x0E–\x1F, \x7F – other C0/DEL control chars
+ *
+ * KEY_UNSAFE_RE additionally strips TAB (\x09); values keep TAB because it is
+ * a valid header-value continuation character per RFC 7230.
+ */
+
+export const KEY_VALUE_PAIR_KEY_MAX_LENGTH = 256;
+export const KEY_VALUE_PAIR_VALUE_MAX_LENGTH = 8192;
+
+// eslint-disable-next-line no-control-regex
+export const KEY_VALUE_PAIR_UNSAFE_KEY_RE = /[\x00-\x1F\x7F]/g;
+// eslint-disable-next-line no-control-regex
+export const KEY_VALUE_PAIR_UNSAFE_VALUE_RE = /[\x00-\x08\x0A-\x0D\x0E-\x1F\x7F]/g;
+++ /dev/null
-/**
- * Key-value pair form constraints and sanitization patterns.
- *
- * Both regexes target characters dangerous in HTTP-header / env-var contexts:
- * \x00 – null byte (injection)
- * \x0A (\n) – LF (HTTP header injection / response splitting)
- * \x0D (\r) – CR (HTTP header injection / response splitting)
- * \x01–\x08, \x0B–\x0C, \x0E–\x1F, \x7F – other C0/DEL control chars
- *
- * KEY_UNSAFE_RE additionally strips TAB (\x09); values keep TAB because it is
- * a valid header-value continuation character per RFC 7230.
- */
-
-export const KEY_VALUE_PAIR_KEY_MAX_LENGTH = 256;
-export const KEY_VALUE_PAIR_VALUE_MAX_LENGTH = 8192;
-
-// eslint-disable-next-line no-control-regex
-export const KEY_VALUE_PAIR_UNSAFE_KEY_RE = /[\x00-\x1F\x7F]/g;
-// eslint-disable-next-line no-control-regex
-export const KEY_VALUE_PAIR_UNSAFE_VALUE_RE = /[\x00-\x08\x0A-\x0D\x0E-\x1F\x7F]/g;
--- /dev/null
+/**
+ * Matches common Markdown code blocks to exclude them from further processing (e.g. LaTeX).
+ * - Fenced: ```...```
+ * - Inline: `...` (does NOT support nested backticks or multi-backtick syntax)
+ *
+ * Note: This pattern does not handle advanced cases like:
+ * `` `code with `backticks` `` or \\``...\\``
+ */
+export const CODE_BLOCK_REGEXP = /(```[\s\S]*?```|`[^`\n]+`)/g;
+
+/**
+ * Matches LaTeX math delimiters \(...\) and \[...\] only when not preceded by a backslash (i.e., not escaped),
+ * while also capturing code blocks (```, `...`) so they can be skipped during processing.
+ *
+ * Uses negative lookbehind `(?<!\\)` to avoid matching \\( or \\[.
+ * Using the look‑behind pattern `(?<!\\)` we skip matches
+ * that are preceded by a backslash, e.g.
+ * `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook)
+ * or `\\[4pt]` (LaTeX line-break).
+ *
+ * group 1: code-block
+ * group 2: square-bracket
+ * group 3: round-bracket
+ */
+export const LATEX_MATH_AND_CODE_PATTERN =
+ /(```[\S\s]*?```|`.*?`)|(?<!\\)\\\[([\S\s]*?[^\\])\\]|(?<!\\)\\\((.*?)\\\)/g;
+
+/** Regex to capture the content of a $$...\\\\...$$ block (display-formula with line-break) */
+export const LATEX_LINEBREAK_REGEXP = /\$\$([\s\S]*?\\\\[\s\S]*?)\$\$/;
+
+/**
+ * Matches the unescaped `\[...\]` display-math delimiter and surrounding
+ * context so callers can insert line-breaks around the placeholder or convert
+ * to inline when the formula has a non-empty trailing context (e.g. a table
+ * cell that opens with `\[` and closes with content after `\]`).
+ *
+ * group 1: prefix before `\[`
+ * group 2: formula body
+ * group 3: trailing context after `\]`
+ */
+export const LATEX_DISPLAY_BLOCK_REGEXP = /([\S].*?)\\\[([\s\S]*?)\\\](.*)/g;
+
+/**
+ * Cheap gate for `preprocessLaTeX`. Every transformation it performs is triggered
+ * by a `$` (inline/display math, currency escaping) or a backslash escape
+ * (`\(`, `\[`, `\ce{`, `\pu{`). Text containing neither is returned untouched, so
+ * this lets the caller skip the whole protect/restore pipeline.
+ */
+export const LATEX_TRIGGER_REGEXP = /[$\\]/;
+
+/** Inline LaTeX math delimiter (the dollar sign). */
+export const LATEX_INLINE_DELIMITER = '$';
+
+/** Display LaTeX math delimiter (paired dollar signs). */
+export const LATEX_DISPLAY_DELIMITER = '$$';
+
+/** Matches a single non-whitespace character. */
+export const LATEX_NON_WHITESPACE_REGEXP = /\S/;
+
+/** Matches a character that may appear adjacent to `$`, indicating a non-TeX
+ * context such as an identifier (`var$`, `$var`), currency ($5), or code. */
+export const LATEX_NEIGHBOR_CHAR_REGEXP = /[A-Za-z0-9_$-]/;
+
+/** Matches a single digit (used to detect currency-like `$5`). */
+export const LATEX_DIGIT_REGEXP = /[0-9]/;
+
+/** Matches the leading blockquote prefix (`> ` or `>`) on a markdown line. */
+export const LATEX_BLOCKQUOTE_PREFIX_REGEXP = /^(>\s*)/;
+
+/** Matches the placeholder inserted by the protect/restore pipeline for a
+ * protected LaTeX expression. Group 1 is the index into `latexExpressions`. */
+export const LATEX_PLACEHOLDER_REGEXP = /<<LATEX_(\d+)>>/g;
+
+/** Matches the placeholder inserted by the protect/restore pipeline for a
+ * protected code block. Group 1 is the index into `codeBlocks`. */
+export const CODE_BLOCK_PLACEHOLDER_REGEXP = /<<CODE_BLOCK_(\d+)>>/g;
+
+/** Matches a `$` immediately followed by a digit, which is treated as a
+ * currency amount (e.g. `$5`) and escaped to `\$5` so it isn't parsed as math. */
+export const LATEX_CURRENCY_DOLLAR_REGEXP = /\$(?=\d)/g;
+
+/** Captures remaining `$$...$$`, `\[...\]`, `\(...\)` (only unescaped via
+ * `(?<!\\)`) after the display-block pass has run. Group 1 holds the
+ * matched formula. */
+export const LATEX_PROTECT_REGEXP =
+ /(\$\$[\s\S]*?\$\$|(?<!\\)\\\[[\s\S]*?\\\]|(?<!\\)\\\(.*?\\\))/g;
+
+/** Matches unescaped inline `\(...\)` (at least one char inside) used to
+ * convert `\(` → `$` after the protect pass. */
+export const LATEX_INLINE_CONVERT_REGEXP = /(?<!\\)\\\((.+?)\\\)/g;
+
+/** Matches unescaped display `\[...\]` used to convert `\[` → `$$`
+ * after the protect pass. */
+export const LATEX_DISPLAY_CONVERT_REGEXP = /(?<!\\)\\\[([\s\S]*?)\\\]/g;
+
+/** `\(` — opens an inline LaTeX math block. */
+export const LATEX_INLINE_OPEN = '\\(';
+
+/** `\)` — closes an inline LaTeX math block. */
+export const LATEX_INLINE_CLOSE = '\\)';
+
+/** `\[` — opens a display LaTeX math block. */
+export const LATEX_DISPLAY_OPEN = '\\[';
+
+/** `\]` — closes a display LaTeX math block. */
+export const LATEX_DISPLAY_CLOSE = '\\]';
+
+/** `\` — the LaTeX escape character. */
+export const LATEX_BACKSLASH = '\\';
+
+/** `\$` — dollar sign escaped so it isn't parsed as math (used to disambiguate
+ * currency amounts like `$5`). */
+export const LATEX_CURRENCY_ESCAPE = '\\$';
+
+/** `\ce{` — mhchem chemistry command prefix. */
+export const LATEX_MHCHEM_CE = '\\ce{';
+
+/** `\pu{` — mhchem physics-unit command prefix. */
+export const LATEX_MHCHEM_PU = '\\pu{';
+
+/** map from mchem-regexp to replacement */
+export const MHCHEM_PATTERN_MAP: readonly [RegExp, string][] = [
+ [/(\s)\$\\ce{/g, '$1$\\\\ce{'],
+ [/(\s)\$\\pu{/g, '$1$\\\\pu{']
+] as const;
+++ /dev/null
-/**
- * Matches common Markdown code blocks to exclude them from further processing (e.g. LaTeX).
- * - Fenced: ```...```
- * - Inline: `...` (does NOT support nested backticks or multi-backtick syntax)
- *
- * Note: This pattern does not handle advanced cases like:
- * `` `code with `backticks` `` or \\``...\\``
- */
-export const CODE_BLOCK_REGEXP = /(```[\s\S]*?```|`[^`\n]+`)/g;
-
-/**
- * Matches LaTeX math delimiters \(...\) and \[...\] only when not preceded by a backslash (i.e., not escaped),
- * while also capturing code blocks (```, `...`) so they can be skipped during processing.
- *
- * Uses negative lookbehind `(?<!\\)` to avoid matching \\( or \\[.
- * Using the look‑behind pattern `(?<!\\)` we skip matches
- * that are preceded by a backslash, e.g.
- * `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook)
- * or `\\[4pt]` (LaTeX line-break).
- *
- * group 1: code-block
- * group 2: square-bracket
- * group 3: round-bracket
- */
-export const LATEX_MATH_AND_CODE_PATTERN =
- /(```[\S\s]*?```|`.*?`)|(?<!\\)\\\[([\S\s]*?[^\\])\\]|(?<!\\)\\\((.*?)\\\)/g;
-
-/** Regex to capture the content of a $$...\\\\...$$ block (display-formula with line-break) */
-export const LATEX_LINEBREAK_REGEXP = /\$\$([\s\S]*?\\\\[\s\S]*?)\$\$/;
-
-/**
- * Matches the unescaped `\[...\]` display-math delimiter and surrounding
- * context so callers can insert line-breaks around the placeholder or convert
- * to inline when the formula has a non-empty trailing context (e.g. a table
- * cell that opens with `\[` and closes with content after `\]`).
- *
- * group 1: prefix before `\[`
- * group 2: formula body
- * group 3: trailing context after `\]`
- */
-export const LATEX_DISPLAY_BLOCK_REGEXP = /([\S].*?)\\\[([\s\S]*?)\\\](.*)/g;
-
-/**
- * Cheap gate for `preprocessLaTeX`. Every transformation it performs is triggered
- * by a `$` (inline/display math, currency escaping) or a backslash escape
- * (`\(`, `\[`, `\ce{`, `\pu{`). Text containing neither is returned untouched, so
- * this lets the caller skip the whole protect/restore pipeline.
- */
-export const LATEX_TRIGGER_REGEXP = /[$\\]/;
-
-/** Inline LaTeX math delimiter (the dollar sign). */
-export const LATEX_INLINE_DELIMITER = '$';
-
-/** Display LaTeX math delimiter (paired dollar signs). */
-export const LATEX_DISPLAY_DELIMITER = '$$';
-
-/** Matches a single non-whitespace character. */
-export const LATEX_NON_WHITESPACE_REGEXP = /\S/;
-
-/** Matches a character that may appear adjacent to `$`, indicating a non-TeX
- * context such as an identifier (`var$`, `$var`), currency ($5), or code. */
-export const LATEX_NEIGHBOR_CHAR_REGEXP = /[A-Za-z0-9_$-]/;
-
-/** Matches a single digit (used to detect currency-like `$5`). */
-export const LATEX_DIGIT_REGEXP = /[0-9]/;
-
-/** Matches the leading blockquote prefix (`> ` or `>`) on a markdown line. */
-export const LATEX_BLOCKQUOTE_PREFIX_REGEXP = /^(>\s*)/;
-
-/** Matches the placeholder inserted by the protect/restore pipeline for a
- * protected LaTeX expression. Group 1 is the index into `latexExpressions`. */
-export const LATEX_PLACEHOLDER_REGEXP = /<<LATEX_(\d+)>>/g;
-
-/** Matches the placeholder inserted by the protect/restore pipeline for a
- * protected code block. Group 1 is the index into `codeBlocks`. */
-export const CODE_BLOCK_PLACEHOLDER_REGEXP = /<<CODE_BLOCK_(\d+)>>/g;
-
-/** Matches a `$` immediately followed by a digit, which is treated as a
- * currency amount (e.g. `$5`) and escaped to `\$5` so it isn't parsed as math. */
-export const LATEX_CURRENCY_DOLLAR_REGEXP = /\$(?=\d)/g;
-
-/** Captures remaining `$$...$$`, `\[...\]`, `\(...\)` (only unescaped via
- * `(?<!\\)`) after the display-block pass has run. Group 1 holds the
- * matched formula. */
-export const LATEX_PROTECT_REGEXP =
- /(\$\$[\s\S]*?\$\$|(?<!\\)\\\[[\s\S]*?\\\]|(?<!\\)\\\(.*?\\\))/g;
-
-/** Matches unescaped inline `\(...\)` (at least one char inside) used to
- * convert `\(` → `$` after the protect pass. */
-export const LATEX_INLINE_CONVERT_REGEXP = /(?<!\\)\\\((.+?)\\\)/g;
-
-/** Matches unescaped display `\[...\]` used to convert `\[` → `$$`
- * after the protect pass. */
-export const LATEX_DISPLAY_CONVERT_REGEXP = /(?<!\\)\\\[([\s\S]*?)\\\]/g;
-
-/** `\(` — opens an inline LaTeX math block. */
-export const LATEX_INLINE_OPEN = '\\(';
-
-/** `\)` — closes an inline LaTeX math block. */
-export const LATEX_INLINE_CLOSE = '\\)';
-
-/** `\[` — opens a display LaTeX math block. */
-export const LATEX_DISPLAY_OPEN = '\\[';
-
-/** `\]` — closes a display LaTeX math block. */
-export const LATEX_DISPLAY_CLOSE = '\\]';
-
-/** `\` — the LaTeX escape character. */
-export const LATEX_BACKSLASH = '\\';
-
-/** `\$` — dollar sign escaped so it isn't parsed as math (used to disambiguate
- * currency amounts like `$5`). */
-export const LATEX_CURRENCY_ESCAPE = '\\$';
-
-/** `\ce{` — mhchem chemistry command prefix. */
-export const LATEX_MHCHEM_CE = '\\ce{';
-
-/** `\pu{` — mhchem physics-unit command prefix. */
-export const LATEX_MHCHEM_PU = '\\pu{';
-
-/** map from mchem-regexp to replacement */
-export const MHCHEM_PATTERN_MAP: readonly [RegExp, string][] = [
- [/(\s)\$\\ce{/g, '$1$\\\\ce{'],
- [/(\s)\$\\pu{/g, '$1$\\\\pu{']
-] as const;
--- /dev/null
+export const PHRASE_PARENTS = new Set([
+ 'paragraph',
+ 'heading',
+ 'emphasis',
+ 'strong',
+ 'delete',
+ 'link',
+ 'linkReference',
+ 'tableCell'
+]);
+++ /dev/null
-export const LINE_BREAK = /\r?\n/;
-
-export const PHRASE_PARENTS = new Set([
- 'paragraph',
- 'heading',
- 'emphasis',
- 'strong',
- 'delete',
- 'link',
- 'linkReference',
- 'tableCell'
-]);
-
-export const NBSP = '\u00a0';
-export const TAB_AS_SPACES = NBSP.repeat(4);
--- /dev/null
+export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])';
+export const DATA_ERROR_BOUND_ATTR = 'errorBound';
+export const DATA_ERROR_HANDLED_ATTR = 'errorHandled';
+export const BOOL_TRUE_STRING = 'true';
+export const BOOL_FALSE_STRING = 'false';
+
+/** Markdown structural markers used by `looksLikeMarkdown`. Inline / line-level. */
+export const MARKDOWN = {
+ ATX_HEADING_REGEX: /^#{1,6}\s+\S/,
+ BLOCKQUOTE_REGEX: /^>\s+\S/,
+ BOLD_REGEX: /\*\*[^*\n]+\*\*|__[^_\n]+__/,
+ CODE_FENCE_REGEX: /^(```|~~~)/m,
+ LINK_REGEX: /\[[^\]\n]+\]\([^)\s]+\)/,
+ LIST_BULLET_REGEX: /^\s*[-*+]\s+\S/,
+ LIST_NUMBERED_REGEX: /^\s*\d+[.)]\s+\S/,
+ TABLE_SEPARATOR_REGEX: /^\s*\|?[\s:|-]+\|?\s*$/
+} as const;
+++ /dev/null
-export const IMAGE_NOT_ERROR_BOUND_SELECTOR = 'img:not([data-error-bound])';
-export const DATA_ERROR_BOUND_ATTR = 'errorBound';
-export const DATA_ERROR_HANDLED_ATTR = 'errorHandled';
-export const BOOL_TRUE_STRING = 'true';
-export const BOOL_FALSE_STRING = 'false';
--- /dev/null
+export const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
+++ /dev/null
-export const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
--- /dev/null
+export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
+export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
+++ /dev/null
-export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
-export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
--- /dev/null
+import { MimeTypeAudio, MimeTypeImage } from '$lib/enums';
+
+// File extension patterns for resource type detection
+export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/i;
+export const CODE_FILE_EXTENSION_REGEX =
+ /\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i;
+export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/i;
+
+// URI protocol prefix pattern
+export const PROTOCOL_PREFIX_REGEX = /^[a-z]+:\/\//;
+
+// File extension regex for display name extraction
+export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
+
+// Separator regex for splitting display names (kebab-case/snake_case)
+export const DISPLAY_NAME_SEPARATOR_REGEX = /[-_]/;
+
+// Regex for matching base64-encoded data URIs
+export const DATA_URI_BASE64_REGEX = /^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/;
+
+// Prefix for MCP attachment filenames
+export const MCP_ATTACHMENT_NAME_PREFIX = 'mcp-attachment';
+
+// Prefix for MCP resource attachment IDs
+export const MCP_RESOURCE_ATTACHMENT_ID_PREFIX = 'res';
+
+// Default file extension for unknown image types
+export const DEFAULT_IMAGE_EXTENSION = 'img';
+
+// Default file extension for unknown audio types
+export const DEFAULT_AUDIO_EXTENSION = 'mp3';
+
+// Default filename for resource content downloads
+export const DEFAULT_RESOURCE_FILENAME = 'resource.txt';
+
+// Path separator for resource URI parsing
+export const PATH_SEPARATOR = '/';
+
+// Separator for joining text content from multiple resource parts
+export const RESOURCE_TEXT_CONTENT_SEPARATOR = '\n\n';
+
+// Fallback text for unknown content types
+export const RESOURCE_UNKNOWN_TYPE = 'unknown type';
+
+// Label prefix for binary blob content
+export const BINARY_CONTENT_LABEL = 'Binary content';
+
+/**
+ * Mapping from image MIME types to file extensions.
+ * Used for generating attachment filenames from MIME types.
+ */
+export const IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
+ [MimeTypeImage.GIF]: 'gif',
+ [MimeTypeImage.JPEG]: 'jpg',
+ [MimeTypeImage.JPG]: 'jpg',
+ [MimeTypeImage.PNG]: 'png',
+ [MimeTypeImage.WEBP]: 'webp'
+} as const;
+
+/**
+ * Mapping from audio MIME types to file extensions.
+ * Used for generating attachment filenames from MIME types.
+ */
+export const AUDIO_MIME_TO_EXTENSION: Record<string, string> = {
+ [MimeTypeAudio.MP3]: 'mp3',
+ [MimeTypeAudio.MP3_MPEG]: 'mp3',
+ [MimeTypeAudio.VND_WAVE]: 'wav',
+ [MimeTypeAudio.WAV]: 'wav',
+ [MimeTypeAudio.WAVE]: 'wav',
+ [MimeTypeAudio.X_PN_WAV]: 'wav',
+ [MimeTypeAudio.X_WAV]: 'wav',
+ [MimeTypeAudio.X_WAVE]: 'wav'
+} as const;
+++ /dev/null
-import { MimeTypeAudio, MimeTypeImage } from '$lib/enums';
-
-// File extension patterns for resource type detection
-export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/i;
-export const CODE_FILE_EXTENSION_REGEX =
- /\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i;
-export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/i;
-
-// URI protocol prefix pattern
-export const PROTOCOL_PREFIX_REGEX = /^[a-z]+:\/\//;
-
-// File extension regex for display name extraction
-export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
-
-// Separator regex for splitting display names (kebab-case/snake_case)
-export const DISPLAY_NAME_SEPARATOR_REGEX = /[-_]/;
-
-// Regex for matching base64-encoded data URIs
-export const DATA_URI_BASE64_REGEX = /^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/;
-
-// Prefix for MCP attachment filenames
-export const MCP_ATTACHMENT_NAME_PREFIX = 'mcp-attachment';
-
-// Prefix for MCP resource attachment IDs
-export const MCP_RESOURCE_ATTACHMENT_ID_PREFIX = 'res';
-
-// Default file extension for unknown image types
-export const DEFAULT_IMAGE_EXTENSION = 'img';
-
-// Default file extension for unknown audio types
-export const DEFAULT_AUDIO_EXTENSION = 'mp3';
-
-// Default filename for resource content downloads
-export const DEFAULT_RESOURCE_FILENAME = 'resource.txt';
-
-// Path separator for resource URI parsing
-export const PATH_SEPARATOR = '/';
-
-// Separator for joining text content from multiple resource parts
-export const RESOURCE_TEXT_CONTENT_SEPARATOR = '\n\n';
-
-// Fallback text for unknown content types
-export const RESOURCE_UNKNOWN_TYPE = 'unknown type';
-
-// Label prefix for binary blob content
-export const BINARY_CONTENT_LABEL = 'Binary content';
-
-/**
- * Mapping from image MIME types to file extensions.
- * Used for generating attachment filenames from MIME types.
- */
-export const IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
- [MimeTypeImage.GIF]: 'gif',
- [MimeTypeImage.JPEG]: 'jpg',
- [MimeTypeImage.JPG]: 'jpg',
- [MimeTypeImage.PNG]: 'png',
- [MimeTypeImage.WEBP]: 'webp'
-} as const;
-
-/**
- * Mapping from audio MIME types to file extensions.
- * Used for generating attachment filenames from MIME types.
- */
-export const AUDIO_MIME_TO_EXTENSION: Record<string, string> = {
- [MimeTypeAudio.MP3]: 'mp3',
- [MimeTypeAudio.MP3_MPEG]: 'mp3',
- [MimeTypeAudio.VND_WAVE]: 'wav',
- [MimeTypeAudio.WAV]: 'wav',
- [MimeTypeAudio.WAVE]: 'wav',
- [MimeTypeAudio.X_PN_WAV]: 'wav',
- [MimeTypeAudio.X_WAV]: 'wav',
- [MimeTypeAudio.X_WAVE]: 'wav'
-} as const;
--- /dev/null
+import { Globe, Radio, Zap } from '@lucide/svelte';
+import { MCPTransportType } from '$lib/enums';
+import { MimeTypeImage } from '$lib/enums/files.enums';
+import type { ClientCapabilities, Implementation } from '$lib/types';
+import type { Component } from 'svelte';
+
+export const DEFAULT_CLIENT_VERSION = '1.0.0';
+export const MCP_CLIENT_NAME = 'llama-ui-mcp';
+export const DEFAULT_IMAGE_MIME_TYPE = MimeTypeImage.PNG;
+
+/** MIME types considered safe for rendering MCP server icons */
+export const MCP_ALLOWED_ICON_MIME_TYPES = new Set([
+ MimeTypeImage.PNG,
+ MimeTypeImage.JPEG,
+ MimeTypeImage.JPG,
+ MimeTypeImage.SVG,
+ MimeTypeImage.WEBP,
+ MimeTypeImage.ICO,
+ MimeTypeImage.ICO_MICROSOFT
+]);
+
+/**
+ * MCP specification version this client targets.
+ * Update when the upstream MCP spec introduces a new stable version:
+ * https://spec.modelcontextprotocol.io/
+ */
+export const MCP_PROTOCOL_VERSION = '2025-06-18';
+
+export const DEFAULT_MCP_CONFIG = {
+ capabilities: { tools: { listChanged: true } } as ClientCapabilities,
+ clientInfo: { name: MCP_CLIENT_NAME, version: DEFAULT_CLIENT_VERSION } as Implementation,
+ connectionTimeoutMs: 10_000, // 10 seconds for connection establishment
+ protocolVersion: MCP_PROTOCOL_VERSION,
+ requestTimeoutSeconds: 300 // 5 minutes for long-running tools
+} as const;
+
+export const MCP_SERVER_ID_PREFIX = 'LlamaUI-MCP-Server';
+
+/** Backoff policy for reconnecting to a dropped MCP server. */
+export const MCP_RECONNECT = {
+ /** Per-attempt timeout for a single reconnection attempt before giving up and backing off. */
+ ATTEMPT_TIMEOUT_MS: 15_000,
+ BACKOFF_MULTIPLIER: 2,
+ INITIAL_DELAY: 1000,
+ MAX_DELAY: 30000
+};
+
+/** Maximum number of MCP server avatars to display in the chat form */
+export const MAX_DISPLAYED_MCP_AVATARS = 4;
+
+/** Expected count when two theme-less icons represent a light/dark pair */
+export const EXPECTED_THEMED_ICON_PAIR_COUNT = 2;
+
+/** CORS proxy connection settings */
+export const CORS_PROXY = {
+ /** Header prefix for headers that should be forwarded by the CORS proxy */
+ HEADER_PREFIX: 'x-llama-server-proxy-header-',
+ /** CORS proxy URL query parameter name */
+ URL_PARAM: 'url'
+} as const;
+
+/** Standard SSE endpoint path indicators */
+export const MCP_SSE = {
+ ENDPOINT: '/sse',
+ ENDPOINT_QUERY: '/sse?',
+ ENDPOINT_SLASH: '/sse/'
+} as const;
+
+/** Human-readable labels for MCP transport types */
+export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
+ [MCPTransportType.SSE]: 'SSE',
+ [MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
+ [MCPTransportType.WEBSOCKET]: 'WebSocket'
+};
+
+/** Icon components for MCP transport types */
+export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
+ [MCPTransportType.SSE]: Radio,
+ [MCPTransportType.STREAMABLE_HTTP]: Globe,
+ [MCPTransportType.WEBSOCKET]: Zap
+};
+++ /dev/null
-import { Globe, Radio, Zap } from '@lucide/svelte';
-import { MCPTransportType } from '$lib/enums';
-import { MimeTypeImage } from '$lib/enums/files.enums';
-import type { ClientCapabilities, Implementation } from '$lib/types';
-import type { Component } from 'svelte';
-
-export const DEFAULT_CLIENT_VERSION = '1.0.0';
-export const MCP_CLIENT_NAME = 'llama-ui-mcp';
-export const DEFAULT_IMAGE_MIME_TYPE = MimeTypeImage.PNG;
-
-/** MIME types considered safe for rendering MCP server icons */
-export const MCP_ALLOWED_ICON_MIME_TYPES = new Set([
- MimeTypeImage.PNG,
- MimeTypeImage.JPEG,
- MimeTypeImage.JPG,
- MimeTypeImage.SVG,
- MimeTypeImage.WEBP,
- MimeTypeImage.ICO,
- MimeTypeImage.ICO_MICROSOFT
-]);
-
-/**
- * MCP specification version this client targets.
- * Update when the upstream MCP spec introduces a new stable version:
- * https://spec.modelcontextprotocol.io/
- */
-export const MCP_PROTOCOL_VERSION = '2025-06-18';
-
-export const DEFAULT_MCP_CONFIG = {
- capabilities: { tools: { listChanged: true } } as ClientCapabilities,
- clientInfo: { name: MCP_CLIENT_NAME, version: DEFAULT_CLIENT_VERSION } as Implementation,
- connectionTimeoutMs: 10_000, // 10 seconds for connection establishment
- protocolVersion: MCP_PROTOCOL_VERSION,
- requestTimeoutSeconds: 300 // 5 minutes for long-running tools
-} as const;
-
-export const MCP_SERVER_ID_PREFIX = 'LlamaUI-MCP-Server';
-
-export const MCP_RECONNECT_INITIAL_DELAY = 1000;
-export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2;
-export const MCP_RECONNECT_MAX_DELAY = 30000;
-/** Per-attempt timeout for a single reconnection attempt before giving up and backing off. */
-export const MCP_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
-
-/** Maximum number of MCP server avatars to display in the chat form */
-export const MAX_DISPLAYED_MCP_AVATARS = 4;
-
-/** Expected count when two theme-less icons represent a light/dark pair */
-export const EXPECTED_THEMED_ICON_PAIR_COUNT = 2;
-
-/** CORS proxy URL query parameter name */
-export const CORS_PROXY_URL_PARAM = 'url';
-
-/** Header prefix for headers that should be forwarded by the CORS proxy */
-export const CORS_PROXY_HEADER_PREFIX = 'x-llama-server-proxy-header-';
-
-/** Number of trailing characters to keep visible when partially redacting mcp-session-id */
-export const MCP_SESSION_ID_VISIBLE_CHARS = 5;
-
-/** Partial-redaction rules for MCP headers: header name -> visible trailing chars */
-export const MCP_PARTIAL_REDACT_HEADERS = new Map<string, number>([
- ['mcp-session-id', MCP_SESSION_ID_VISIBLE_CHARS]
-]);
-
-/** Bearer scheme prefix used for Authorization headers (RFC 6750) */
-export const BEARER_PREFIX = 'Bearer ';
-
-/** Canonical casing for the Authorization header (RFC 7235) */
-export const AUTHORIZATION_HEADER = 'Authorization';
-
-/** Content-Type HTTP header name */
-export const CONTENT_TYPE_HEADER = 'Content-Type';
-
-/** Header names whose values should be redacted in diagnostic logs */
-export const REDACTED_HEADERS = new Set([
- 'authorization',
- 'api-key',
- 'cookie',
- 'mcp-session-id',
- 'proxy-authorization',
- 'set-cookie',
- 'x-auth-token',
- 'x-api-key'
-]);
-
-/** Human-readable labels for MCP transport types */
-export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
- [MCPTransportType.SSE]: 'SSE',
- [MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
- [MCPTransportType.WEBSOCKET]: 'WebSocket'
-};
-
-/** Icon components for MCP transport types */
-export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
- [MCPTransportType.SSE]: Radio,
- [MCPTransportType.STREAMABLE_HTTP]: Globe,
- [MCPTransportType.WEBSOCKET]: Zap
-};
-
-/** Standard SSE endpoint path indicators */
-export const MCP_SSE_ENDPOINT = '/sse';
-export const MCP_SSE_ENDPOINT_SLASH = '/sse/';
-export const MCP_SSE_ENDPOINT_QUERY = '/sse?';
--- /dev/null
+/**
+ * Shared visual contract between the two DOM-only badge paths (the
+ * contenteditable tokenizer + the rehype plugin). Svelte cannot be
+ * mounted at the per-keystroke tokenizer hot path nor from a hast tree,
+ * so both emit the badge with the same class string literal; Tailwind's
+ * scanner picks it up in both sources.
+ */
+export const MENTION_BADGE_CLASSNAME =
+ 'inline-flex w-fit shrink-0 items-center gap-1 whitespace-nowrap rounded-md border border-border/50 bg-foreground/5 px-1.5 py-0.5 text-xs font-mono text-foreground hover:bg-foreground/10 dark:bg-foreground/10 dark:text-secondary-foreground';
+
+export const MENTION_BADGE_ICON_CLASSNAME = 'h-3 w-3 shrink-0';
+
+/** Regex flag that makes the mention scanner walk every link in a message instead of the first. */
+export const MENTION_LINK_SCAN_FLAGS = 'g';
+
+/**
+ * SVG attributes shared by the DOM-built and hast-built badge icons.
+ * The tokenizer applies them via `setAttribute`, the rehype plugin
+ * spreads them onto the hast `<svg>` `properties`; string values are
+ * valid for both.
+ */
+export const MENTION_BADGE_SVG_ATTRIBUTES: Readonly<Record<string, string>> = {
+ 'aria-hidden': 'true',
+ fill: 'none',
+ stroke: 'currentColor',
+ 'stroke-linecap': 'round',
+ 'stroke-linejoin': 'round',
+ 'stroke-width': '2',
+ viewBox: '0 0 24 24',
+ xmlns: 'http://www.w3.org/2000/svg'
+};
+
+/**
+ * SVG path strings for the badge's inline icon; each entry becomes one
+ * `<path>` child of the wrapper `<svg>`. Paths match `lucide-svelte`'s
+ * current `File` and `Folder` glyphs.
+ */
+export const MENTION_BADGE_FILE_ICON_PATHS: readonly string[] = [
+ 'M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z',
+ 'M14 2v5a1 1 0 0 0 1 1h5'
+];
+
+export const MENTION_BADGE_FOLDER_ICON_PATHS: readonly string[] = [
+ 'M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'
+];
+++ /dev/null
-/**
- * Shared visual contract between the two DOM-only badge paths (the
- * contenteditable tokenizer + the rehype plugin). Svelte cannot be
- * mounted at the per-keystroke tokenizer hot path nor from a hast tree,
- * so both emit the badge with the same class string literal; Tailwind's
- * scanner picks it up in both sources.
- */
-export const MENTION_BADGE_CLASSNAME =
- 'inline-flex w-fit shrink-0 items-center gap-1 whitespace-nowrap rounded-md border border-border/50 bg-foreground/5 px-1.5 py-0.5 text-xs font-mono text-foreground hover:bg-foreground/10 dark:bg-foreground/10 dark:text-secondary-foreground';
-
-export const MENTION_BADGE_ICON_CLASSNAME = 'h-3 w-3 shrink-0';
-
-/** Regex flag that makes the mention scanner walk every link in a message instead of the first. */
-export const MENTION_LINK_SCAN_FLAGS = 'g';
-
-/**
- * SVG attributes shared by the DOM-built and hast-built badge icons.
- * The tokenizer applies them via `setAttribute`, the rehype plugin
- * spreads them onto the hast `<svg>` `properties`; string values are
- * valid for both.
- */
-export const MENTION_BADGE_SVG_ATTRIBUTES: Readonly<Record<string, string>> = {
- 'aria-hidden': 'true',
- fill: 'none',
- stroke: 'currentColor',
- 'stroke-linecap': 'round',
- 'stroke-linejoin': 'round',
- 'stroke-width': '2',
- viewBox: '0 0 24 24',
- xmlns: 'http://www.w3.org/2000/svg'
-};
-
-/**
- * SVG path strings for the badge's inline icon; each entry becomes one
- * `<path>` child of the wrapper `<svg>`. Paths match `lucide-svelte`'s
- * current `File` and `Folder` glyphs.
- */
-export const MENTION_BADGE_FILE_ICON_PATHS: readonly string[] = [
- 'M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z',
- 'M14 2v5a1 1 0 0 0 1 1h5'
-];
-
-export const MENTION_BADGE_FOLDER_ICON_PATHS: readonly string[] = [
- 'M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'
-];
--- /dev/null
+export const MERMAID_WRAPPER_CLASS = 'mermaid-block-wrapper';
+export const MERMAID_SCROLL_CONTAINER_CLASS = 'mermaid-scroll-container';
+export const MERMAID_BLOCK_CLASS = 'mermaid';
+
+export const MERMAID_LANGUAGE = 'mermaid';
+
+export const MERMAID_SYNTAX_ATTR = 'data-mermaid-syntax';
+export const MERMAID_ID_ATTR = 'data-mermaid-id';
+export const MERMAID_RENDERED_ATTR = 'data-mermaid-rendered';
+++ /dev/null
-export const MERMAID_WRAPPER_CLASS = 'mermaid-block-wrapper';
-export const MERMAID_SCROLL_CONTAINER_CLASS = 'mermaid-scroll-container';
-export const MERMAID_BLOCK_CLASS = 'mermaid';
-
-export const MERMAID_LANGUAGE = 'mermaid';
-
-export const MERMAID_SYNTAX_ATTR = 'data-mermaid-syntax';
-export const MERMAID_ID_ATTR = 'data-mermaid-id';
-export const MERMAID_RENDERED_ATTR = 'data-mermaid-rendered';
--- /dev/null
+// Conversation exporter / filename constants
+
+export const EXPORT_CONV = {
+ // Producer marker carried by the session record of a JSONL export
+ HARNESS: 'llama.app',
+ // Length of the trimmed conversation ID in the filename
+ ID_TRIM_LENGTH: 8,
+ // Replacements to the ISO date for use in the export filename
+ ISO_DATE_TIME_SEPARATOR: 'T',
+
+ ISO_DATE_TIME_SEPARATOR_REPLACEMENT: '_',
+
+ ISO_TIME_SEPARATOR: ':',
+ ISO_TIME_SEPARATOR_REPLACEMENT: '-',
+ // Characters to keep in the ISO timestamp. 19 keeps 2026-01-01T00:00:00
+ ISO_TIMESTAMP_SLICE: 19,
+
+ MULTIPLE_UNDERSCORE_REGEX: /_+/g,
+ // Maximum length of the sanitized conversation name snippet
+ NAME_SUFFIX_MAX_LENGTH: 20,
+ // Replacements for making the conversation title filename-friendly
+ NON_ALPHANUMERIC_REGEX: /[^a-z0-9]/gi,
+ NONALNUM_REPLACEMENT: '_'
+} as const;
+++ /dev/null
-// Conversation filename constants
-
-// Length of the trimmed conversation ID in the filename
-export const EXPORT_CONV_ID_TRIM_LENGTH = 8;
-// Maximum length of the sanitized conversation name snippet
-export const EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH = 20;
-// Characters to keep in the ISO timestamp. 19 keeps 2026-01-01T00:00:00
-export const ISO_TIMESTAMP_SLICE_LENGTH = 19;
-
-// Producer marker carried by the session record of a JSONL export
-export const SESSION_HARNESS = 'llama.app';
-
-// Replacements for making the conversation title filename-friendly
-export const NON_ALPHANUMERIC_REGEX = /[^a-z0-9]/gi;
-export const EXPORT_CONV_NONALNUM_REPLACEMENT = '_';
-export const MULTIPLE_UNDERSCORE_REGEX = /_+/g;
-
-// Replacements to the ISO date for use in the export filename
-export const ISO_DATE_TIME_SEPARATOR = 'T';
-export const ISO_DATE_TIME_SEPARATOR_REPLACEMENT = '_';
-
-export const ISO_TIME_SEPARATOR = ':';
-export const ISO_TIME_SEPARATOR_REPLACEMENT = '-';
--- /dev/null
+/**
+ * Parsing of `org/ModelName[-tag][:quant]` style model IDs.
+ */
+
+export const MODEL_ID = {
+ /**
+ * Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
+ * The leading `A`/`a` distinguishes it from a regular params segment.
+ */
+ ACTIVATED_PARAMS_RE: /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/,
+
+ /** Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`. */
+ CUSTOM_QUANTIZATION_PREFIX_RE: /^UD$/i,
+ /** Container format segments to exclude from tags (every model uses these). */
+ IGNORED_SEGMENTS: new Set(['GGUF', 'GGML']),
+ /** Sentinel value returned by `indexOf` when a substring is not found. */
+ NOT_FOUND: -1,
+
+ /** Separates `<org>` from `<model>` in a model ID, e.g. `org/ModelName`. */
+ ORG_SEPARATOR: '/',
+
+ /**
+ * Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
+ * The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
+ * `E2B`/`E4B` (MatFormer models sized by resident params).
+ */
+ PARAMS_RE: /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/,
+
+ /**
+ * Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`.
+ * Case-insensitive to handle both uppercase and lowercase inputs.
+ */
+ QUANTIZATION_SEGMENT_RE: /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i,
+
+ /** Separates the model path from the quantization tag, e.g. `model:Q4_K_M`. */
+ QUANTIZATION_SEPARATOR: ':',
+
+ /** Separates named segments within the model path, e.g. `ModelName-7B-GGUF`. */
+ SEGMENT_SEPARATOR: '-',
+
+ /** Matches a trailing weight file extension, e.g. `model.gguf` -> `model`. */
+ WEIGHT_EXTENSION_RE: /\.(gguf|ggml)$/i
+};
+++ /dev/null
-/** Sentinel value returned by `indexOf` when a substring is not found. */
-export const MODEL_ID_NOT_FOUND = -1;
-
-/** Separates `<org>` from `<model>` in a model ID, e.g. `org/ModelName`. */
-export const MODEL_ID_ORG_SEPARATOR = '/';
-
-/** Separates named segments within the model path, e.g. `ModelName-7B-GGUF`. */
-export const MODEL_ID_SEGMENT_SEPARATOR = '-';
-
-/** Separates the model path from the quantization tag, e.g. `model:Q4_K_M`. */
-export const MODEL_ID_QUANTIZATION_SEPARATOR = ':';
-
-/**
- * Matches a quantization/precision segment, e.g. `Q4_K_M`, `IQ4_XS`, `F16`, `BF16`, `MXFP4`.
- * Case-insensitive to handle both uppercase and lowercase inputs.
- */
-export const MODEL_QUANTIZATION_SEGMENT_RE =
- /^(I?Q\d+(_[A-Z0-9]+)*|F\d+|BF\d+|MXFP\d+(_[A-Z0-9]+)*)$/i;
-
-/**
- * Matches prefix for custom quantization types, e.g. `UD-Q8_K_XL`.
- */
-export const MODEL_CUSTOM_QUANTIZATION_PREFIX_RE = /^UD$/i;
-
-/**
- * Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
- * The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
- * `E2B`/`E4B` (MatFormer models sized by resident params).
- */
-export const MODEL_PARAMS_RE = /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/;
-
-/**
- * Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
- * The leading `A`/`a` distinguishes it from a regular params segment.
- */
-export const MODEL_ACTIVATED_PARAMS_RE = /^[Aa]\d+(\.\d+)?[BbMmKkTt]$/;
-
-/**
- * Container format segments to exclude from tags (every model uses these).
- */
-export const MODEL_IGNORED_SEGMENTS = new Set(['GGUF', 'GGML']);
-
-/**
- * Matches a trailing weight file extension, e.g. `model.gguf` -> `model`.
- */
-export const MODEL_WEIGHT_EXTENSION_RE = /\.(gguf|ggml)$/i;
--- /dev/null
+/**
+ * Labels shown while a model loads, keyed by the stage reported on /models/sse.
+ */
+export const MODEL_LOAD_STAGE_LABELS: Record<ApiModelLoadStage, string> = {
+ mmproj_model: 'Loading projector',
+ spec_model: 'Loading draft',
+ text_model: 'Loading weights'
+};
+
+/**
+ * Share of the bar reserved for each load phase after text_model.
+ * text_model fills the rest, so a plain model reaches 100% on its own.
+ */
+export const MODEL_LOAD_TAIL_SHARE = 0.1;
+++ /dev/null
-/**
- * Labels shown while a model loads, keyed by the stage reported on /models/sse.
- */
-export const MODEL_LOAD_STAGE_LABELS: Record<ApiModelLoadStage, string> = {
- mmproj_model: 'Loading projector',
- spec_model: 'Loading draft',
- text_model: 'Loading weights'
-};
-
-/**
- * Share of the bar reserved for each load phase after text_model.
- * text_model fills the rest, so a plain model reaches 100% on its own.
- */
-export const MODEL_LOAD_TAIL_SHARE = 0.1;
--- /dev/null
+/**
+ * Constants for synthetic working-directory messages.
+ *
+ * The synthetic cwd-change message is text the UI renders as a folder row
+ * and the model sees as a turn reminder. The prefix and cleared marker keep
+ * the human-readable wording; the file-link regexes parse the
+ * `[file:///abs/path](display)` payload back out on the UI side.
+ */
+
+import { UrlProtocol } from '$lib/enums';
+
+export const CWD_CHANGED_PREFIX = 'Set working directory to ';
+export const CWD_CLEARED_TEXT = 'Working directory cleared';
+
+/** Trailing separator that marks a path as a directory. */
+export const DIRECTORY_PATH_SUFFIX = '/';
+
+export const HOME_TILDE = '~';
+export const HOME_TILDE_PREFIX = '~/'; // tilde plus path separator
+
+/** Scheme prefix of the file link embedded in a synthetic cwd message. */
+export const FILE_URI_PREFIX = `${UrlProtocol.FILE}//`;
+
+/** Matches the leading `[file:///abs/path](display)` link; not anchored to the end so trailing guidance may follow. */
+export const CWD_LINK_REGEX = /^\[file:\/\/([\s\S]*?)\]\(([\s\S]*?)\)/;
+++ /dev/null
-/**
- * Constants for synthetic working-directory messages.
- *
- * The synthetic cwd-change message is text the UI renders as a folder row
- * and the model sees as a turn reminder. The prefix and cleared marker keep
- * the human-readable wording; the file-link regexes parse the
- * `[file:///abs/path](display)` payload back out on the UI side.
- */
-
-import { UrlProtocol } from '$lib/enums';
-
-export const CWD_CHANGED_PREFIX = 'Set working directory to ';
-export const CWD_CLEARED_TEXT = 'Working directory cleared';
-
-/** Trailing separator that marks a path as a directory. */
-export const DIRECTORY_PATH_SUFFIX = '/';
-
-export const HOME_TILDE = '~';
-export const HOME_TILDE_PREFIX = '~/'; // tilde plus path separator
-
-/** Scheme prefix of the file link embedded in a synthetic cwd message. */
-export const FILE_URI_PREFIX = `${UrlProtocol.FILE}//`;
-
-/** Matches the leading `[file:///abs/path](display)` link; not anchored to the end so trailing guidance may follow. */
-export const CWD_LINK_REGEX = /^\[file:\/\/([\s\S]*?)\]\(([\s\S]*?)\)/;
--- /dev/null
+export const PRECISION_MULTIPLIER = 1000000;
+export const PRECISION_DECIMAL_PLACES = 6;
+++ /dev/null
-export const PRECISION_MULTIPLIER = 1000000;
-export const PRECISION_DECIMAL_PLACES = 6;
+++ /dev/null
-export const PROCESSING_INFO_TIMEOUT = 2000;
-
-/**
- * Statistics units labels
- */
-export const STATS_UNITS = {
- TOKENS_PER_SECOND: 't/s'
-} as const;
--- /dev/null
+/**
+ * Centralized PWA constants to avoid magic strings, regexes, and duplicated
+ * definitions across the codebase.
+ */
+
+import { APP_NAME } from './app.constants';
+
+export const MEDIA_QUERIES = {
+ DISPLAY_MODE_STANDALONE: '(display-mode: standalone)',
+ PREFERS_DARK: '(prefers-color-scheme: dark)',
+ PREFERS_LIGHT: '(prefers-color-scheme: light)'
+} as const;
+
+export const THEME_COLORS = {
+ ACCENT_BLUE: '#2563eb',
+ ACCENT_BLUE_HOVER: '#1d4ed8',
+ BACKGROUND_DARK: '#111111',
+ BACKGROUND_LIGHT: 'white',
+ DARK: '#0d0d0d',
+ LIGHT: '#ffffff',
+ TITLE_UPDATE_ALERT: {
+ BG_DARK: 'zinc-800',
+ BG_LIGHT: 'white',
+ BORDER_DARK: 'zinc-700',
+ BORDER_LIGHT: 'zinc-200',
+ TEXT_DARK: 'zinc-400',
+ TEXT_LIGHT: 'zinc-500'
+ }
+} as const;
+
+export const FAVICON_PATHS = {
+ ICO_DARK: 'favicon-dark.ico',
+ ICO_LIGHT: 'favicon.ico',
+ SVG_DARK: 'favicon-dark.svg',
+ SVG_LIGHT: 'favicon.svg'
+} as const;
+
+// Substituted for `currentColor` in src/lib/assets/logo.svg when generating
+// the light/dark static sources consumed by the PWA asset generator.
+export const FAVICON_COLORS = {
+ DARK: '#fafafa',
+ LIGHT: '#111111'
+} as const;
+
+export const FAVICON_SELECTORS = {
+ ICO_48X48: 'link[rel="icon"][sizes="48x48"]',
+ SVG_ANY: 'link[rel="icon"][type="image/svg+xml"]'
+} as const;
+
+export const APPLE_ASSETS = {
+ TOUCH_ICON: 'apple-touch-icon-180x180.png'
+} as const;
+
+export const PWA_MANIFEST = {
+ background_color: THEME_COLORS.BACKGROUND_LIGHT,
+ description: 'Local AI chat interface powered by llama.cpp',
+ display: 'standalone' as const,
+ icons: [
+ { sizes: '64x64', src: 'pwa-64x64.png', type: 'image/png' },
+ { sizes: '192x192', src: 'pwa-192x192.png', type: 'image/png' },
+ { purpose: 'any' as const, sizes: '512x512', src: 'pwa-512x512.png', type: 'image/png' },
+ {
+ purpose: 'maskable' as const,
+ sizes: '512x512',
+ src: 'maskable-icon-512x512.png',
+ type: 'image/png'
+ }
+ ],
+ name: APP_NAME,
+ short_name: APP_NAME,
+ start_url: './',
+ theme_color: THEME_COLORS.BACKGROUND_LIGHT
+};
+
+export const PWA_ICON_PATHS = {
+ MASKABLE_512: '/maskable-icon-512x512.png',
+ PWA_64: '/pwa-64x64.png',
+ PWA_192: '/pwa-192x192.png',
+ PWA_512: '/pwa-512x512.png'
+} as const;
+
+/** Apple device dimensions (logical points) and DPR, from Apple HIG. */
+export const APPLE_DEVICES = {
+ '640x1136': { dpr: 2, height: 568, width: 320 }, // iPhone 6/7/8 Plus
+ '744x1133': { dpr: 2, height: 573, width: 376 }, // iPad mini 8.3"
+ '750x1334': { dpr: 2, height: 667, width: 375 }, // iPhone 6/7/8, 14
+ '1032x1376': { dpr: 2, height: 1376, width: 1032 }, // iPad Air 13"
+ // iPhones (DPR 3)
+ '1170x2532': { dpr: 3, height: 844, width: 390 }, // iPhone 13, 15
+ '1179x2556': { dpr: 3, height: 852, width: 393 }, // iPhone 14, 15 Pro, 16
+ '1206x2622': { dpr: 3, height: 874, width: 402 }, // iPhone 16 Plus, 16e
+ '1284x2778': { dpr: 3, height: 926, width: 428 }, // iPhone 15 Plus
+ '1290x2796': { dpr: 3, height: 932, width: 430 }, // iPhone 15 Pro Max, 16 Pro
+ '1320x2868': { dpr: 3, height: 956, width: 440 }, // iPhone 16 Pro Max
+ '1640x2360': { dpr: 2, height: 1180, width: 820 }, // iPad Air 10.9"
+ // iPads (DPR 2)
+ '1668x2388': { dpr: 2, height: 1194, width: 834 }, // iPad Air 11", iPad 11"
+ '2048x2732': { dpr: 2, height: 1366, width: 1024 } // iPad Pro 12.9"
+} as const;
+
+export type AppleDeviceKey = keyof typeof APPLE_DEVICES;
+
+export const PWA_FILE_PATHS = {
+ MANIFEST: '/manifest.webmanifest',
+ SERVICE_WORKER: '/sw.js',
+ VERSION: '/version.json',
+ WORKBOX: '/workbox-<hash>.js'
+} as const;
+
+// Used by the server middleware to skip API key validation.
+// Keep in sync with tools/server/server-http.cpp public_endpoints list.
+
+export const PUBLIC_ENDPOINTS = [
+ '/health',
+ '/v1/health',
+ '/models',
+ '/v1/models',
+ '/props',
+ '/metrics',
+ '/',
+ '/index.html',
+
+ '/favicon.ico',
+ '/favicon-dark.ico',
+ '/favicon.svg',
+ '/favicon-dark.svg',
+ '/pwa-64x64.png',
+ '/pwa-192x192.png',
+ '/pwa-512x512.png',
+ '/maskable-icon-512x512.png',
+ '/apple-touch-icon-180x180.png',
+ '/apple-splash-portrait-640x1136.png',
+ '/apple-splash-landscape-640x1136.png',
+ '/apple-splash-portrait-750x1334.png',
+ '/apple-splash-landscape-750x1334.png',
+ '/apple-splash-portrait-1170x2532.png',
+ '/apple-splash-landscape-1170x2532.png',
+ '/apple-splash-portrait-1179x2556.png',
+ '/apple-splash-landscape-1179x2556.png',
+ '/apple-splash-portrait-1206x2622.png',
+ '/apple-splash-landscape-1206x2622.png',
+ '/apple-splash-portrait-1284x2778.png',
+ '/apple-splash-landscape-1284x2778.png',
+ '/apple-splash-portrait-1290x2796.png',
+ '/apple-splash-landscape-1290x2796.png',
+ '/apple-splash-portrait-1320x2868.png',
+ '/apple-splash-landscape-1320x2868.png',
+ '/apple-splash-portrait-1488x2266.png',
+ '/apple-splash-landscape-1488x2266.png',
+ '/apple-splash-portrait-1640x2360.png',
+ '/apple-splash-landscape-1640x2360.png',
+ '/apple-splash-portrait-1668x2388.png',
+ '/apple-splash-landscape-1668x2388.png',
+ '/apple-splash-portrait-2048x2732.png',
+ '/apple-splash-landscape-2048x2732.png',
+ '/apple-splash-portrait-dark-640x1136.png',
+ '/apple-splash-landscape-dark-640x1136.png',
+ '/apple-splash-portrait-dark-750x1334.png',
+ '/apple-splash-landscape-dark-750x1334.png',
+ '/apple-splash-portrait-dark-1170x2532.png',
+ '/apple-splash-landscape-dark-1170x2532.png',
+ '/apple-splash-portrait-dark-1179x2556.png',
+ '/apple-splash-landscape-dark-1179x2556.png',
+ '/apple-splash-portrait-dark-1206x2622.png',
+ '/apple-splash-landscape-dark-1206x2622.png',
+ '/apple-splash-portrait-dark-1284x2778.png',
+ '/apple-splash-landscape-dark-1284x2778.png',
+ '/apple-splash-portrait-dark-1290x2796.png',
+ '/apple-splash-landscape-dark-1290x2796.png',
+ '/apple-splash-portrait-dark-1320x2868.png',
+ '/apple-splash-landscape-dark-1320x2868.png',
+ '/apple-splash-portrait-dark-1488x2266.png',
+ '/apple-splash-landscape-dark-1488x2266.png',
+ '/apple-splash-portrait-dark-1640x2360.png',
+ '/apple-splash-landscape-dark-1640x2360.png',
+ '/apple-splash-portrait-dark-1668x2388.png',
+ '/apple-splash-landscape-dark-1668x2388.png',
+ '/apple-splash-portrait-dark-2048x2732.png',
+ '/apple-splash-landscape-dark-2048x2732.png',
+ '/manifest.webmanifest',
+ '/sw.js',
+ '/version.json',
+ '/workbox-<hash>.js'
+] as const;
+export const BUILD_CONFIG = {
+ GUIDE_COMMENT: `
+<!--
+ This is a static build of the frontend.
+ It is automatically generated by the build process.
+ Do not edit this file directly.
+ To make changes, refer to the "Web UI" section in the README.
+-->
+`.trim(),
+ OUTPUT_DIR: './dist'
+} as const;
+
+export const REGEX_PATTERNS = {
+ HEAD_CLOSE: /\t*<\/head>/,
+ SPLASH_FILE: /^apple-splash-(portrait|landscape)-(dark-)?(\d+)x(\d+)\.png$/
+} as const;
+
+// Device names used by @vite-pwa/assets-generator for splash screen generation.
+// Keep in sync with pwa-assets.config.ts.
+export const PWA_GENERATOR_DEVICES = [
+ 'iPhone 13',
+ 'iPhone 13 Pro',
+ 'iPhone 13 Pro Max',
+ 'iPhone 14',
+ 'iPhone 14 Plus',
+ 'iPhone 14 Pro',
+ 'iPhone 14 Pro Max',
+ 'iPhone 15',
+ 'iPhone 15 Plus',
+ 'iPhone 15 Pro',
+ 'iPhone 15 Pro Max',
+ 'iPhone 16',
+ 'iPhone 16 Plus',
+ 'iPhone 16 Pro',
+ 'iPhone 16 Pro Max',
+ 'iPhone 16e',
+ 'iPhone SE 4"',
+ 'iPhone SE 4.7"',
+ 'iPad 11"',
+ 'iPad Air 10.9"',
+ 'iPad Air 11"',
+ 'iPad Air 13"',
+ 'iPad Pro 11"',
+ 'iPad Pro 12.9"',
+ 'iPad mini 8.3"'
+] as const;
+
+// PWA assets generator configuration — used by pwa-assets.config.ts
+// FAVICON_PADDING: fraction (0..1) of the icon reserved as equal margin on
+// each side. Applied to icon PNG/ICO outputs by @vite-pwa/assets-generator and
+// post-processed into the static favicon.svg so the in-app logo (which reads
+// src/lib/assets/logo.svg directly) is unaffected.
+export const PWA_ASSET_GENERATOR = {
+ ADD_MEDIA_SCREEN: true,
+ BASE_PATH: './',
+ DARK_PREFIX: 'dark-',
+ FAVICON_PADDING: 0.04,
+ FIT_MODE: 'contain',
+ LINK_PRESET: '2023',
+ PNG_COMPRESSION_LEVEL: 9,
+ PNG_QUALITY: 60,
+ SPLASH_PADDING: 0.75,
+ XHTML: false
+} as const;
+
+export const CACHE_SETTINGS = {
+ API_CACHE_MAX_AGE_SECONDS: 60 * 60 * 24,
+ API_CACHE_MAX_ENTRIES: 50,
+ IMMUTABLE_MAX_AGE_SECONDS: 31536000,
+ MAX_FILE_SIZE_BYTES: 10 * 1024 * 1024
+} as const;
+
+export const GLOB_PATTERNS: string[] = [
+ '**/*.{js,css,html,ico,svg,png,webp,woff,woff2,json,webmanifest}'
+];
+
+export const SW_CONFIG = {
+ CHECK_INTERVAL_MS: 60000,
+ UPDATE_FETCH_OPTIONS: {
+ CACHE: 'no-store',
+ HEADERS: {
+ CACHE: 'no-store',
+ CACHE_CONTROL: 'no-cache'
+ }
+ }
+} as const;
+
+// Runtime caching configuration for Workbox
+export const RUNTIME_CACHING = {
+ CACHE_NAME: 'api-cache',
+ HANDLER: 'NetworkFirst'
+} as const;
+
+// Workbox runtime caching patterns
+export const API_CACHING_PATTERNS = {
+ STATIC_API: /^\/(health|props|models|tools|slots|cors-proxy).*/,
+ V1_API: /^\/v1\/.*/
+} as const;
+
+// SvelteKit PWA plugin options
+export const PWA_KIT_OPTIONS = {} as const;
+
+export const APPLE_META_TAGS = {
+ MOBILE_WEB_APP_CAPABLE: { content: 'yes', name: 'apple-mobile-web-app-capable' },
+ MOBILE_WEB_APP_TITLE: { name: 'apple-mobile-web-app-title' },
+ STATUS_BAR_STYLE: { content: 'black-translucent', name: 'apple-mobile-web-app-status-bar-style' }
+} as const;
+
+// Splash screen HTML link tag prefix used by generateSplashScreenLinks
+export const SPLASH_LINK = {
+ DARK_MEDIA_SUFFIX: ' and (prefers-color-scheme: dark)',
+ HTML: '<link rel="apple-touch-startup-image"'
+} as const;
+
+// SvelteKit PWA plugin configuration — used by @vite.config.ts
+import type { SvelteKitPWAOptions } from '@vite-pwa/sveltekit';
+
+export const SVELTEKIT_PWA_OPTIONS: SvelteKitPWAOptions = {
+ devOptions: {
+ enabled: true,
+ suppressWarnings: true
+ },
+
+ // SvelteKit-specific options
+ kit: {
+ // Include version file for proper cache invalidation
+ includeVersionFile: true
+ },
+
+ // Strategy: generateSW - the plugin generates a service worker automatically
+ // using Workbox. For a custom SW, use 'injectManifest' instead.
+ // Manifest configuration
+ manifest: PWA_MANIFEST,
+
+ // Workbox configuration for generateSW strategy
+ workbox: {
+ // Match all static assets in the build output.
+ // Uses '**/' because SvelteKit outputs files under _app/immutable/
+ // subdirectories.
+ globPatterns: GLOB_PATTERNS,
+ maximumFileSizeToCacheInBytes: CACHE_SETTINGS.MAX_FILE_SIZE_BYTES,
+
+ // Prevent @vite-pwa/sveltekit from auto-adding a NavigationRoute by
+ // setting navigateFallback to empty string. This keeps the service
+ // worker from intercepting direct browser navigation to server API
+ // endpoints (e.g. /slots, /models, /v1/models) which should return
+ // JSON, not the SPA HTML shell. The server's own static-file fallback
+ // handles non-API navigation to index.html for the SPA router.
+ navigateFallback: '',
+
+ // Runtime caching for API calls - use NetworkFirst so APIs are always fresh
+ runtimeCaching: [
+ {
+ handler: RUNTIME_CACHING.HANDLER,
+ options: {
+ cacheName: RUNTIME_CACHING.CACHE_NAME,
+ expiration: {
+ maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
+ maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
+ }
+ },
+ urlPattern: API_CACHING_PATTERNS.V1_API
+ },
+ {
+ handler: RUNTIME_CACHING.HANDLER,
+ options: {
+ cacheName: RUNTIME_CACHING.CACHE_NAME,
+ expiration: {
+ maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
+ maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
+ }
+ },
+ urlPattern: API_CACHING_PATTERNS.STATIC_API
+ }
+ ]
+ }
+};
+++ /dev/null
-/**
- * Centralized PWA constants to avoid magic strings, regexes, and duplicated
- * definitions across the codebase.
- */
-
-import { APP_NAME } from './app';
-
-export const MEDIA_QUERIES = {
- DISPLAY_MODE_STANDALONE: '(display-mode: standalone)',
- PREFERS_DARK: '(prefers-color-scheme: dark)',
- PREFERS_LIGHT: '(prefers-color-scheme: light)'
-} as const;
-
-export const THEME_COLORS = {
- ACCENT_BLUE: '#2563eb',
- ACCENT_BLUE_HOVER: '#1d4ed8',
- BACKGROUND_DARK: '#111111',
- BACKGROUND_LIGHT: 'white',
- DARK: '#0d0d0d',
- LIGHT: '#ffffff',
- TITLE_UPDATE_ALERT: {
- BG_DARK: 'zinc-800',
- BG_LIGHT: 'white',
- BORDER_DARK: 'zinc-700',
- BORDER_LIGHT: 'zinc-200',
- TEXT_DARK: 'zinc-400',
- TEXT_LIGHT: 'zinc-500'
- }
-} as const;
-
-export const FAVICON_PATHS = {
- ICO_DARK: 'favicon-dark.ico',
- ICO_LIGHT: 'favicon.ico',
- SVG_DARK: 'favicon-dark.svg',
- SVG_LIGHT: 'favicon.svg'
-} as const;
-
-// Substituted for `currentColor` in src/lib/assets/logo.svg when generating
-// the light/dark static sources consumed by the PWA asset generator.
-export const FAVICON_COLORS = {
- DARK: '#fafafa',
- LIGHT: '#111111'
-} as const;
-
-export const FAVICON_SELECTORS = {
- ICO_48X48: 'link[rel="icon"][sizes="48x48"]',
- SVG_ANY: 'link[rel="icon"][type="image/svg+xml"]'
-} as const;
-
-export const APPLE_ASSETS = {
- TOUCH_ICON: 'apple-touch-icon-180x180.png'
-} as const;
-
-export const PWA_MANIFEST = {
- background_color: THEME_COLORS.BACKGROUND_LIGHT,
- description: 'Local AI chat interface powered by llama.cpp',
- display: 'standalone' as const,
- icons: [
- { sizes: '64x64', src: 'pwa-64x64.png', type: 'image/png' },
- { sizes: '192x192', src: 'pwa-192x192.png', type: 'image/png' },
- { purpose: 'any' as const, sizes: '512x512', src: 'pwa-512x512.png', type: 'image/png' },
- {
- purpose: 'maskable' as const,
- sizes: '512x512',
- src: 'maskable-icon-512x512.png',
- type: 'image/png'
- }
- ],
- name: APP_NAME,
- short_name: APP_NAME,
- start_url: './',
- theme_color: THEME_COLORS.BACKGROUND_LIGHT
-};
-
-export const PWA_ICON_PATHS = {
- MASKABLE_512: '/maskable-icon-512x512.png',
- PWA_64: '/pwa-64x64.png',
- PWA_192: '/pwa-192x192.png',
- PWA_512: '/pwa-512x512.png'
-} as const;
-
-/** Apple device dimensions (logical points) and DPR, from Apple HIG. */
-export const APPLE_DEVICES = {
- '640x1136': { dpr: 2, height: 568, width: 320 }, // iPhone 6/7/8 Plus
- '744x1133': { dpr: 2, height: 573, width: 376 }, // iPad mini 8.3"
- '750x1334': { dpr: 2, height: 667, width: 375 }, // iPhone 6/7/8, 14
- '1032x1376': { dpr: 2, height: 1376, width: 1032 }, // iPad Air 13"
- // iPhones (DPR 3)
- '1170x2532': { dpr: 3, height: 844, width: 390 }, // iPhone 13, 15
- '1179x2556': { dpr: 3, height: 852, width: 393 }, // iPhone 14, 15 Pro, 16
- '1206x2622': { dpr: 3, height: 874, width: 402 }, // iPhone 16 Plus, 16e
- '1284x2778': { dpr: 3, height: 926, width: 428 }, // iPhone 15 Plus
- '1290x2796': { dpr: 3, height: 932, width: 430 }, // iPhone 15 Pro Max, 16 Pro
- '1320x2868': { dpr: 3, height: 956, width: 440 }, // iPhone 16 Pro Max
- '1640x2360': { dpr: 2, height: 1180, width: 820 }, // iPad Air 10.9"
- // iPads (DPR 2)
- '1668x2388': { dpr: 2, height: 1194, width: 834 }, // iPad Air 11", iPad 11"
- '2048x2732': { dpr: 2, height: 1366, width: 1024 } // iPad Pro 12.9"
-} as const;
-
-export type AppleDeviceKey = keyof typeof APPLE_DEVICES;
-
-export const PWA_FILE_PATHS = {
- MANIFEST: '/manifest.webmanifest',
- SERVICE_WORKER: '/sw.js',
- VERSION: '/version.json',
- WORKBOX: '/workbox-<hash>.js'
-} as const;
-
-// Used by the server middleware to skip API key validation.
-// Keep in sync with tools/server/server-http.cpp public_endpoints list.
-
-export const PUBLIC_ENDPOINTS = [
- '/health',
- '/v1/health',
- '/models',
- '/v1/models',
- '/props',
- '/metrics',
- '/',
- '/index.html',
-
- '/favicon.ico',
- '/favicon-dark.ico',
- '/favicon.svg',
- '/favicon-dark.svg',
- '/pwa-64x64.png',
- '/pwa-192x192.png',
- '/pwa-512x512.png',
- '/maskable-icon-512x512.png',
- '/apple-touch-icon-180x180.png',
- '/apple-splash-portrait-640x1136.png',
- '/apple-splash-landscape-640x1136.png',
- '/apple-splash-portrait-750x1334.png',
- '/apple-splash-landscape-750x1334.png',
- '/apple-splash-portrait-1170x2532.png',
- '/apple-splash-landscape-1170x2532.png',
- '/apple-splash-portrait-1179x2556.png',
- '/apple-splash-landscape-1179x2556.png',
- '/apple-splash-portrait-1206x2622.png',
- '/apple-splash-landscape-1206x2622.png',
- '/apple-splash-portrait-1284x2778.png',
- '/apple-splash-landscape-1284x2778.png',
- '/apple-splash-portrait-1290x2796.png',
- '/apple-splash-landscape-1290x2796.png',
- '/apple-splash-portrait-1320x2868.png',
- '/apple-splash-landscape-1320x2868.png',
- '/apple-splash-portrait-1488x2266.png',
- '/apple-splash-landscape-1488x2266.png',
- '/apple-splash-portrait-1640x2360.png',
- '/apple-splash-landscape-1640x2360.png',
- '/apple-splash-portrait-1668x2388.png',
- '/apple-splash-landscape-1668x2388.png',
- '/apple-splash-portrait-2048x2732.png',
- '/apple-splash-landscape-2048x2732.png',
- '/apple-splash-portrait-dark-640x1136.png',
- '/apple-splash-landscape-dark-640x1136.png',
- '/apple-splash-portrait-dark-750x1334.png',
- '/apple-splash-landscape-dark-750x1334.png',
- '/apple-splash-portrait-dark-1170x2532.png',
- '/apple-splash-landscape-dark-1170x2532.png',
- '/apple-splash-portrait-dark-1179x2556.png',
- '/apple-splash-landscape-dark-1179x2556.png',
- '/apple-splash-portrait-dark-1206x2622.png',
- '/apple-splash-landscape-dark-1206x2622.png',
- '/apple-splash-portrait-dark-1284x2778.png',
- '/apple-splash-landscape-dark-1284x2778.png',
- '/apple-splash-portrait-dark-1290x2796.png',
- '/apple-splash-landscape-dark-1290x2796.png',
- '/apple-splash-portrait-dark-1320x2868.png',
- '/apple-splash-landscape-dark-1320x2868.png',
- '/apple-splash-portrait-dark-1488x2266.png',
- '/apple-splash-landscape-dark-1488x2266.png',
- '/apple-splash-portrait-dark-1640x2360.png',
- '/apple-splash-landscape-dark-1640x2360.png',
- '/apple-splash-portrait-dark-1668x2388.png',
- '/apple-splash-landscape-dark-1668x2388.png',
- '/apple-splash-portrait-dark-2048x2732.png',
- '/apple-splash-landscape-dark-2048x2732.png',
- '/manifest.webmanifest',
- '/sw.js',
- '/version.json',
- '/workbox-<hash>.js'
-] as const;
-export const BUILD_CONFIG = {
- GUIDE_COMMENT: `
-<!--
- This is a static build of the frontend.
- It is automatically generated by the build process.
- Do not edit this file directly.
- To make changes, refer to the "Web UI" section in the README.
--->
-`.trim(),
- OUTPUT_DIR: './dist'
-} as const;
-
-export const REGEX_PATTERNS = {
- HEAD_CLOSE: /\t*<\/head>/,
- SPLASH_FILE: /^apple-splash-(portrait|landscape)-(dark-)?(\d+)x(\d+)\.png$/
-} as const;
-
-// Device names used by @vite-pwa/assets-generator for splash screen generation.
-// Keep in sync with pwa-assets.config.ts.
-export const PWA_GENERATOR_DEVICES = [
- 'iPhone 13',
- 'iPhone 13 Pro',
- 'iPhone 13 Pro Max',
- 'iPhone 14',
- 'iPhone 14 Plus',
- 'iPhone 14 Pro',
- 'iPhone 14 Pro Max',
- 'iPhone 15',
- 'iPhone 15 Plus',
- 'iPhone 15 Pro',
- 'iPhone 15 Pro Max',
- 'iPhone 16',
- 'iPhone 16 Plus',
- 'iPhone 16 Pro',
- 'iPhone 16 Pro Max',
- 'iPhone 16e',
- 'iPhone SE 4"',
- 'iPhone SE 4.7"',
- 'iPad 11"',
- 'iPad Air 10.9"',
- 'iPad Air 11"',
- 'iPad Air 13"',
- 'iPad Pro 11"',
- 'iPad Pro 12.9"',
- 'iPad mini 8.3"'
-] as const;
-
-// PWA assets generator configuration — used by pwa-assets.config.ts
-// FAVICON_PADDING: fraction (0..1) of the icon reserved as equal margin on
-// each side. Applied to icon PNG/ICO outputs by @vite-pwa/assets-generator and
-// post-processed into the static favicon.svg so the in-app logo (which reads
-// src/lib/assets/logo.svg directly) is unaffected.
-export const PWA_ASSET_GENERATOR = {
- ADD_MEDIA_SCREEN: true,
- BASE_PATH: './',
- DARK_PREFIX: 'dark-',
- FAVICON_PADDING: 0.04,
- FIT_MODE: 'contain',
- LINK_PRESET: '2023',
- PNG_COMPRESSION_LEVEL: 9,
- PNG_QUALITY: 60,
- SPLASH_PADDING: 0.75,
- XHTML: false
-} as const;
-
-export const CACHE_SETTINGS = {
- API_CACHE_MAX_AGE_SECONDS: 60 * 60 * 24,
- API_CACHE_MAX_ENTRIES: 50,
- IMMUTABLE_MAX_AGE_SECONDS: 31536000,
- MAX_FILE_SIZE_BYTES: 10 * 1024 * 1024
-} as const;
-
-export const GLOB_PATTERNS: string[] = [
- '**/*.{js,css,html,ico,svg,png,webp,woff,woff2,json,webmanifest}'
-];
-
-export const SW_CONFIG = {
- CHECK_INTERVAL_MS: 60000,
- UPDATE_FETCH_OPTIONS: {
- CACHE: 'no-store',
- HEADERS: {
- CACHE: 'no-store',
- CACHE_CONTROL: 'no-cache'
- }
- }
-} as const;
-
-// Runtime caching configuration for Workbox
-export const RUNTIME_CACHING = {
- CACHE_NAME: 'api-cache',
- HANDLER: 'NetworkFirst'
-} as const;
-
-// Workbox runtime caching patterns
-export const API_CACHING_PATTERNS = {
- STATIC_API: /^\/(health|props|models|tools|slots|cors-proxy).*/,
- V1_API: /^\/v1\/.*/
-} as const;
-
-// SvelteKit PWA plugin options
-export const PWA_KIT_OPTIONS = {} as const;
-
-export const APPLE_META_TAGS = {
- MOBILE_WEB_APP_CAPABLE: { content: 'yes', name: 'apple-mobile-web-app-capable' },
- MOBILE_WEB_APP_TITLE: { name: 'apple-mobile-web-app-title' },
- STATUS_BAR_STYLE: { content: 'black-translucent', name: 'apple-mobile-web-app-status-bar-style' }
-} as const;
-
-// Splash screen HTML link tag prefix used by generateSplashScreenLinks
-export const SPLASH_LINK = {
- DARK_MEDIA_SUFFIX: ' and (prefers-color-scheme: dark)',
- HTML: '<link rel="apple-touch-startup-image"'
-} as const;
-
-// SvelteKit PWA plugin configuration — used by @vite.config.ts
-import type { SvelteKitPWAOptions } from '@vite-pwa/sveltekit';
-
-export const SVELTEKIT_PWA_OPTIONS: SvelteKitPWAOptions = {
- devOptions: {
- enabled: true,
- suppressWarnings: true
- },
-
- // SvelteKit-specific options
- kit: {
- // Include version file for proper cache invalidation
- includeVersionFile: true
- },
-
- // Strategy: generateSW - the plugin generates a service worker automatically
- // using Workbox. For a custom SW, use 'injectManifest' instead.
- // Manifest configuration
- manifest: PWA_MANIFEST,
-
- // Workbox configuration for generateSW strategy
- workbox: {
- // Match all static assets in the build output.
- // Uses '**/' because SvelteKit outputs files under _app/immutable/
- // subdirectories.
- globPatterns: GLOB_PATTERNS,
- maximumFileSizeToCacheInBytes: CACHE_SETTINGS.MAX_FILE_SIZE_BYTES,
-
- // Prevent @vite-pwa/sveltekit from auto-adding a NavigationRoute by
- // setting navigateFallback to empty string. This keeps the service
- // worker from intercepting direct browser navigation to server API
- // endpoints (e.g. /slots, /models, /v1/models) which should return
- // JSON, not the SPA HTML shell. The server's own static-file fallback
- // handles non-API navigation to index.html for the SPA router.
- navigateFallback: '',
-
- // Runtime caching for API calls - use NetworkFirst so APIs are always fresh
- runtimeCaching: [
- {
- handler: RUNTIME_CACHING.HANDLER,
- options: {
- cacheName: RUNTIME_CACHING.CACHE_NAME,
- expiration: {
- maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
- maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
- }
- },
- urlPattern: API_CACHING_PATTERNS.V1_API
- },
- {
- handler: RUNTIME_CACHING.HANDLER,
- options: {
- cacheName: RUNTIME_CACHING.CACHE_NAME,
- expiration: {
- maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
- maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
- }
- },
- urlPattern: API_CACHING_PATTERNS.STATIC_API
- }
- ]
- }
-};
+++ /dev/null
-import { ReasoningEffort } from '$lib/enums';
-
-/**
- * Reasoning effort to token budget mapping.
- * Maps the ReasoningEffort enum values to concrete token counts for the server.
- */
-export const REASONING_EFFORT_TOKENS: Record<string, number> = {
- [ReasoningEffort.HIGH]: 8192,
- [ReasoningEffort.LOW]: 512,
- [ReasoningEffort.MAX]: -1, // unlimited
- [ReasoningEffort.MEDIUM]: 2048
-};
--- /dev/null
+import { ReasoningEffort } from '$lib/enums';
+import type { ReasoningEffortLevel } from '$lib/types';
+
+/**
+ * Reasoning effort UI labels.
+ * Keys match the ReasoningEffort enum values for type-safe lookups.
+ */
+export const REASONING_EFFORT_LABELS: Record<string, string> = {
+ [ReasoningEffort.DEFAULT]: 'Default',
+ [ReasoningEffort.HIGH]: 'High',
+ [ReasoningEffort.LOW]: 'Low',
+ [ReasoningEffort.MAX]: 'Max',
+ [ReasoningEffort.MEDIUM]: 'Medium',
+ [ReasoningEffort.OFF]: 'Off'
+};
+
+export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [
+ { label: 'Default', value: ReasoningEffort.DEFAULT },
+ { label: 'Off', value: ReasoningEffort.OFF },
+ { label: 'Low', value: ReasoningEffort.LOW },
+ { label: 'Medium', value: ReasoningEffort.MEDIUM },
+ { label: 'High', value: ReasoningEffort.HIGH },
+ { hasInfo: true, label: 'Max', value: ReasoningEffort.MAX }
+];
+
+/**
+ * Reasoning effort to token budget mapping.
+ * Maps the ReasoningEffort enum values to concrete token counts for the server.
+ */
+export const REASONING_EFFORT_TOKENS: Record<string, number> = {
+ [ReasoningEffort.HIGH]: 8192,
+ [ReasoningEffort.LOW]: 512,
+ [ReasoningEffort.MAX]: -1, // unlimited
+ [ReasoningEffort.MEDIUM]: 2048
+};
+++ /dev/null
-import { ReasoningEffort } from '$lib/enums';
-import type { ReasoningEffortLevel } from '$lib/types';
-
-/**
- * Reasoning effort UI labels.
- * Keys match the ReasoningEffort enum values for type-safe lookups.
- */
-export const REASONING_EFFORT_LABELS: Record<string, string> = {
- [ReasoningEffort.DEFAULT]: 'Default',
- [ReasoningEffort.HIGH]: 'High',
- [ReasoningEffort.LOW]: 'Low',
- [ReasoningEffort.MAX]: 'Max',
- [ReasoningEffort.MEDIUM]: 'Medium',
- [ReasoningEffort.OFF]: 'Off'
-};
-
-export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [
- { label: 'Default', value: ReasoningEffort.DEFAULT },
- { label: 'Off', value: ReasoningEffort.OFF },
- { label: 'Low', value: ReasoningEffort.LOW },
- { label: 'Medium', value: ReasoningEffort.MEDIUM },
- { label: 'High', value: ReasoningEffort.HIGH },
- { hasInfo: true, label: 'Max', value: ReasoningEffort.MAX }
-];
--- /dev/null
+import type { RecommendedMCPServer } from '$lib/types';
+
+// Suggested MCP servers shown as opt-in cards in the "Add New Server" dialog.
+// Rendering these cards never reaches the upstream domain - favicons come
+// from local bundles in static/recommended-mcp/ and the URL is only used
+// after the user clicks Add.
+export const RECOMMENDED_MCP_SERVERS: RecommendedMCPServer[] = [
+ {
+ description: 'Search the web and fetch full page content as clean markdown.',
+ iconUrl: '/recommended-mcp/exa.ico',
+ id: 'exa',
+ name: 'Exa',
+ url: 'https://mcp.exa.ai/mcp'
+ },
+ {
+ description: 'Search and browse AI models, datasets, spaces, and docs on the Hugging Face Hub.',
+ iconUrl: '/recommended-mcp/huggingface.ico',
+ id: 'huggingface',
+ name: 'Hugging Face',
+ url: 'https://huggingface.co/mcp'
+ },
+ {
+ description: 'Search repositories, issues, pull requests and interact with code on GitHub.',
+ iconUrlDark: '/recommended-mcp/github-dark.png',
+ iconUrlLight: '/recommended-mcp/github-light.png',
+ id: 'github',
+ name: 'GitHub',
+ needsAuthorization: true,
+ url: 'https://api.githubcopilot.com/mcp'
+ },
+ {
+ description: 'Browse up-to-date documentation and code examples for libraries and frameworks.',
+ iconUrl: '/recommended-mcp/context7.png',
+ id: 'context7',
+ name: 'Context7',
+ url: 'https://mcp.context7.com/mcp'
+ }
+];
+++ /dev/null
-import type { RecommendedMCPServer } from '$lib/types';
-
-// Suggested MCP servers shown as opt-in cards in the "Add New Server" dialog.
-// Rendering these cards never reaches the upstream domain - favicons come
-// from local bundles in static/recommended-mcp/ and the URL is only used
-// after the user clicks Add.
-export const RECOMMENDED_MCP_SERVERS: RecommendedMCPServer[] = [
- {
- description: 'Search the web and fetch full page content as clean markdown.',
- iconUrl: '/recommended-mcp/exa.ico',
- id: 'exa',
- name: 'Exa',
- url: 'https://mcp.exa.ai/mcp'
- },
- {
- description: 'Search and browse AI models, datasets, spaces, and docs on the Hugging Face Hub.',
- iconUrl: '/recommended-mcp/huggingface.ico',
- id: 'huggingface',
- name: 'Hugging Face',
- url: 'https://huggingface.co/mcp'
- },
- {
- description: 'Search repositories, issues, pull requests and interact with code on GitHub.',
- iconUrlDark: '/recommended-mcp/github-dark.png',
- iconUrlLight: '/recommended-mcp/github-light.png',
- id: 'github',
- name: 'GitHub',
- needsAuthorization: true,
- url: 'https://api.githubcopilot.com/mcp'
- },
- {
- description: 'Browse up-to-date documentation and code examples for libraries and frameworks.',
- iconUrl: '/recommended-mcp/context7.png',
- id: 'context7',
- name: 'Context7',
- url: 'https://mcp.context7.com/mcp'
- }
-];
--- /dev/null
+/** Query params the chat routes read from the URL. */
+export const URL_PARAMS = {
+ /** Load the selected model instead of waiting for the first message. */
+ LOAD: 'load',
+ /** Model to select. */
+ MODEL: 'model',
+ /** Start a new chat. */
+ NEW_CHAT: 'new_chat',
+ /** Prompt to send on arrival. */
+ QUERY: 'q'
+} as const;
+
+/** Settings section slugs — used for routes and navigation. */
+export const SETTINGS_SECTION_SLUGS = {
+ AGENTIC: 'agentic',
+ DEVELOPER: 'developer',
+ DISPLAY: 'display',
+ GENERAL: 'general',
+ IMPORT_EXPORT: 'import-export',
+ PENALTIES: 'penalties',
+ SAMPLING: 'sampling',
+ TOOLS: 'tools'
+} as const;
+
+export const ROUTES = {
+ /** Chat base — for dynamic chat URLs use RouterService. */
+ CHAT: '#/chat',
+ /** MCP servers. */
+ MCP_SERVERS: '#/mcp-servers',
+ /** New chat — root with new chat query param. */
+ NEW_CHAT: `?${URL_PARAMS.NEW_CHAT}=true#/`,
+ /** Search — mobile-only full-page conversation search. */
+ SEARCH: '#/search',
+ /** Settings base — for dynamic settings URLs use RouterService. */
+ SETTINGS: '#/settings',
+ /** Root — start of the app. */
+ START: '#/'
+} as const;
+++ /dev/null
-/** Query params the chat routes read from the URL. */
-export const URL_PARAMS = {
- /** Load the selected model instead of waiting for the first message. */
- LOAD: 'load',
- /** Model to select. */
- MODEL: 'model',
- /** Start a new chat. */
- NEW_CHAT: 'new_chat',
- /** Prompt to send on arrival. */
- QUERY: 'q'
-} as const;
-
-/** Settings section slugs — used for routes and navigation. */
-export const SETTINGS_SECTION_SLUGS = {
- AGENTIC: 'agentic',
- DEVELOPER: 'developer',
- DISPLAY: 'display',
- GENERAL: 'general',
- IMPORT_EXPORT: 'import-export',
- PENALTIES: 'penalties',
- SAMPLING: 'sampling',
- TOOLS: 'tools'
-} as const;
-
-export const ROUTES = {
- /** Chat base — for dynamic chat URLs use RouterService. */
- CHAT: '#/chat',
- /** MCP servers. */
- MCP_SERVERS: '#/mcp-servers',
- /** New chat — root with new chat query param. */
- NEW_CHAT: `?${URL_PARAMS.NEW_CHAT}=true#/`,
- /** Search — mobile-only full-page conversation search. */
- SEARCH: '#/search',
- /** Settings base — for dynamic settings URLs use RouterService. */
- SETTINGS: '#/settings',
- /** Root — start of the app. */
- START: '#/'
-} as const;
--- /dev/null
+import { BuiltInTool } from '$lib/enums';
+
+export const SANDBOX_TOOL_NAME = BuiltInTool.RUN_JAVASCRIPT;
+
+export const SANDBOX_TIMEOUT_MS_DEFAULT = 10000;
+
+export const SANDBOX_TIMEOUT_MS_MAX = 30000;
+
+export const SANDBOX_OUTPUT_MAX_CHARS = 8192;
+
+export const SANDBOX_EMPTY_OUTPUT = '(no output)';
+
+export const SANDBOX_TRUNCATION_NOTICE = '[output truncated]';
+++ /dev/null
-import { BuiltInTool, JsonSchemaType, ToolCallType } from '$lib/enums';
-import type { OpenAIToolDefinition } from '$lib/types';
-
-export const SANDBOX_TOOL_NAME = BuiltInTool.RUN_JAVASCRIPT;
-
-export const SANDBOX_TIMEOUT_MS_DEFAULT = 10000;
-
-export const SANDBOX_TIMEOUT_MS_MAX = 30000;
-
-export const SANDBOX_OUTPUT_MAX_CHARS = 8192;
-
-export const SANDBOX_EMPTY_OUTPUT = '(no output)';
-
-export const SANDBOX_TRUNCATION_NOTICE = '[output truncated]';
-
-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);
--- /dev/null
+/**
+ * Settings key constants for ChatSettings configuration.
+ *
+ * These keys correspond to properties in SettingsConfigType and are used
+ * in settings field configurations to ensure consistency.
+ */
+export const SETTINGS_KEYS = {
+ AGENTIC_MAX_TURNS: 'agenticMaxTurns',
+ ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
+ ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
+ API_KEY: 'apiKey',
+ AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
+ BACKEND_SAMPLING: 'backend_sampling',
+ COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
+ CUSTOM_CSS: 'customCss',
+ // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
+ CUSTOM_JSON: 'customJson',
+ DISABLE_AUTO_SCROLL: 'disableAutoScroll',
+ // Developer
+ DISABLE_REASONING_PARSING: 'disableReasoningParsing',
+ DRY_ALLOWED_LENGTH: 'dry_allowed_length',
+ DRY_BASE: 'dry_base',
+ DRY_MULTIPLIER: 'dry_multiplier',
+ DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
+ DYNATEMP_EXPONENT: 'dynatemp_exponent',
+ DYNATEMP_RANGE: 'dynatemp_range',
+ ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
+ EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
+ FREQUENCY_PENALTY: 'frequency_penalty',
+ FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
+ JS_SANDBOX_ENABLED: 'jsSandboxEnabled',
+ MAX_IMAGE_RESOLUTION: 'maxImageMPixels',
+ MAX_TOKENS: 'max_tokens',
+ MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
+ // MCP
+ MCP_SERVERS: 'mcpServers',
+ MENTION_SEARCH_MAX_DEPTH: 'mentionSearchMaxDepth',
+ MIN_P: 'min_p',
+ PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
+ PDF_AS_IMAGE: 'pdfAsImage',
+ // Performance
+ PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
+ PRESENCE_PENALTY: 'presence_penalty',
+ RENDER_THINKING_AS_MARKDOWN: 'renderThinkingAsMarkdown',
+ RENDER_USER_CONTENT_AS_MARKDOWN: 'renderUserContentAsMarkdown',
+ // Penalties
+ REPEAT_LAST_N: 'repeat_last_n',
+ REPEAT_PENALTY: 'repeat_penalty',
+ SAMPLERS: 'samplers',
+ SEND_ON_ENTER: 'sendOnEnter',
+ SHOW_AGENTIC_TURN_STATS: 'showAgenticTurnStats',
+ SHOW_BUILD_VERSION: 'showBuildVersion',
+ SHOW_FULL_PATH_IN_MENTIONS: 'showFullPathInMentions',
+ // Display
+ SHOW_MESSAGE_STATS: 'showMessageStats',
+ SHOW_MODEL_QUANTIZATION: 'showModelQuantization',
+ SHOW_MODEL_TAGS: 'showModelTags',
+ SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
+ SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
+ SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
+ SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
+ SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
+ SYSTEM_MESSAGE: 'systemMessage',
+ // Sampling
+ TEMPERATURE: 'temperature',
+ // General
+ THEME: 'theme',
+ TITLE_GENERATION_PROMPT: 'titleGenerationPrompt',
+ TITLE_GENERATION_USE_FIRST_LINE: 'titleGenerationUseFirstLine',
+ TITLE_GENERATION_USE_LLM: 'titleGenerationUseLLM',
+ TOP_K: 'top_k',
+ TOP_P: 'top_p',
+ TYP_P: 'typ_p',
+ XTC_PROBABILITY: 'xtc_probability',
+ XTC_THRESHOLD: 'xtc_threshold'
+} as const;
+++ /dev/null
-/**
- * Settings key constants for ChatSettings configuration.
- *
- * These keys correspond to properties in SettingsConfigType and are used
- * in settings field configurations to ensure consistency.
- */
-export const SETTINGS_KEYS = {
- AGENTIC_MAX_TURNS: 'agenticMaxTurns',
- ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
- ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
- API_KEY: 'apiKey',
- AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
- BACKEND_SAMPLING: 'backend_sampling',
- COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
- CUSTOM_CSS: 'customCss',
- // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
- CUSTOM_JSON: 'customJson',
- DISABLE_AUTO_SCROLL: 'disableAutoScroll',
- // Developer
- DISABLE_REASONING_PARSING: 'disableReasoningParsing',
- DRY_ALLOWED_LENGTH: 'dry_allowed_length',
- DRY_BASE: 'dry_base',
- DRY_MULTIPLIER: 'dry_multiplier',
- DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
- DYNATEMP_EXPONENT: 'dynatemp_exponent',
- DYNATEMP_RANGE: 'dynatemp_range',
- ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
- EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
- FREQUENCY_PENALTY: 'frequency_penalty',
- FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
- JS_SANDBOX_ENABLED: 'jsSandboxEnabled',
- MAX_IMAGE_RESOLUTION: 'maxImageMPixels',
- MAX_TOKENS: 'max_tokens',
- MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
- // MCP
- MCP_SERVERS: 'mcpServers',
- MENTION_SEARCH_MAX_DEPTH: 'mentionSearchMaxDepth',
- MIN_P: 'min_p',
- PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
- PDF_AS_IMAGE: 'pdfAsImage',
- // Performance
- PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
- PRESENCE_PENALTY: 'presence_penalty',
- RENDER_THINKING_AS_MARKDOWN: 'renderThinkingAsMarkdown',
- RENDER_USER_CONTENT_AS_MARKDOWN: 'renderUserContentAsMarkdown',
- // Penalties
- REPEAT_LAST_N: 'repeat_last_n',
- REPEAT_PENALTY: 'repeat_penalty',
- SAMPLERS: 'samplers',
- SEND_ON_ENTER: 'sendOnEnter',
- SHOW_AGENTIC_TURN_STATS: 'showAgenticTurnStats',
- SHOW_BUILD_VERSION: 'showBuildVersion',
- SHOW_FULL_PATH_IN_MENTIONS: 'showFullPathInMentions',
- // Display
- SHOW_MESSAGE_STATS: 'showMessageStats',
- SHOW_MODEL_QUANTIZATION: 'showModelQuantization',
- SHOW_MODEL_TAGS: 'showModelTags',
- SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
- SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
- SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
- SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
- SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
- SYSTEM_MESSAGE: 'systemMessage',
- // Sampling
- TEMPERATURE: 'temperature',
- // General
- THEME: 'theme',
- TITLE_GENERATION_PROMPT: 'titleGenerationPrompt',
- TITLE_GENERATION_USE_FIRST_LINE: 'titleGenerationUseFirstLine',
- TITLE_GENERATION_USE_LLM: 'titleGenerationUseLLM',
- TOP_K: 'top_k',
- TOP_P: 'top_p',
- TYP_P: 'typ_p',
- XTC_PROBABILITY: 'xtc_probability',
- XTC_THRESHOLD: 'xtc_threshold'
-} as const;
--- /dev/null
+import { CLI_FLAGS } from './cli-flags.constants';
+import { DEFAULT_MCP_CONFIG } from './mcp.constants';
+import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes.constants';
+import { SETTINGS_KEYS } from './settings-keys.constants';
+import { TITLE_GENERATION } from './title-generation.constants';
+import { FILE_GLOB_SEARCH_PICKERS } from './working-directory.constants';
+import {
+ AlertTriangle,
+ Code,
+ Database,
+ Funnel,
+ ListRestart,
+ Monitor,
+ Moon,
+ PencilRuler,
+ Sliders,
+ Sun
+} from '@lucide/svelte';
+import { SyncableParameterType } from '$lib/enums';
+import { SettingsFieldType } from '$lib/enums/settings.enums';
+import { ColorMode } from '$lib/enums/ui.enums';
+import type {
+ SettingsConfigValue,
+ SettingsEntry,
+ SettingsSection,
+ SettingsSectionEntry,
+ SettingsSectionTitle,
+ SyncableParameter
+} from '$lib/types';
+import type { Component } from 'svelte';
+
+export const SETTINGS_SECTION_TITLES = {
+ AGENTIC: 'Agentic',
+ DEVELOPER: 'Developer',
+ DISPLAY: 'Display',
+ GENERAL: 'General',
+ IMPORT_EXPORT: 'Import/Export',
+ PENALTIES: 'Penalties',
+ SAMPLING: 'Sampling',
+ TOOLS: 'Tools'
+} as const;
+
+const STANDALONE_SECTIONS: { title: SettingsSectionTitle; slug: string; icon: Component }[] = [
+ { icon: PencilRuler, slug: SETTINGS_SECTION_SLUGS.TOOLS, title: SETTINGS_SECTION_TITLES.TOOLS },
+ {
+ icon: Database,
+ slug: SETTINGS_SECTION_SLUGS.IMPORT_EXPORT,
+ title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT
+ }
+];
+const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component }> = [
+ { icon: Monitor, label: 'System', value: ColorMode.SYSTEM },
+ { icon: Sun, label: 'Light', value: ColorMode.LIGHT },
+ { icon: Moon, label: 'Dark', value: ColorMode.DARK }
+];
+// Shared options for the title-generation radio group. Both paired registry entries
+// (USE_FIRST_LINE, USE_LLM) reference this list so labels stay in lockstep.
+const TITLE_GENERATION_RADIO_OPTIONS: Array<{
+ value: string;
+ label: string;
+ key: string;
+ isExperimental?: boolean;
+}> = [
+ {
+ key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
+ label: 'Use first non-empty line for the conversation title',
+ value: 'firstLine'
+ },
+ {
+ isExperimental: true,
+ key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
+ label: 'Generate title with LLM',
+ value: 'llm'
+ }
+];
+// Common shape for the conversation title radio entry.
+const TITLE_GENERATION_BASE = {
+ radioOptions: TITLE_GENERATION_RADIO_OPTIONS,
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.RADIO
+} as const;
+const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
+ [SETTINGS_SECTION_SLUGS.AGENTIC]: {
+ icon: ListRestart,
+ settings: [
+ {
+ defaultValue: 10,
+ help: 'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
+ isPositiveInteger: true,
+ key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
+ label: 'Agentic turns',
+ section: SETTINGS_SECTION_SLUGS.AGENTIC,
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
+ help: 'Timeout for individual MCP tool calls.',
+ isPositiveInteger: true,
+ key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
+ label: 'MCP request timeout (seconds)',
+ section: SETTINGS_SECTION_SLUGS.AGENTIC,
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: FILE_GLOB_SEARCH_PICKERS.DEFAULT_SEARCH_DEPTH,
+ help: 'How many directory levels below the working directory the @-mention file search descends. Larger values surface deeply nested files but take longer on large trees.',
+ isPositiveInteger: true,
+ key: SETTINGS_KEYS.MENTION_SEARCH_MAX_DEPTH,
+ label: 'Mention search depth',
+ max: FILE_GLOB_SEARCH_PICKERS.MAX_SEARCH_DEPTH,
+ min: 1,
+ placeholder: `${FILE_GLOB_SEARCH_PICKERS.DEFAULT_SEARCH_DEPTH}`,
+ section: SETTINGS_SECTION_SLUGS.AGENTIC,
+ type: SettingsFieldType.INPUT
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.AGENTIC,
+ title: SETTINGS_SECTION_TITLES.AGENTIC
+ },
+ [SETTINGS_SECTION_SLUGS.DEVELOPER]: {
+ icon: Code,
+ settings: [
+ {
+ defaultValue: false,
+ help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
+ key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
+ label: 'Pre-fill KV cache after response',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
+ key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
+ label: 'Disable reasoning content parsing',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
+ key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
+ label: 'Exclude reasoning from context',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
+ key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
+ label: 'Enable raw output toggle',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Expose a run_javascript tool to the model. Code runs in a Web Worker inside a sandboxed iframe with an opaque origin, isolated from the WebUI and its API, with a hard timeout.',
+ key: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
+ label: 'JavaScript sandbox tool',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ dependsOn: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
+ help: 'Pre-load nerdamer in the sandbox for symbolic computation: simplify, diff, integrate, solve, and more. Requires "JavaScript sandbox tool" to be enabled.',
+ key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED,
+ label: 'Symbolic math (nerdamer)',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: '',
+ help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
+ key: SETTINGS_KEYS.CUSTOM_JSON,
+ label: 'Custom JSON',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.TEXTAREA
+ },
+ {
+ defaultValue: '',
+ help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
+ key: SETTINGS_KEYS.CUSTOM_CSS,
+ label: 'Custom CSS',
+ section: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ type: SettingsFieldType.TEXTAREA
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.DEVELOPER,
+ title: SETTINGS_SECTION_TITLES.DEVELOPER
+ },
+ [SETTINGS_SECTION_SLUGS.DISPLAY]: {
+ icon: Monitor,
+ settings: [
+ {
+ defaultValue: true,
+ help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
+ key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
+ label: 'Show message generation statistics',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ dependsOn: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
+ help: 'Display per-turn statistics (tokens, duration) under each turn in agentic responses. Shown only when "Show message generation statistics" is enabled.',
+ key: SETTINGS_KEYS.SHOW_AGENTIC_TURN_STATS,
+ label: 'Show statistics for individual agentic turns',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: true,
+ help: 'Expand thought process by default when generating messages.',
+ key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
+ label: 'Show thought in progress',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
+ key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
+ label: 'Always show tool call content',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: true,
+ help: 'Render user messages using markdown formatting in the chat. Turn this off to keep a message exactly as typed; @-mention badges show either way.',
+ key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
+ label: 'Render user content as Markdown',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: true,
+ help: 'Render the reasoning/thinking block content as formatted Markdown instead of plain text.',
+ key: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN,
+ label: 'Render thinking as Markdown',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Always display code blocks at their full natural height, overriding any height limits.',
+ key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
+ label: 'Use full height code blocks',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
+ key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
+ label: 'Disable automatic scroll',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
+ key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
+ label: 'Always show sidebar on desktop',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
+ key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
+ label: 'Show raw model names',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: true,
+ help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.',
+ key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION,
+ label: 'Show model quantization information',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: true,
+ help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.',
+ key: SETTINGS_KEYS.SHOW_MODEL_TAGS,
+ label: 'Show model tags',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Display the current build version in the bottom-right corner of the interface.',
+ key: SETTINGS_KEYS.SHOW_BUILD_VERSION,
+ label: 'Show build version information',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Display the full file system path inside file and folder @-mention badges instead of just the file or folder name.',
+ key: SETTINGS_KEYS.SHOW_FULL_PATH_IN_MENTIONS,
+ label: 'Show full path in mentions',
+ section: SETTINGS_SECTION_SLUGS.DISPLAY,
+ type: SettingsFieldType.CHECKBOX
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.DISPLAY,
+ title: SETTINGS_SECTION_TITLES.DISPLAY
+ },
+ [SETTINGS_SECTION_SLUGS.GENERAL]: {
+ icon: Sliders,
+ settings: [
+ {
+ defaultValue: ColorMode.SYSTEM,
+ help: 'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
+ key: SETTINGS_KEYS.THEME,
+ label: 'Theme',
+ options: COLOR_MODE_OPTIONS,
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.SELECT
+ },
+ {
+ defaultValue: '',
+ help: `Set the API Key if you are using <code> ${CLI_FLAGS.API_KEY} </code> option for the server.`,
+ key: SETTINGS_KEYS.API_KEY,
+ label: 'API Key',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: '',
+ help: 'The starting message that defines how model should behave.',
+ key: SETTINGS_KEYS.SYSTEM_MESSAGE,
+ label: 'System Message',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.TEXTAREA
+ },
+ {
+ defaultValue: 2500,
+ help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
+ key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
+ label: 'Paste long text to file length',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: true,
+ help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
+ key: SETTINGS_KEYS.SEND_ON_ENTER,
+ label: 'Send message on Enter',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
+ isExperimental: true,
+ key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
+ label: 'Show microphone on empty input',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Enable "Continue" button for assistant messages, including reasoning models.',
+ isExperimental: true,
+ key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
+ label: 'Enable "Continue" button',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ ...TITLE_GENERATION_BASE,
+ defaultValue: true,
+ help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.',
+ key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
+ label: 'Conversation title'
+ },
+ {
+ defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
+ dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
+ help: 'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
+ key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
+ label: 'LLM title generation prompt',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.TEXTAREA
+ },
+ {
+ defaultValue: false,
+ help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
+ key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
+ label: 'Copy text attachments as plain text',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: false,
+ help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
+ key: SETTINGS_KEYS.PDF_AS_IMAGE,
+ label: 'Parse PDF as image',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: 0,
+ help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.',
+ key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
+ label: 'Maximum image resolution (megapixels)',
+ section: SETTINGS_SECTION_SLUGS.GENERAL,
+ type: SettingsFieldType.INPUT
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.GENERAL,
+ title: SETTINGS_SECTION_TITLES.GENERAL
+ },
+ [SETTINGS_SECTION_SLUGS.PENALTIES]: {
+ icon: AlertTriangle,
+ settings: [
+ {
+ defaultValue: undefined,
+ help: 'Last n tokens to consider for penalizing repetition',
+ key: SETTINGS_KEYS.REPEAT_LAST_N,
+ label: 'Repeat last N',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.REPEAT_LAST_N
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Controls the repetition of token sequences in the generated text',
+ key: SETTINGS_KEYS.REPEAT_PENALTY,
+ label: 'Repeat penalty',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.REPEAT_PENALTY
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Limits tokens based on whether they appear in the output or not.',
+ key: SETTINGS_KEYS.PRESENCE_PENALTY,
+ label: 'Presence penalty',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.PRESENCE_PENALTY
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Limits tokens based on how often they appear in the output.',
+ key: SETTINGS_KEYS.FREQUENCY_PENALTY,
+ label: 'Frequency penalty',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.FREQUENCY_PENALTY
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
+ key: SETTINGS_KEYS.DRY_MULTIPLIER,
+ label: 'DRY multiplier',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.DRY_MULTIPLIER
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
+ key: SETTINGS_KEYS.DRY_BASE,
+ label: 'DRY base',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.DRY_BASE },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
+ key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
+ label: 'DRY allowed length',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.DRY_ALLOWED_LENGTH
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
+ key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
+ label: 'DRY penalty last N',
+ section: SETTINGS_SECTION_SLUGS.PENALTIES,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.DRY_PENALTY_LAST_N
+ },
+ type: SettingsFieldType.INPUT
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.PENALTIES,
+ title: SETTINGS_SECTION_TITLES.PENALTIES
+ },
+ [SETTINGS_SECTION_SLUGS.SAMPLING]: {
+ icon: Funnel,
+ settings: [
+ {
+ defaultValue: undefined,
+ help: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
+ key: SETTINGS_KEYS.TEMPERATURE,
+ label: 'Temperature',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.TEMPERATURE
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
+ key: SETTINGS_KEYS.DYNATEMP_RANGE,
+ label: 'Dynamic temperature range',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.DYNATEMP_RANGE
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
+ key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
+ label: 'Dynamic temperature exponent',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.DYNATEMP_EXPONENT
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Keeps only k top tokens.',
+ key: SETTINGS_KEYS.TOP_K,
+ label: 'Top K',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TOP_K },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Limits tokens to those that together have a cumulative probability of at least p',
+ key: SETTINGS_KEYS.TOP_P,
+ label: 'Top P',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TOP_P },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
+ key: SETTINGS_KEYS.MIN_P,
+ label: 'Min P',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.MIN_P },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
+ key: SETTINGS_KEYS.XTC_PROBABILITY,
+ label: 'XTC probability',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.XTC_PROBABILITY
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
+ key: SETTINGS_KEYS.XTC_THRESHOLD,
+ label: 'XTC threshold',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.XTC_THRESHOLD
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
+ key: SETTINGS_KEYS.TYP_P,
+ label: 'Typical P',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TYP_P },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: undefined,
+ help: 'The maximum number of token per output. Use -1 for infinite (no limit).',
+ key: SETTINGS_KEYS.MAX_TOKENS,
+ label: 'Max tokens',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: {
+ paramType: SyncableParameterType.NUMBER,
+ serverKey: SETTINGS_KEYS.MAX_TOKENS
+ },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: '',
+ help: 'The order at which samplers are applied, in simplified way. Default is "top_k;typ_p;top_p;min_p;temperature": top_k->typ_p->top_p->min_p->temperature',
+ key: SETTINGS_KEYS.SAMPLERS,
+ label: 'Samplers',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ sync: { paramType: SyncableParameterType.STRING, serverKey: SETTINGS_KEYS.SAMPLERS },
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: false,
+ help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
+ key: SETTINGS_KEYS.BACKEND_SAMPLING,
+ label: 'Backend sampling',
+ section: SETTINGS_SECTION_SLUGS.SAMPLING,
+ type: SettingsFieldType.CHECKBOX
+ }
+ ],
+ slug: SETTINGS_SECTION_SLUGS.SAMPLING,
+ title: SETTINGS_SECTION_TITLES.SAMPLING
+ }
+} as const;
+const NON_UI_SETTINGS: SettingsEntry[] = [
+ {
+ defaultValue: true,
+ help: 'Display the system message at the top of each conversation.',
+ key: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE,
+ label: 'Show system message',
+ type: SettingsFieldType.CHECKBOX
+ },
+ {
+ defaultValue: '[]',
+ help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
+ key: SETTINGS_KEYS.MCP_SERVERS,
+ label: 'MCP servers',
+ type: SettingsFieldType.INPUT
+ },
+ {
+ defaultValue: false,
+ help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
+ key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
+ label: 'Generate title with LLM',
+ type: SettingsFieldType.CHECKBOX
+ }
+ // {
+ // key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
+ // label: 'Python interpreter enabled',
+ // help: 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
+ // defaultValue: false,
+ // type: SettingsFieldType.CHECKBOX,
+ // isExperimental: true,
+ //
+ // }
+];
+
+function getAllSettings(): SettingsEntry[] {
+ const result: SettingsEntry[] = [];
+
+ for (const section of Object.values(SETTINGS_REGISTRY)) {
+ result.push(...section.settings);
+ }
+ result.push(...NON_UI_SETTINGS);
+
+ return result;
+}
+
+/** Flat config object stored in localStorage. */
+export const SETTING_CONFIG_DEFAULT: Record<string, SettingsConfigValue> = Object.fromEntries(
+ getAllSettings().map((s) => [s.key, s.defaultValue])
+) as Record<string, SettingsConfigValue>;
+
+/** Help text for every setting (including non-UI). */
+export const SETTING_CONFIG_INFO: Record<string, string> = Object.fromEntries(
+ getAllSettings().map((s) => [s.key, s.help])
+) as Record<string, string>;
+
+/** Theme select options. */
+export const SETTINGS_COLOR_MODES_CONFIG = COLOR_MODE_OPTIONS;
+
+/** Sidebar sections + field configs (as consumed by UI). */
+export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
+ ...Object.values(SETTINGS_REGISTRY).map((section) => ({
+ fields: section.settings.map((s) => ({
+ dependsOn: s.dependsOn,
+ help: s.help,
+ isExperimental: s.isExperimental,
+ isPositiveInteger: s.isPositiveInteger,
+ key: s.key,
+ label: s.label,
+ max: s.max,
+ min: s.min,
+ options: s.options,
+ placeholder: s.placeholder,
+ radioOptions: s.radioOptions,
+ type: s.type
+ })),
+ icon: section.icon,
+ slug: section.slug,
+ title: section.title
+ })),
+ ...STANDALONE_SECTIONS
+];
+
+/** INPUT-type settings whose value is a number. */
+export const NUMERIC_FIELDS = getAllSettings()
+ .filter((s) => s.type === SettingsFieldType.INPUT && typeof s.defaultValue !== 'string')
+ .map((s) => s.key) as readonly string[];
+
+/** Numeric fields clamped to ≥ 1 and rounded. */
+export const POSITIVE_INTEGER_FIELDS = getAllSettings()
+ .filter((s) => s.isPositiveInteger)
+ .map((s) => s.key) as readonly string[];
+
+/** Derived for the parameter sync service. */
+export const SYNCABLE_PARAMETERS: SyncableParameter[] = getAllSettings()
+ .filter((s) => s.sync !== undefined)
+ .map((s) => ({
+ canSync: true,
+ key: s.key,
+ serverKey: s.sync!.serverKey,
+ type: s.sync!.paramType
+ }));
+
+export const SETTINGS_FALLBACK_EXIT_ROUTE = ROUTES.START;
+++ /dev/null
-import { CLI_FLAGS } from './cli-flags';
-import { DEFAULT_MCP_CONFIG } from './mcp';
-import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes';
-import { SETTINGS_KEYS } from './settings-keys';
-import { TITLE_GENERATION } from './title-generation';
-import {
- FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
- FILE_GLOB_SEARCH_PICKERS_MAX_SEARCH_DEPTH
-} from './working-directory';
-import {
- AlertTriangle,
- Code,
- Database,
- Funnel,
- ListRestart,
- Monitor,
- Monitor as MonitorIcon,
- Moon,
- PencilRuler,
- Sliders,
- Sun
-} from '@lucide/svelte';
-import { SyncableParameterType } from '$lib/enums';
-import { SettingsFieldType } from '$lib/enums/settings.enums';
-import { ColorMode } from '$lib/enums/ui.enums';
-import type {
- SettingsConfigValue,
- SettingsEntry,
- SettingsSection,
- SettingsSectionEntry,
- SettingsSectionTitle,
- SyncableParameter
-} from '$lib/types';
-import type { Component } from 'svelte';
-
-export const SETTINGS_SECTION_TITLES = {
- AGENTIC: 'Agentic',
- DEVELOPER: 'Developer',
- DISPLAY: 'Display',
- GENERAL: 'General',
- IMPORT_EXPORT: 'Import/Export',
- PENALTIES: 'Penalties',
- SAMPLING: 'Sampling',
- TOOLS: 'Tools'
-} as const;
-
-const STANDALONE_SECTIONS: { title: SettingsSectionTitle; slug: string; icon: Component }[] = [
- { icon: PencilRuler, slug: SETTINGS_SECTION_SLUGS.TOOLS, title: SETTINGS_SECTION_TITLES.TOOLS },
- {
- icon: Database,
- slug: SETTINGS_SECTION_SLUGS.IMPORT_EXPORT,
- title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT
- }
-];
-const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component }> = [
- { icon: MonitorIcon, label: 'System', value: ColorMode.SYSTEM },
- { icon: Sun, label: 'Light', value: ColorMode.LIGHT },
- { icon: Moon, label: 'Dark', value: ColorMode.DARK }
-];
-// Shared options for the title-generation radio group. Both paired registry entries
-// (USE_FIRST_LINE, USE_LLM) reference this list so labels stay in lockstep.
-const TITLE_GENERATION_RADIO_OPTIONS: Array<{
- value: string;
- label: string;
- key: string;
- isExperimental?: boolean;
-}> = [
- {
- key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
- label: 'Use first non-empty line for the conversation title',
- value: 'firstLine'
- },
- {
- isExperimental: true,
- key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
- label: 'Generate title with LLM',
- value: 'llm'
- }
-];
-// Common shape for the conversation title radio entry.
-const TITLE_GENERATION_BASE = {
- radioOptions: TITLE_GENERATION_RADIO_OPTIONS,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.RADIO
-} as const;
-const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
- [SETTINGS_SECTION_SLUGS.AGENTIC]: {
- icon: ListRestart,
- settings: [
- {
- defaultValue: 10,
- help: 'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
- isPositiveInteger: true,
- key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
- label: 'Agentic turns',
- section: SETTINGS_SECTION_SLUGS.AGENTIC,
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
- help: 'Timeout for individual MCP tool calls.',
- isPositiveInteger: true,
- key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
- label: 'MCP request timeout (seconds)',
- section: SETTINGS_SECTION_SLUGS.AGENTIC,
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
- help: 'How many directory levels below the working directory the @-mention file search descends. Larger values surface deeply nested files but take longer on large trees.',
- isPositiveInteger: true,
- key: SETTINGS_KEYS.MENTION_SEARCH_MAX_DEPTH,
- label: 'Mention search depth',
- max: FILE_GLOB_SEARCH_PICKERS_MAX_SEARCH_DEPTH,
- min: 1,
- placeholder: `${FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH}`,
- section: SETTINGS_SECTION_SLUGS.AGENTIC,
- type: SettingsFieldType.INPUT
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.AGENTIC,
- title: SETTINGS_SECTION_TITLES.AGENTIC
- },
- [SETTINGS_SECTION_SLUGS.DEVELOPER]: {
- icon: Code,
- settings: [
- {
- defaultValue: false,
- help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.',
- key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
- label: 'Pre-fill KV cache after response',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
- key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
- label: 'Disable reasoning content parsing',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.',
- key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
- label: 'Exclude reasoning from context',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
- key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
- label: 'Enable raw output toggle',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Expose a run_javascript tool to the model. Code runs in a Web Worker inside a sandboxed iframe with an opaque origin, isolated from the WebUI and its API, with a hard timeout.',
- key: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
- label: 'JavaScript sandbox tool',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- dependsOn: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
- help: 'Pre-load nerdamer in the sandbox for symbolic computation: simplify, diff, integrate, solve, and more. Requires "JavaScript sandbox tool" to be enabled.',
- key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED,
- label: 'Symbolic math (nerdamer)',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: '',
- help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
- key: SETTINGS_KEYS.CUSTOM_JSON,
- label: 'Custom JSON',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.TEXTAREA
- },
- {
- defaultValue: '',
- help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
- key: SETTINGS_KEYS.CUSTOM_CSS,
- label: 'Custom CSS',
- section: SETTINGS_SECTION_SLUGS.DEVELOPER,
- type: SettingsFieldType.TEXTAREA
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.DEVELOPER,
- title: SETTINGS_SECTION_TITLES.DEVELOPER
- },
- [SETTINGS_SECTION_SLUGS.DISPLAY]: {
- icon: Monitor,
- settings: [
- {
- defaultValue: true,
- help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
- key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
- label: 'Show message generation statistics',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- dependsOn: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
- help: 'Display per-turn statistics (tokens, duration) under each turn in agentic responses. Shown only when "Show message generation statistics" is enabled.',
- key: SETTINGS_KEYS.SHOW_AGENTIC_TURN_STATS,
- label: 'Show statistics for individual agentic turns',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: true,
- help: 'Expand thought process by default when generating messages.',
- key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
- label: 'Show thought in progress',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
- key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT,
- label: 'Always show tool call content',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: true,
- help: 'Render user messages using markdown formatting in the chat. Turn this off to keep a message exactly as typed; @-mention badges show either way.',
- key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
- label: 'Render user content as Markdown',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: true,
- help: 'Render the reasoning/thinking block content as formatted Markdown instead of plain text.',
- key: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN,
- label: 'Render thinking as Markdown',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Always display code blocks at their full natural height, overriding any height limits.',
- key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS,
- label: 'Use full height code blocks',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
- key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
- label: 'Disable automatic scroll',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
- key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
- label: 'Always show sidebar on desktop',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
- key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES,
- label: 'Show raw model names',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: true,
- help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.',
- key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION,
- label: 'Show model quantization information',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: true,
- help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.',
- key: SETTINGS_KEYS.SHOW_MODEL_TAGS,
- label: 'Show model tags',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Display the current build version in the bottom-right corner of the interface.',
- key: SETTINGS_KEYS.SHOW_BUILD_VERSION,
- label: 'Show build version information',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Display the full file system path inside file and folder @-mention badges instead of just the file or folder name.',
- key: SETTINGS_KEYS.SHOW_FULL_PATH_IN_MENTIONS,
- label: 'Show full path in mentions',
- section: SETTINGS_SECTION_SLUGS.DISPLAY,
- type: SettingsFieldType.CHECKBOX
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.DISPLAY,
- title: SETTINGS_SECTION_TITLES.DISPLAY
- },
- [SETTINGS_SECTION_SLUGS.GENERAL]: {
- icon: Sliders,
- settings: [
- {
- defaultValue: ColorMode.SYSTEM,
- help: 'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
- key: SETTINGS_KEYS.THEME,
- label: 'Theme',
- options: COLOR_MODE_OPTIONS,
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.SELECT
- },
- {
- defaultValue: '',
- help: `Set the API Key if you are using <code> ${CLI_FLAGS.API_KEY} </code> option for the server.`,
- key: SETTINGS_KEYS.API_KEY,
- label: 'API Key',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: '',
- help: 'The starting message that defines how model should behave.',
- key: SETTINGS_KEYS.SYSTEM_MESSAGE,
- label: 'System Message',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.TEXTAREA
- },
- {
- defaultValue: 2500,
- help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.',
- key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
- label: 'Paste long text to file length',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: true,
- help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
- key: SETTINGS_KEYS.SEND_ON_ENTER,
- label: 'Send message on Enter',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
- isExperimental: true,
- key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
- label: 'Show microphone on empty input',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Enable "Continue" button for assistant messages, including reasoning models.',
- isExperimental: true,
- key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
- label: 'Enable "Continue" button',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.CHECKBOX
- },
- {
- ...TITLE_GENERATION_BASE,
- defaultValue: true,
- help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.',
- key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
- label: 'Conversation title'
- },
- {
- defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
- dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
- help: 'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
- key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
- label: 'LLM title generation prompt',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.TEXTAREA
- },
- {
- defaultValue: false,
- help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.',
- key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
- label: 'Copy text attachments as plain text',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: false,
- help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
- key: SETTINGS_KEYS.PDF_AS_IMAGE,
- label: 'Parse PDF as image',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: 0,
- help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.',
- key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
- label: 'Maximum image resolution (megapixels)',
- section: SETTINGS_SECTION_SLUGS.GENERAL,
- type: SettingsFieldType.INPUT
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.GENERAL,
- title: SETTINGS_SECTION_TITLES.GENERAL
- },
- [SETTINGS_SECTION_SLUGS.PENALTIES]: {
- icon: AlertTriangle,
- settings: [
- {
- defaultValue: undefined,
- help: 'Last n tokens to consider for penalizing repetition',
- key: SETTINGS_KEYS.REPEAT_LAST_N,
- label: 'Repeat last N',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.REPEAT_LAST_N
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Controls the repetition of token sequences in the generated text',
- key: SETTINGS_KEYS.REPEAT_PENALTY,
- label: 'Repeat penalty',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.REPEAT_PENALTY
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Limits tokens based on whether they appear in the output or not.',
- key: SETTINGS_KEYS.PRESENCE_PENALTY,
- label: 'Presence penalty',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.PRESENCE_PENALTY
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Limits tokens based on how often they appear in the output.',
- key: SETTINGS_KEYS.FREQUENCY_PENALTY,
- label: 'Frequency penalty',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.FREQUENCY_PENALTY
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
- key: SETTINGS_KEYS.DRY_MULTIPLIER,
- label: 'DRY multiplier',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.DRY_MULTIPLIER
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
- key: SETTINGS_KEYS.DRY_BASE,
- label: 'DRY base',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.DRY_BASE },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
- key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
- label: 'DRY allowed length',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.DRY_ALLOWED_LENGTH
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
- key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
- label: 'DRY penalty last N',
- section: SETTINGS_SECTION_SLUGS.PENALTIES,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.DRY_PENALTY_LAST_N
- },
- type: SettingsFieldType.INPUT
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.PENALTIES,
- title: SETTINGS_SECTION_TITLES.PENALTIES
- },
- [SETTINGS_SECTION_SLUGS.SAMPLING]: {
- icon: Funnel,
- settings: [
- {
- defaultValue: undefined,
- help: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
- key: SETTINGS_KEYS.TEMPERATURE,
- label: 'Temperature',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.TEMPERATURE
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
- key: SETTINGS_KEYS.DYNATEMP_RANGE,
- label: 'Dynamic temperature range',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.DYNATEMP_RANGE
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
- key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
- label: 'Dynamic temperature exponent',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.DYNATEMP_EXPONENT
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Keeps only k top tokens.',
- key: SETTINGS_KEYS.TOP_K,
- label: 'Top K',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TOP_K },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Limits tokens to those that together have a cumulative probability of at least p',
- key: SETTINGS_KEYS.TOP_P,
- label: 'Top P',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TOP_P },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
- key: SETTINGS_KEYS.MIN_P,
- label: 'Min P',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.MIN_P },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
- key: SETTINGS_KEYS.XTC_PROBABILITY,
- label: 'XTC probability',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.XTC_PROBABILITY
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
- key: SETTINGS_KEYS.XTC_THRESHOLD,
- label: 'XTC threshold',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.XTC_THRESHOLD
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
- key: SETTINGS_KEYS.TYP_P,
- label: 'Typical P',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: { paramType: SyncableParameterType.NUMBER, serverKey: SETTINGS_KEYS.TYP_P },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: undefined,
- help: 'The maximum number of token per output. Use -1 for infinite (no limit).',
- key: SETTINGS_KEYS.MAX_TOKENS,
- label: 'Max tokens',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: {
- paramType: SyncableParameterType.NUMBER,
- serverKey: SETTINGS_KEYS.MAX_TOKENS
- },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: '',
- help: 'The order at which samplers are applied, in simplified way. Default is "top_k;typ_p;top_p;min_p;temperature": top_k->typ_p->top_p->min_p->temperature',
- key: SETTINGS_KEYS.SAMPLERS,
- label: 'Samplers',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- sync: { paramType: SyncableParameterType.STRING, serverKey: SETTINGS_KEYS.SAMPLERS },
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: false,
- help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
- key: SETTINGS_KEYS.BACKEND_SAMPLING,
- label: 'Backend sampling',
- section: SETTINGS_SECTION_SLUGS.SAMPLING,
- type: SettingsFieldType.CHECKBOX
- }
- ],
- slug: SETTINGS_SECTION_SLUGS.SAMPLING,
- title: SETTINGS_SECTION_TITLES.SAMPLING
- }
-} as const;
-const NON_UI_SETTINGS: SettingsEntry[] = [
- {
- defaultValue: true,
- help: 'Display the system message at the top of each conversation.',
- key: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE,
- label: 'Show system message',
- type: SettingsFieldType.CHECKBOX
- },
- {
- defaultValue: '[]',
- help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
- key: SETTINGS_KEYS.MCP_SERVERS,
- label: 'MCP servers',
- type: SettingsFieldType.INPUT
- },
- {
- defaultValue: false,
- help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
- key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
- label: 'Generate title with LLM',
- type: SettingsFieldType.CHECKBOX
- }
- // {
- // key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED,
- // label: 'Python interpreter enabled',
- // help: 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
- // defaultValue: false,
- // type: SettingsFieldType.CHECKBOX,
- // isExperimental: true,
- //
- // }
-];
-
-function getAllSettings(): SettingsEntry[] {
- const result: SettingsEntry[] = [];
-
- for (const section of Object.values(SETTINGS_REGISTRY)) {
- result.push(...section.settings);
- }
- result.push(...NON_UI_SETTINGS);
-
- return result;
-}
-
-/** Flat config object stored in localStorage. */
-export const SETTING_CONFIG_DEFAULT: Record<string, SettingsConfigValue> = Object.fromEntries(
- getAllSettings().map((s) => [s.key, s.defaultValue])
-) as Record<string, SettingsConfigValue>;
-
-/** Help text for every setting (including non-UI). */
-export const SETTING_CONFIG_INFO: Record<string, string> = Object.fromEntries(
- getAllSettings().map((s) => [s.key, s.help])
-) as Record<string, string>;
-
-/** Theme select options. */
-export const SETTINGS_COLOR_MODES_CONFIG = COLOR_MODE_OPTIONS;
-
-/** Sidebar sections + field configs (as consumed by UI). */
-export const SETTINGS_CHAT_SECTIONS: SettingsSection[] = [
- ...Object.values(SETTINGS_REGISTRY).map((section) => ({
- fields: section.settings.map((s) => ({
- dependsOn: s.dependsOn,
- help: s.help,
- isExperimental: s.isExperimental,
- isPositiveInteger: s.isPositiveInteger,
- key: s.key,
- label: s.label,
- max: s.max,
- min: s.min,
- options: s.options,
- placeholder: s.placeholder,
- radioOptions: s.radioOptions,
- type: s.type
- })),
- icon: section.icon,
- slug: section.slug,
- title: section.title
- })),
- ...STANDALONE_SECTIONS
-];
-
-/** INPUT-type settings whose value is a number. */
-export const NUMERIC_FIELDS = getAllSettings()
- .filter((s) => s.type === SettingsFieldType.INPUT && typeof s.defaultValue !== 'string')
- .map((s) => s.key) as readonly string[];
-
-/** Numeric fields clamped to ≥ 1 and rounded. */
-export const POSITIVE_INTEGER_FIELDS = getAllSettings()
- .filter((s) => s.isPositiveInteger)
- .map((s) => s.key) as readonly string[];
-
-/** Derived for the parameter sync service. */
-export const SYNCABLE_PARAMETERS: SyncableParameter[] = getAllSettings()
- .filter((s) => s.sync !== undefined)
- .map((s) => ({
- canSync: true,
- key: s.key,
- serverKey: s.sync!.serverKey,
- type: s.sync!.paramType
- }));
-
-export const SETTINGS_FALLBACK_EXIT_ROUTE = ROUTES.START;
--- /dev/null
+// Control / whitespace / formatting characters that appear literally inside rendered text.
+
+/** Line feed. */
+export const NEWLINE = '\n';
+
+/** Horizontal tab. */
+export const TAB = '\t';
+
+/** Non-breaking space. */
+export const NBSP = '\u00a0';
+
+/** Non-breaking spaces used to render a tab stop that whitespace collapsing would otherwise squash. */
+export const TAB_AS_SPACES = NBSP.repeat(4);
+
+/** Matches a CR-terminated or bare LF line break. */
+export const LINE_BREAK = /\r?\n/;
+++ /dev/null
-/**
- * Server-sent events wire format, shared by the chat stream and the
- * /models/sse status feed (text/event-stream).
- */
-
-// blank line between two events
-export const SSE_RECORD_SEPARATOR = '\n\n';
-
-// line break inside an event
-export const SSE_LINE_SEPARATOR = '\n';
-
-// data field prefix, the value follows after an optional space
-export const SSE_DATA_PREFIX = 'data:';
-
-// end-of-stream marker on the chat completion stream
-export const SSE_DONE_MARKER = '[DONE]';
--- /dev/null
+/**
+ * Storage-related constants (localStorage, IndexedDB).
+ *
+ * Centralized to ensure consistency across the app and simplify future
+ * name changes.
+ */
+
+/** Name prefix for all localStorage keys */
+export const STORAGE_APP_NAME = 'LlamaUi';
+
+/** Deprecated localStorage key prefix (old app name) */
+export const STORAGE_APP_NAME_DEPRECATED = 'LlamaCppWebui';
+
+/** @deprecated Deprecated IndexedDB name — will be removed after all users have migrated */
+export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui';
+
+export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`;
+export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`;
+export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTools`;
+
+/** Disabled tools keyed by stable selection identity, no migration from the name based key */
+export const DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolKeys`;
+export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`;
+export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`;
+export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`;
+export const DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.dismissedRecommendedMcpServers`;
+
+/** Key prefix for per-conversation resumable stream state, conversationId is appended */
+export const STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX = `${STORAGE_APP_NAME}.streamResume.`;
+
+// Deprecated old key names (kept for backward compat while users migrate)
+/** @deprecated Use {@link ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY} instead */
+export const DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.alwaysAllowedTools`;
+/** @deprecated Use {@link CONFIG_LOCALSTORAGE_KEY} instead */
+export const DEPRECATED_CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.config`;
+/** @deprecated Use {@link DISABLED_TOOLS_LOCALSTORAGE_KEY} instead */
+export const DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.disabledTools`;
+/** @deprecated Use {@link FAVORITE_MODELS_LOCALSTORAGE_KEY} instead */
+export const DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.favoriteModels`;
+/** @deprecated Use {@link USER_OVERRIDES_LOCALSTORAGE_KEY} instead */
+export const DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.userOverrides`;
+
+/** Build version stored in localStorage for non-PWA update detection */
+export const BUILD_VERSION_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.buildVersion`;
+
+/** Maps new keys to their deprecated fallback keys */
+export const NEW_TO_DEPRECATED_MAP: Record<string, string> = {
+ [ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY,
+ [CONFIG_LOCALSTORAGE_KEY]: DEPRECATED_CONFIG_LOCALSTORAGE_KEY,
+ [DISABLED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY,
+ [FAVORITE_MODELS_LOCALSTORAGE_KEY]: DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY,
+ [USER_OVERRIDES_LOCALSTORAGE_KEY]: DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY
+};
+++ /dev/null
-/**
- * Storage-related constants (localStorage, IndexedDB).
- *
- * Centralized to ensure consistency across the app and simplify future
- * name changes.
- */
-
-/** Name prefix for all localStorage keys */
-export const STORAGE_APP_NAME = 'LlamaUi';
-
-/** Deprecated localStorage key prefix (old app name) */
-export const STORAGE_APP_NAME_DEPRECATED = 'LlamaCppWebui';
-
-/** @deprecated Deprecated IndexedDB name — will be removed after all users have migrated */
-export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui';
-
-export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`;
-export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`;
-export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTools`;
-
-/** Disabled tools keyed by stable selection identity, no migration from the name based key */
-export const DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolKeys`;
-export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`;
-export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`;
-export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`;
-export const DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.dismissedRecommendedMcpServers`;
-
-/** Key prefix for per-conversation resumable stream state, conversationId is appended */
-export const STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX = `${STORAGE_APP_NAME}.streamResume.`;
-
-// Deprecated old key names (kept for backward compat while users migrate)
-/** @deprecated Use {@link ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY} instead */
-export const DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.alwaysAllowedTools`;
-/** @deprecated Use {@link CONFIG_LOCALSTORAGE_KEY} instead */
-export const DEPRECATED_CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.config`;
-/** @deprecated Use {@link DISABLED_TOOLS_LOCALSTORAGE_KEY} instead */
-export const DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.disabledTools`;
-/** @deprecated Use {@link FAVORITE_MODELS_LOCALSTORAGE_KEY} instead */
-export const DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.favoriteModels`;
-/** @deprecated Use {@link USER_OVERRIDES_LOCALSTORAGE_KEY} instead */
-export const DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME_DEPRECATED}.userOverrides`;
-
-/** Build version stored in localStorage for non-PWA update detection */
-export const BUILD_VERSION_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.buildVersion`;
-
-/** Maps new keys to their deprecated fallback keys */
-export const NEW_TO_DEPRECATED_MAP: Record<string, string> = {
- [ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY,
- [CONFIG_LOCALSTORAGE_KEY]: DEPRECATED_CONFIG_LOCALSTORAGE_KEY,
- [DISABLED_TOOLS_LOCALSTORAGE_KEY]: DEPRECATED_DISABLED_TOOLS_LOCALSTORAGE_KEY,
- [FAVORITE_MODELS_LOCALSTORAGE_KEY]: DEPRECATED_FAVORITE_MODELS_LOCALSTORAGE_KEY,
- [USER_OVERRIDES_LOCALSTORAGE_KEY]: DEPRECATED_USER_OVERRIDES_LOCALSTORAGE_KEY
-};
--- /dev/null
+// grace window after a visibilitychange before we kick a reader whose socket likely died
+// while the tab was hidden. covers brief background pauses without thrashing live streams
+export const STREAM_VISIBILITY_KICK_MS = 3000;
+
+// separator joining a conversation id and its per-model stream identity
+// suffix (conv::model) used by the server side replay buffer
+export const CONVERSATION_ID_SEPARATOR = '::';
+
+/**
+ * Server-sent events wire format, shared by the chat stream and the
+ * /models/sse status feed (text/event-stream).
+ */
+
+// blank line between two events
+export const SSE_RECORD_SEPARATOR = '\n\n';
+
+// line break inside an event
+export const SSE_LINE_SEPARATOR = '\n';
+
+// data field prefix, the value follows after an optional space
+export const SSE_DATA_PREFIX = 'data:';
+
+// end-of-stream marker on the chat completion stream
+export const SSE_DONE_MARKER = '[DONE]';
+++ /dev/null
-// grace window after a visibilitychange before we kick a reader whose socket likely died
-// while the tab was hidden. covers brief background pauses without thrashing live streams
-export const STREAM_VISIBILITY_KICK_MS = 3000;
--- /dev/null
+/**
+ * Comprehensive dictionary of all supported file types in llama-ui
+ * Organized by category with TypeScript enums for better type safety
+ */
+
+import {
+ FileExtensionAudio,
+ FileExtensionImage,
+ FileExtensionPdf,
+ FileExtensionText,
+ FileTypeAudio,
+ FileTypeImage,
+ FileTypePdf,
+ FileTypeText,
+ MimeTypeApplication,
+ MimeTypeAudio,
+ MimeTypeImage,
+ MimeTypeText,
+ MimeTypeVideo
+} from '$lib/enums';
+import { FileExtensionVideo, FileTypeVideo } from '$lib/enums/files.enums';
+
+// File type configuration using enums
+export const AUDIO_FILE_TYPES = {
+ [FileTypeAudio.MP3]: {
+ extensions: [FileExtensionAudio.MP3],
+ mimeTypes: [MimeTypeAudio.MP3_MPEG, MimeTypeAudio.MP3]
+ },
+ [FileTypeAudio.WAV]: {
+ extensions: [FileExtensionAudio.WAV],
+ mimeTypes: [MimeTypeAudio.WAV]
+ }
+} as const;
+
+export const VIDEO_FILE_TYPES = {
+ [FileTypeVideo.MP4]: {
+ extensions: [FileExtensionVideo.MP4],
+ mimeTypes: [MimeTypeVideo.MP4]
+ },
+ [FileTypeVideo.OGG]: {
+ extensions: [FileExtensionVideo.OGG],
+ mimeTypes: [MimeTypeVideo.OGG]
+ }
+} as const;
+
+export const IMAGE_FILE_TYPES = {
+ [FileTypeImage.GIF]: {
+ extensions: [FileExtensionImage.GIF],
+ mimeTypes: [MimeTypeImage.GIF]
+ },
+ [FileTypeImage.HEIC]: {
+ extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
+ mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
+ },
+ [FileTypeImage.JPEG]: {
+ extensions: [FileExtensionImage.JPG, FileExtensionImage.JPEG],
+ mimeTypes: [MimeTypeImage.JPEG]
+ },
+ [FileTypeImage.PNG]: {
+ extensions: [FileExtensionImage.PNG],
+ mimeTypes: [MimeTypeImage.PNG]
+ },
+ [FileTypeImage.SVG]: {
+ extensions: [FileExtensionImage.SVG],
+ mimeTypes: [MimeTypeImage.SVG]
+ },
+ [FileTypeImage.WEBP]: {
+ extensions: [FileExtensionImage.WEBP],
+ mimeTypes: [MimeTypeImage.WEBP]
+ }
+} as const;
+
+export const PDF_FILE_TYPES = {
+ [FileTypePdf.PDF]: {
+ extensions: [FileExtensionPdf.PDF],
+ mimeTypes: [MimeTypeApplication.PDF]
+ }
+} as const;
+
+export const TEXT_FILE_TYPES = {
+ [FileTypeText.ASCIIDOC]: {
+ extensions: [FileExtensionText.ADOC],
+ mimeTypes: [MimeTypeText.ASCIIDOC]
+ },
+ [FileTypeText.BIBTEX]: {
+ extensions: [FileExtensionText.BIB],
+ mimeTypes: [MimeTypeText.BIBTEX]
+ },
+ [FileTypeText.CPP]: {
+ extensions: [
+ FileExtensionText.CPP,
+ FileExtensionText.C,
+ FileExtensionText.H,
+ FileExtensionText.HPP
+ ],
+ mimeTypes: [MimeTypeText.CPP_SRC, MimeTypeText.CPP_HDR, MimeTypeText.C_SRC, MimeTypeText.C_HDR]
+ },
+ [FileTypeText.CSHARP]: {
+ extensions: [FileExtensionText.CS],
+ mimeTypes: [MimeTypeText.CSHARP]
+ },
+ [FileTypeText.CSS]: {
+ extensions: [FileExtensionText.CSS],
+ mimeTypes: [MimeTypeText.CSS]
+ },
+ [FileTypeText.CSV]: {
+ extensions: [FileExtensionText.CSV],
+ mimeTypes: [MimeTypeText.CSV]
+ },
+ [FileTypeText.CUDA]: {
+ extensions: [FileExtensionText.CU, FileExtensionText.CUH],
+ mimeTypes: [MimeTypeText.CUDA]
+ },
+ [FileTypeText.DART]: {
+ extensions: [FileExtensionText.DART],
+ mimeTypes: [MimeTypeText.DART]
+ },
+ [FileTypeText.GO]: {
+ extensions: [FileExtensionText.GO],
+ mimeTypes: [MimeTypeText.GO]
+ },
+ [FileTypeText.HASKELL]: {
+ extensions: [FileExtensionText.HS],
+ mimeTypes: [MimeTypeText.HASKELL]
+ },
+ [FileTypeText.HTML]: {
+ extensions: [FileExtensionText.HTML, FileExtensionText.HTM],
+ mimeTypes: [MimeTypeText.HTML]
+ },
+ [FileTypeText.JAVA]: {
+ extensions: [FileExtensionText.JAVA],
+ mimeTypes: [MimeTypeText.JAVA]
+ },
+ [FileTypeText.JAVASCRIPT]: {
+ extensions: [FileExtensionText.JS],
+ mimeTypes: [MimeTypeText.JAVASCRIPT, MimeTypeText.JAVASCRIPT_APP]
+ },
+ [FileTypeText.JSON]: {
+ extensions: [FileExtensionText.JSON],
+ mimeTypes: [MimeTypeText.JSON]
+ },
+ [FileTypeText.JSX]: {
+ extensions: [FileExtensionText.JSX],
+ mimeTypes: [MimeTypeText.JSX]
+ },
+ [FileTypeText.KOTLIN]: {
+ extensions: [FileExtensionText.KT],
+ mimeTypes: [MimeTypeText.KOTLIN]
+ },
+ [FileTypeText.LATEX]: {
+ extensions: [FileExtensionText.TEX],
+ mimeTypes: [MimeTypeText.LATEX, MimeTypeText.TEX, MimeTypeText.TEX_APP]
+ },
+ [FileTypeText.LOG]: {
+ extensions: [FileExtensionText.LOG],
+ mimeTypes: [MimeTypeText.PLAIN]
+ },
+ [FileTypeText.MARKDOWN]: {
+ extensions: [FileExtensionText.MD],
+ mimeTypes: [MimeTypeText.MARKDOWN]
+ },
+ [FileTypeText.PHP]: {
+ extensions: [FileExtensionText.PHP],
+ mimeTypes: [MimeTypeText.PHP]
+ },
+ [FileTypeText.PLAIN_TEXT]: {
+ extensions: [FileExtensionText.TXT],
+ mimeTypes: [MimeTypeText.PLAIN]
+ },
+ [FileTypeText.PROPERTIES]: {
+ extensions: [FileExtensionText.PROPERTIES],
+ mimeTypes: [MimeTypeText.PROPERTIES]
+ },
+ [FileTypeText.PYTHON]: {
+ extensions: [FileExtensionText.PY],
+ mimeTypes: [MimeTypeText.PYTHON]
+ },
+ [FileTypeText.R]: {
+ extensions: [FileExtensionText.R],
+ mimeTypes: [MimeTypeText.R]
+ },
+ [FileTypeText.RUBY]: {
+ extensions: [FileExtensionText.RB],
+ mimeTypes: [MimeTypeText.RUBY]
+ },
+ [FileTypeText.RUST]: {
+ extensions: [FileExtensionText.RS],
+ mimeTypes: [MimeTypeText.RUST]
+ },
+ [FileTypeText.SCALA]: {
+ extensions: [FileExtensionText.SCALA],
+ mimeTypes: [MimeTypeText.SCALA]
+ },
+ [FileTypeText.SHELL]: {
+ extensions: [FileExtensionText.SH, FileExtensionText.BAT],
+ mimeTypes: [MimeTypeText.SHELL, MimeTypeText.BAT]
+ },
+ [FileTypeText.SQL]: {
+ extensions: [FileExtensionText.SQL],
+ mimeTypes: [MimeTypeText.SQL]
+ },
+ [FileTypeText.SVELTE]: {
+ extensions: [FileExtensionText.SVELTE],
+ mimeTypes: [MimeTypeText.SVELTE]
+ },
+ [FileTypeText.SWIFT]: {
+ extensions: [FileExtensionText.SWIFT],
+ mimeTypes: [MimeTypeText.SWIFT]
+ },
+ [FileTypeText.TSX]: {
+ extensions: [FileExtensionText.TSX],
+ mimeTypes: [MimeTypeText.TSX]
+ },
+ [FileTypeText.TYPESCRIPT]: {
+ extensions: [FileExtensionText.TS],
+ mimeTypes: [MimeTypeText.TYPESCRIPT]
+ },
+ [FileTypeText.VUE]: {
+ extensions: [FileExtensionText.VUE],
+ mimeTypes: [MimeTypeText.VUE]
+ },
+ [FileTypeText.VULKAN]: {
+ extensions: [FileExtensionText.COMP],
+ mimeTypes: [MimeTypeText.PLAIN]
+ },
+ [FileTypeText.XML]: {
+ extensions: [FileExtensionText.XML],
+ mimeTypes: [MimeTypeText.XML_TEXT, MimeTypeText.XML_APP]
+ },
+ [FileTypeText.YAML]: {
+ extensions: [FileExtensionText.YAML, FileExtensionText.YML],
+ mimeTypes: [MimeTypeText.YAML_TEXT, MimeTypeText.YAML_APP]
+ }
+} as const;
+++ /dev/null
-/**
- * Comprehensive dictionary of all supported file types in llama-ui
- * Organized by category with TypeScript enums for better type safety
- */
-
-import {
- FileExtensionAudio,
- FileExtensionImage,
- FileExtensionPdf,
- FileExtensionText,
- FileTypeAudio,
- FileTypeImage,
- FileTypePdf,
- FileTypeText,
- MimeTypeApplication,
- MimeTypeAudio,
- MimeTypeImage,
- MimeTypeText,
- MimeTypeVideo
-} from '$lib/enums';
-import { FileExtensionVideo, FileTypeVideo } from '$lib/enums/files.enums';
-
-// File type configuration using enums
-export const AUDIO_FILE_TYPES = {
- [FileTypeAudio.MP3]: {
- extensions: [FileExtensionAudio.MP3],
- mimeTypes: [MimeTypeAudio.MP3_MPEG, MimeTypeAudio.MP3]
- },
- [FileTypeAudio.WAV]: {
- extensions: [FileExtensionAudio.WAV],
- mimeTypes: [MimeTypeAudio.WAV]
- }
-} as const;
-
-export const VIDEO_FILE_TYPES = {
- [FileTypeVideo.MP4]: {
- extensions: [FileExtensionVideo.MP4],
- mimeTypes: [MimeTypeVideo.MP4]
- },
- [FileTypeVideo.OGG]: {
- extensions: [FileExtensionVideo.OGG],
- mimeTypes: [MimeTypeVideo.OGG]
- }
-} as const;
-
-export const IMAGE_FILE_TYPES = {
- [FileTypeImage.GIF]: {
- extensions: [FileExtensionImage.GIF],
- mimeTypes: [MimeTypeImage.GIF]
- },
- [FileTypeImage.HEIC]: {
- extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
- mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
- },
- [FileTypeImage.JPEG]: {
- extensions: [FileExtensionImage.JPG, FileExtensionImage.JPEG],
- mimeTypes: [MimeTypeImage.JPEG]
- },
- [FileTypeImage.PNG]: {
- extensions: [FileExtensionImage.PNG],
- mimeTypes: [MimeTypeImage.PNG]
- },
- [FileTypeImage.SVG]: {
- extensions: [FileExtensionImage.SVG],
- mimeTypes: [MimeTypeImage.SVG]
- },
- [FileTypeImage.WEBP]: {
- extensions: [FileExtensionImage.WEBP],
- mimeTypes: [MimeTypeImage.WEBP]
- }
-} as const;
-
-export const PDF_FILE_TYPES = {
- [FileTypePdf.PDF]: {
- extensions: [FileExtensionPdf.PDF],
- mimeTypes: [MimeTypeApplication.PDF]
- }
-} as const;
-
-export const TEXT_FILE_TYPES = {
- [FileTypeText.ASCIIDOC]: {
- extensions: [FileExtensionText.ADOC],
- mimeTypes: [MimeTypeText.ASCIIDOC]
- },
- [FileTypeText.BIBTEX]: {
- extensions: [FileExtensionText.BIB],
- mimeTypes: [MimeTypeText.BIBTEX]
- },
- [FileTypeText.CPP]: {
- extensions: [
- FileExtensionText.CPP,
- FileExtensionText.C,
- FileExtensionText.H,
- FileExtensionText.HPP
- ],
- mimeTypes: [MimeTypeText.CPP_SRC, MimeTypeText.CPP_HDR, MimeTypeText.C_SRC, MimeTypeText.C_HDR]
- },
- [FileTypeText.CSHARP]: {
- extensions: [FileExtensionText.CS],
- mimeTypes: [MimeTypeText.CSHARP]
- },
- [FileTypeText.CSS]: {
- extensions: [FileExtensionText.CSS],
- mimeTypes: [MimeTypeText.CSS]
- },
- [FileTypeText.CSV]: {
- extensions: [FileExtensionText.CSV],
- mimeTypes: [MimeTypeText.CSV]
- },
- [FileTypeText.CUDA]: {
- extensions: [FileExtensionText.CU, FileExtensionText.CUH],
- mimeTypes: [MimeTypeText.CUDA]
- },
- [FileTypeText.DART]: {
- extensions: [FileExtensionText.DART],
- mimeTypes: [MimeTypeText.DART]
- },
- [FileTypeText.GO]: {
- extensions: [FileExtensionText.GO],
- mimeTypes: [MimeTypeText.GO]
- },
- [FileTypeText.HASKELL]: {
- extensions: [FileExtensionText.HS],
- mimeTypes: [MimeTypeText.HASKELL]
- },
- [FileTypeText.HTML]: {
- extensions: [FileExtensionText.HTML, FileExtensionText.HTM],
- mimeTypes: [MimeTypeText.HTML]
- },
- [FileTypeText.JAVA]: {
- extensions: [FileExtensionText.JAVA],
- mimeTypes: [MimeTypeText.JAVA]
- },
- [FileTypeText.JAVASCRIPT]: {
- extensions: [FileExtensionText.JS],
- mimeTypes: [MimeTypeText.JAVASCRIPT, MimeTypeText.JAVASCRIPT_APP]
- },
- [FileTypeText.JSON]: {
- extensions: [FileExtensionText.JSON],
- mimeTypes: [MimeTypeText.JSON]
- },
- [FileTypeText.JSX]: {
- extensions: [FileExtensionText.JSX],
- mimeTypes: [MimeTypeText.JSX]
- },
- [FileTypeText.KOTLIN]: {
- extensions: [FileExtensionText.KT],
- mimeTypes: [MimeTypeText.KOTLIN]
- },
- [FileTypeText.LATEX]: {
- extensions: [FileExtensionText.TEX],
- mimeTypes: [MimeTypeText.LATEX, MimeTypeText.TEX, MimeTypeText.TEX_APP]
- },
- [FileTypeText.LOG]: {
- extensions: [FileExtensionText.LOG],
- mimeTypes: [MimeTypeText.PLAIN]
- },
- [FileTypeText.MARKDOWN]: {
- extensions: [FileExtensionText.MD],
- mimeTypes: [MimeTypeText.MARKDOWN]
- },
- [FileTypeText.PHP]: {
- extensions: [FileExtensionText.PHP],
- mimeTypes: [MimeTypeText.PHP]
- },
- [FileTypeText.PLAIN_TEXT]: {
- extensions: [FileExtensionText.TXT],
- mimeTypes: [MimeTypeText.PLAIN]
- },
- [FileTypeText.PROPERTIES]: {
- extensions: [FileExtensionText.PROPERTIES],
- mimeTypes: [MimeTypeText.PROPERTIES]
- },
- [FileTypeText.PYTHON]: {
- extensions: [FileExtensionText.PY],
- mimeTypes: [MimeTypeText.PYTHON]
- },
- [FileTypeText.R]: {
- extensions: [FileExtensionText.R],
- mimeTypes: [MimeTypeText.R]
- },
- [FileTypeText.RUBY]: {
- extensions: [FileExtensionText.RB],
- mimeTypes: [MimeTypeText.RUBY]
- },
- [FileTypeText.RUST]: {
- extensions: [FileExtensionText.RS],
- mimeTypes: [MimeTypeText.RUST]
- },
- [FileTypeText.SCALA]: {
- extensions: [FileExtensionText.SCALA],
- mimeTypes: [MimeTypeText.SCALA]
- },
- [FileTypeText.SHELL]: {
- extensions: [FileExtensionText.SH, FileExtensionText.BAT],
- mimeTypes: [MimeTypeText.SHELL, MimeTypeText.BAT]
- },
- [FileTypeText.SQL]: {
- extensions: [FileExtensionText.SQL],
- mimeTypes: [MimeTypeText.SQL]
- },
- [FileTypeText.SVELTE]: {
- extensions: [FileExtensionText.SVELTE],
- mimeTypes: [MimeTypeText.SVELTE]
- },
- [FileTypeText.SWIFT]: {
- extensions: [FileExtensionText.SWIFT],
- mimeTypes: [MimeTypeText.SWIFT]
- },
- [FileTypeText.TSX]: {
- extensions: [FileExtensionText.TSX],
- mimeTypes: [MimeTypeText.TSX]
- },
- [FileTypeText.TYPESCRIPT]: {
- extensions: [FileExtensionText.TS],
- mimeTypes: [MimeTypeText.TYPESCRIPT]
- },
- [FileTypeText.VUE]: {
- extensions: [FileExtensionText.VUE],
- mimeTypes: [MimeTypeText.VUE]
- },
- [FileTypeText.VULKAN]: {
- extensions: [FileExtensionText.COMP],
- mimeTypes: [MimeTypeText.PLAIN]
- },
- [FileTypeText.XML]: {
- extensions: [FileExtensionText.XML],
- mimeTypes: [MimeTypeText.XML_TEXT, MimeTypeText.XML_APP]
- },
- [FileTypeText.YAML]: {
- extensions: [FileExtensionText.YAML, FileExtensionText.YML],
- mimeTypes: [MimeTypeText.YAML_TEXT, MimeTypeText.YAML_APP]
- }
-} as const;
--- /dev/null
+/**
+ * Constants for rendering svg code blocks inline.
+ */
+export const SVG = {
+ // CSS classes applied to the inline svg block and its chrome.
+ BLOCK_CLASS: 'svg-block',
+ /**
+ * Shadow root style for the zoom dialog svg. Lets the svg grow past its
+ * intrinsic size so pan and zoom have room to work.
+ */
+ DIALOG_SHADOW_STYLE:
+ ':host{display:inline-block}svg{min-height:min(50vh,12rem);min-width:min(80vw,20rem);max-width:none;max-height:none;height:auto;width:auto;display:block}',
+ ID_ATTR: 'data-svg-id',
+
+ /**
+ * Shadow root style for an inline svg block. Mirrors the centered, padded
+ * sizing the light dom used before the svg moved behind a shadow boundary.
+ */
+ INLINE_SHADOW_STYLE:
+ ':host{display:block;width:100%;text-align:center}svg{display:block;margin:0 auto;width:auto;height:auto;max-width:100%;max-height:70vh;min-height:8rem;padding:3rem 1rem}',
+ // Languages that mark a code block as svg content.
+ LANGUAGE: 'svg',
+ /**
+ * Hard size ceiling for a single inline svg block.
+ * Above this the source is left as raw text instead of being rendered.
+ */
+ MAX_BYTES: 256 * 1024,
+
+ RENDERED_ATTR: 'data-svg-rendered',
+ /**
+ * DOMPurify config for untrusted svg coming from model output.
+ *
+ * foreignObject and script stay forbidden unconditionally, they are the only
+ * inline svg vectors that execute arbitrary html or js. Everything else is
+ * allowed for maximum rendering compatibility: href and xlink:href stay so
+ * use, image, a and animateMotion work, and DOMPurify still neutralizes
+ * javascript: and data: uri schemes natively. External resource refs are
+ * allowed by design on a local first tool, the user browser fetches them.
+ *
+ * The sanitized svg is always mounted inside a shadow root (see svg-shadow),
+ * so an author <style> stays scoped to that root and can not reach the page.
+ */
+ SANITIZE_CONFIG: {
+ FORBID_TAGS: ['foreignObject', 'script'],
+ USE_PROFILES: { svg: true, svgFilters: true }
+ },
+ SCROLL_CONTAINER_CLASS: 'svg-scroll-container',
+
+ // data-attributes used to stash per-block svg state on the DOM node.
+ SOURCE_ATTR: 'data-svg-source',
+
+ TAG_PREFIX: '<svg',
+
+ WRAPPER_CLASS: 'svg-block-wrapper',
+
+ XML_LANGUAGE: 'xml'
+};
+++ /dev/null
-export const SVG_WRAPPER_CLASS = 'svg-block-wrapper';
-export const SVG_SCROLL_CONTAINER_CLASS = 'svg-scroll-container';
-export const SVG_BLOCK_CLASS = 'svg-block';
-
-export const SVG_LANGUAGE = 'svg';
-export const XML_LANGUAGE = 'xml';
-export const SVG_TAG_PREFIX = '<svg';
-
-export const SVG_SOURCE_ATTR = 'data-svg-source';
-export const SVG_ID_ATTR = 'data-svg-id';
-export const SVG_RENDERED_ATTR = 'data-svg-rendered';
-
-/**
- * Hard size ceiling for a single inline svg block.
- * Above this the source is left as raw text instead of being rendered.
- */
-export const SVG_MAX_BYTES = 256 * 1024;
-
-/**
- * DOMPurify config for untrusted svg coming from model output.
- *
- * foreignObject and script stay forbidden unconditionally, they are the only
- * inline svg vectors that execute arbitrary html or js. Everything else is
- * allowed for maximum rendering compatibility: href and xlink:href stay so
- * use, image, a and animateMotion work, and DOMPurify still neutralizes
- * javascript: and data: uri schemes natively. External resource refs are
- * allowed by design on a local first tool, the user browser fetches them.
- *
- * The sanitized svg is always mounted inside a shadow root (see svg-shadow),
- * so an author <style> stays scoped to that root and can not reach the page.
- */
-export const SVG_SANITIZE_CONFIG = {
- FORBID_TAGS: ['foreignObject', 'script'],
- USE_PROFILES: { svg: true, svgFilters: true }
-};
-
-/**
- * Shadow root style for an inline svg block. Mirrors the centered, padded
- * sizing the light dom used before the svg moved behind a shadow boundary.
- */
-export const SVG_INLINE_SHADOW_STYLE =
- ':host{display:block;width:100%;text-align:center}svg{display:block;margin:0 auto;width:auto;height:auto;max-width:100%;max-height:70vh;min-height:8rem;padding:3rem 1rem}';
-
-/**
- * Shadow root style for the zoom dialog svg. Lets the svg grow past its
- * intrinsic size so pan and zoom have room to work.
- */
-export const SVG_DIALOG_SHADOW_STYLE =
- ':host{display:inline-block}svg{min-height:min(50vh,12rem);min-width:min(80vw,20rem);max-width:none;max-height:none;height:auto;width:auto;display:block}';
--- /dev/null
+/**
+ * Matches <br>, <br/>, <br /> tags (case-insensitive).
+ * Used to detect line breaks in table cell text content.
+ */
+export const BR_PATTERN = /<br\s*\/?\s*>/gi;
+
+/**
+ * Matches a complete <ul>...</ul> block.
+ * Captures the inner content (group 1) for further <li> extraction.
+ * Case-insensitive, allows multiline content.
+ */
+export const LIST_PATTERN = /^<ul>([\s\S]*)<\/ul>$/i;
+
+/**
+ * Matches individual <li>...</li> elements within a list.
+ * Captures the inner content (group 1) of each list item.
+ * Non-greedy to handle multiple consecutive items.
+ * Case-insensitive, allows multiline content.
+ */
+export const LI_PATTERN = /<li>([\s\S]*?)<\/li>/gi;
+++ /dev/null
-/**
- * Matches <br>, <br/>, <br /> tags (case-insensitive).
- * Used to detect line breaks in table cell text content.
- */
-export const BR_PATTERN = /<br\s*\/?\s*>/gi;
-
-/**
- * Matches a complete <ul>...</ul> block.
- * Captures the inner content (group 1) for further <li> extraction.
- * Case-insensitive, allows multiline content.
- */
-export const LIST_PATTERN = /^<ul>([\s\S]*)<\/ul>$/i;
-
-/**
- * Matches individual <li>...</li> elements within a list.
- * Captures the inner content (group 1) of each list item.
- * Non-greedy to handle multiple consecutive items.
- * Case-insensitive, allows multiline content.
- */
-export const LI_PATTERN = /<li>([\s\S]*?)<\/li>/gi;
--- /dev/null
+/* Title generation constants */
+export const TITLE_GENERATION = {
+ DEFAULT_PROMPT:
+ 'Based on the following interaction, generate a short, concise title (maximum 6-8 words) that captures the main topic. Return ONLY the title text, nothing else. Do not use quotes.\n\nUser: {{USER}}\n\nAssistant: {{ASSISTANT}}\n\nTitle:',
+ FALLBACK: 'New Chat',
+ MIN_LENGTH: 3,
+ PREFIX_PATTERN: /^(Title:|Subject:|Topic:)\s*/i,
+ QUOTE_PATTERN: /^["]|["]$/g
+} as const;
+++ /dev/null
-/* Title generation constants */
-export const TITLE_GENERATION = {
- DEFAULT_PROMPT:
- 'Based on the following interaction, generate a short, concise title (maximum 6-8 words) that captures the main topic. Return ONLY the title text, nothing else. Do not use quotes.\n\nUser: {{USER}}\n\nAssistant: {{ASSISTANT}}\n\nTitle:',
- FALLBACK: 'New Chat',
- MIN_LENGTH: 3,
- PREFIX_PATTERN: /^(Title:|Subject:|Topic:)\s*/i,
- QUOTE_PATTERN: /^["]|["]$/g
-} as const;
+++ /dev/null
-import { ToolSource } from '$lib/enums/tools.enums';
-
-/** HTTP header carrying the working directory a tool call runs in. The server resolves relative paths against it; the model cannot override it. */
-export const X_TOOL_CWD_HEADER = 'x-tool-cwd';
-
-/** HTTP header asking the server to encode a tool's output differently, e.g. read_file returning base64. Not a tool parameter, so it stays out of the definition the model sees. */
-export const X_RESP_TYPE_HEADER = 'x-resp-type';
-
-/** `X_RESP_TYPE_HEADER` value that makes read_file return the raw bytes as base64 instead of text. */
-export const RESP_TYPE_BASE64 = 'base64';
-
-export const TOOL_GROUP_LABELS = {
- [ToolSource.BUILTIN]: 'Built-in',
- [ToolSource.CUSTOM]: 'JSON Schema',
- [ToolSource.FRONTEND]: 'Browser'
-} as const;
-
-export const TOOL_SERVER_LABELS = {
- [ToolSource.BUILTIN]: 'Built-in Tools',
- [ToolSource.CUSTOM]: 'Custom Tools',
- [ToolSource.FRONTEND]: 'Browser Tools'
-} as const;
+++ /dev/null
-export const TOOLTIP_DELAY_DURATION = 500;
--- /dev/null
+import { ROUTES } from './routes.constants';
+import { Package, Search, Settings, SquarePen } from '@lucide/svelte';
+import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
+import { ToolSource } from '$lib/enums/tools.enums';
+import type { DesktopIconStripItem } from '$lib/types';
+
+export const FORK_TREE_DEPTH_PADDING = 8;
+export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
+
+export const TOOL_GROUP_LABELS = {
+ [ToolSource.BUILTIN]: 'Built-in',
+ [ToolSource.CUSTOM]: 'JSON Schema',
+ [ToolSource.FRONTEND]: 'Browser'
+} as const;
+
+export const TOOL_SERVER_LABELS = {
+ [ToolSource.BUILTIN]: 'Built-in Tools',
+ [ToolSource.CUSTOM]: 'Custom Tools',
+ [ToolSource.FRONTEND]: 'Browser Tools'
+} as const;
+
+export const TOOLTIP_DELAY_DURATION = 500;
+
+export const VIEWPORT_GUTTER = 8;
+export const MENU_OFFSET = 6;
+
+export const PROCESSING_INFO_TIMEOUT = 2000;
+
+/**
+ * Statistics units labels
+ */
+export const STATS_UNITS = {
+ TOKENS_PER_SECOND: 't/s'
+} as const;
+
+export const DEFAULT_MOBILE_BREAKPOINT = 768;
+
+/** Icon used for the model selector and the `/model` slash command. */
+export const MODEL_SELECTOR_ICON = Package;
+
+export const ICON_STRIP_TRANSITION_DURATION = 150;
+export const ICON_STRIP_TRANSITION_DELAY_MULTIPLIER = 50;
+
+/** Max height for tool-result code blocks (json / source / diff / streaming code). */
+export const MAX_HEIGHT_CODE_BLOCK = '22rem';
+
+export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
+ { icon: SquarePen, keys: ['shift', 'cmd', 'o'], route: ROUTES.NEW_CHAT, tooltip: 'New chat' },
+ { icon: Search, keys: ['cmd', 'k'], tooltip: 'Search' },
+ {
+ activeRouteId: '/mcp-servers',
+ icon: McpLogo,
+ route: ROUTES.MCP_SERVERS,
+ tooltip: 'MCP Servers'
+ },
+ {
+ activeUrlIncludes: '#/settings',
+ icon: Settings,
+ route: `${ROUTES.SETTINGS}/general`,
+ tooltip: 'Settings'
+ }
+];
+++ /dev/null
-import { ROUTES } from './routes';
-import { Package, Search, Settings, SquarePen } from '@lucide/svelte';
-import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
-import type { Component } from 'svelte';
-
-export const FORK_TREE_DEPTH_PADDING = 8;
-export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
-
-/** Icon used for the model selector and the `/model` slash command. */
-export const MODEL_SELECTOR_ICON = Package;
-
-export const ICON_STRIP_TRANSITION_DURATION = 150;
-export const ICON_STRIP_TRANSITION_DELAY_MULTIPLIER = 50;
-
-/** Max height for tool-result code blocks (json / source / diff / streaming code). */
-export const MAX_HEIGHT_CODE_BLOCK = '22rem';
-
-export interface DesktopIconStripItem {
- icon: Component;
- tooltip: string;
- route?: string;
- activeRouteId?: string;
- activeRoutePrefix?: string;
- activeUrlIncludes?: string;
- keys?: string[];
-}
-
-export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
- { icon: SquarePen, keys: ['shift', 'cmd', 'o'], route: ROUTES.NEW_CHAT, tooltip: 'New chat' },
- { icon: Search, keys: ['cmd', 'k'], tooltip: 'Search' },
- {
- activeRouteId: '/mcp-servers',
- icon: McpLogo,
- route: ROUTES.MCP_SERVERS,
- tooltip: 'MCP Servers'
- },
- {
- activeUrlIncludes: '#/settings',
- icon: Settings,
- route: `${ROUTES.SETTINGS}/general`,
- tooltip: 'Settings'
- }
-];
--- /dev/null
+/**
+ * URI Template constants for RFC 6570 template processing.
+ */
+
+/** URI scheme separator */
+export const URI_SCHEME_SEPARATOR = '://';
+
+/** Regex to match template expressions like {var}, {+var}, {#var}, {/var} */
+export const TEMPLATE_EXPRESSION_REGEX = /\{([+#./;?&]?)([^}]+)\}/g;
+
+/** RFC 6570 URI template operators and separators. A single object covers both: an operator prefix character doubles as the separator written into the expansion (e.g. `{/a}`/`{;a}` use `/` and `;` for both), so the characters live here once. */
+export const URI_TEMPLATE_SYMBOLS = {
+ /** Comma separator for list expansion */
+ COMMA: ',',
+ /** Form-style query */
+ FORM_CONTINUATION: '&',
+ /** Form-style query prefix */
+ FORM_QUERY: '?',
+ /** Fragment expansion */
+ FRAGMENT: '#',
+ /** Label expansion */
+ LABEL: '.',
+ /** Path-style parameters */
+ PATH_PARAM: ';',
+ /** Path segment expansion */
+ PATH_SEGMENT: '/',
+ /** Reserved expansion */
+ RESERVED: '+',
+ /** Simple string expansion (default) */
+ SIMPLE: ''
+} as const;
+
+/** Maximum number of leading slashes to strip during URI normalization */
+export const MAX_LEADING_SLASHES_TO_STRIP = 3;
+
+/** Regex to strip explode modifier (*) from variable names */
+export const VARIABLE_EXPLODE_MODIFIER_REGEX = /[*]$/;
+
+/** Regex to strip prefix modifier (:N) from variable names */
+export const VARIABLE_PREFIX_MODIFIER_REGEX = /:[\d]+$/;
+
+/** Regex to strip one or more leading slashes */
+export const LEADING_SLASHES_REGEX = /^\/+/;
+
+/** Regex to match base64-encoded image URIs (format: "data:image/[media type];base64,[data]")*/
+export const BASE64_IMAGE_URI_REGEX = /^data:(image\/[a-z0-9.\-+]+);base64/;
+++ /dev/null
-/**
- * URI Template constants for RFC 6570 template processing.
- */
-
-/** URI scheme separator */
-export const URI_SCHEME_SEPARATOR = '://';
-
-/** Regex to match template expressions like {var}, {+var}, {#var}, {/var} */
-export const TEMPLATE_EXPRESSION_REGEX = /\{([+#./;?&]?)([^}]+)\}/g;
-
-/** RFC 6570 URI template operators */
-export const URI_TEMPLATE_OPERATORS = {
- /** Form-style query continuation */
- FORM_CONTINUATION: '&',
- /** Form-style query */
- FORM_QUERY: '?',
- /** Fragment expansion */
- FRAGMENT: '#',
- /** Label expansion */
- LABEL: '.',
- /** Path-style parameters */
- PATH_PARAM: ';',
- /** Path segment expansion */
- PATH_SEGMENT: '/',
- /** Reserved expansion */
- RESERVED: '+',
- /** Simple string expansion (default) */
- SIMPLE: ''
-} as const;
-
-/** URI template separators used in expansion */
-export const URI_TEMPLATE_SEPARATORS = {
- /** Comma separator for list expansion */
- COMMA: ',',
- /** Period separator for label expansion */
- PERIOD: '.',
- /** Ampersand prefix for query continuation */
- QUERY_CONTINUATION: '&',
- /** Question mark prefix for query string */
- QUERY_PREFIX: '?',
- /** Semicolon separator for path parameters */
- SEMICOLON: ';',
- /** Slash separator for path segments */
- SLASH: '/'
-} as const;
-
-/** Maximum number of leading slashes to strip during URI normalization */
-export const MAX_LEADING_SLASHES_TO_STRIP = 3;
-
-/** Regex to strip explode modifier (*) from variable names */
-export const VARIABLE_EXPLODE_MODIFIER_REGEX = /[*]$/;
-
-/** Regex to strip prefix modifier (:N) from variable names */
-export const VARIABLE_PREFIX_MODIFIER_REGEX = /:[\d]+$/;
-
-/** Regex to strip one or more leading slashes */
-export const LEADING_SLASHES_REGEX = /^\/+/;
-
-/** Regex to match base64-encoded image URIs (format: "data:image/[media type];base64,[data]")*/
-export const BASE64_IMAGE_URI_REGEX = /^data:(image\/[a-z0-9.\-+]+);base64/;
--- /dev/null
+const STD = ['com', 'net', 'org', 'gov', 'edu'] as const;
+const STD_MIL = [...STD, 'mil'] as const;
+const ccTLD_PREFIXES: Record<string, readonly string[]> = {
+ ae: ['co', 'net', 'org', 'gov', 'ac', 'sch'],
+ // --- Standard 5 only ---
+ ar: STD,
+ au: [...STD_MIL, 'id', 'asn', 'csiro'],
+ bd: STD,
+ bg: STD,
+ br: [
+ ...STD_MIL,
+ 'art',
+ 'eco',
+ 'eng',
+ 'inf',
+ 'med',
+ 'psi',
+ 'tmp',
+ 'etc',
+ 'adm',
+ 'adv',
+ 'arq',
+ 'bio',
+ 'bmd',
+ 'cim',
+ 'cng',
+ 'cnt',
+ 'coop',
+ 'ecn',
+ 'esp',
+ 'far',
+ 'fm',
+ 'fnd',
+ 'fot',
+ 'fst',
+ 'g12',
+ 'ggf',
+ 'imb',
+ 'ind',
+ 'jor',
+ 'jus',
+ 'leg',
+ 'lel',
+ 'mat',
+ 'mp',
+ 'mus',
+ 'not',
+ 'ntr',
+ 'odo',
+ 'ppg',
+ 'pro',
+ 'psc',
+ 'qsl',
+ 'rec',
+ 'slg',
+ 'srv',
+ 'trd',
+ 'tur',
+ 'tv',
+ 'vet',
+ 'vlog',
+ 'wiki',
+ 'zlg'
+ ],
+ cn: STD_MIL,
+ eg: STD,
+ gr: STD,
+ hk: STD,
+ hr: STD,
+ hu: ['co', 'net', 'org', 'gov', 'edu'],
+ id: [...STD_MIL, 'co', 'go', 'or', 'web', 'sch'],
+ il: ['co', 'net', 'org', 'gov', 'ac', 'muni'],
+ in: [...STD_MIL, 'co', 'gen', 'ind', 'firm', 'ernet', 'nic'],
+ jp: ['ac', 'ad', 'co', 'ed', 'go', 'gr', 'lg', 'ne', 'or'],
+ ke: ['co', 'or', 'ne', 'go', 'ac', 'sc'],
+ kr: [...STD_MIL, 'co', 'go', 'or', 'ac', 're'],
+ lk: STD,
+ mx: STD_MIL,
+ my: STD_MIL,
+ ng: STD,
+ nz: [
+ ...STD_MIL,
+ 'co',
+ 'gen',
+ 'geek',
+ 'kiwi',
+ 'maori',
+ 'school',
+ 'govt',
+ 'health',
+ 'iwi',
+ 'parliament'
+ ],
+
+ ph: STD,
+ pk: STD,
+ pl: STD,
+ ro: STD,
+ rs: ['co', 'net', 'org', 'gov', 'edu'],
+ ru: STD,
+ sa: STD,
+ sg: [...STD, 'per'],
+
+ si: STD,
+ th: ['co', 'go', 'or', 'in', 'ac', 'mi', 'net'],
+ tr: STD,
+ tw: STD,
+ ua: STD,
+ uk: ['co', 'org', 'net', 'ac', 'gov', 'mil', 'nhs', 'police', 'mod', 'ltd', 'plc', 'me', 'sch'],
+ ve: STD,
+ za: ['co', 'org', 'net', 'web', 'law', 'mil']
+};
+const WILDCARD_BASES: Record<string, readonly string[]> = {
+ br: ['nom', 'blog'],
+ jp: [
+ 'kobe',
+ 'kyoto',
+ 'nagoya',
+ 'osaka',
+ 'sapporo',
+ 'sendai',
+ 'tokyo',
+ 'yokohama',
+ 'aichi',
+ 'akita',
+ 'aomori',
+ 'chiba',
+ 'ehime',
+ 'fukui',
+ 'fukuoka',
+ 'fukushima',
+ 'gifu',
+ 'gunma',
+ 'hiroshima',
+ 'hokkaido',
+ 'hyogo',
+ 'ibaraki',
+ 'ishikawa',
+ 'iwate',
+ 'kagawa',
+ 'kagoshima',
+ 'kanagawa',
+ 'kochi',
+ 'kumamoto',
+ 'mie',
+ 'miyagi',
+ 'miyazaki',
+ 'nagano',
+ 'nara',
+ 'niigata',
+ 'oita',
+ 'okayama',
+ 'okinawa',
+ 'saga',
+ 'saitama',
+ 'shiga',
+ 'shimane',
+ 'shizuoka',
+ 'tochigi',
+ 'tokushima',
+ 'tottori',
+ 'toyama',
+ 'wakayama',
+ 'yamagata',
+ 'yamaguchi',
+ 'yamanashi'
+ ]
+};
+
+function buildSuffixSet(suffixes: Record<string, readonly string[]>): Set<string> {
+ const set = new Set<string>();
+
+ for (const [tld, parts] of Object.entries(suffixes)) {
+ for (const part of parts) {
+ set.add(`${part}.${tld}`);
+ }
+ }
+
+ return set;
+}
+
+export const TWO_PART_PUBLIC_SUFFIXES = buildSuffixSet(ccTLD_PREFIXES);
+export const WILDCARD_PUBLIC_SUFFIXES = buildSuffixSet(WILDCARD_BASES);
+
+// Matches one or more trailing "/" characters at the end of a URL/path.
+export const TRAILING_SLASHES_REGEX = /\/+$/;
+++ /dev/null
-const STD = ['com', 'net', 'org', 'gov', 'edu'] as const;
-const STD_MIL = [...STD, 'mil'] as const;
-const ccTLD_PREFIXES: Record<string, readonly string[]> = {
- ae: ['co', 'net', 'org', 'gov', 'ac', 'sch'],
- // --- Standard 5 only ---
- ar: STD,
- au: [...STD_MIL, 'id', 'asn', 'csiro'],
- bd: STD,
- bg: STD,
- br: [
- ...STD_MIL,
- 'art',
- 'eco',
- 'eng',
- 'inf',
- 'med',
- 'psi',
- 'tmp',
- 'etc',
- 'adm',
- 'adv',
- 'arq',
- 'bio',
- 'bmd',
- 'cim',
- 'cng',
- 'cnt',
- 'coop',
- 'ecn',
- 'esp',
- 'far',
- 'fm',
- 'fnd',
- 'fot',
- 'fst',
- 'g12',
- 'ggf',
- 'imb',
- 'ind',
- 'jor',
- 'jus',
- 'leg',
- 'lel',
- 'mat',
- 'mp',
- 'mus',
- 'not',
- 'ntr',
- 'odo',
- 'ppg',
- 'pro',
- 'psc',
- 'qsl',
- 'rec',
- 'slg',
- 'srv',
- 'trd',
- 'tur',
- 'tv',
- 'vet',
- 'vlog',
- 'wiki',
- 'zlg'
- ],
- cn: STD_MIL,
- eg: STD,
- gr: STD,
- hk: STD,
- hr: STD,
- hu: ['co', 'net', 'org', 'gov', 'edu'],
- id: [...STD_MIL, 'co', 'go', 'or', 'web', 'sch'],
- il: ['co', 'net', 'org', 'gov', 'ac', 'muni'],
- in: [...STD_MIL, 'co', 'gen', 'ind', 'firm', 'ernet', 'nic'],
- jp: ['ac', 'ad', 'co', 'ed', 'go', 'gr', 'lg', 'ne', 'or'],
- ke: ['co', 'or', 'ne', 'go', 'ac', 'sc'],
- kr: [...STD_MIL, 'co', 'go', 'or', 'ac', 're'],
- lk: STD,
- mx: STD_MIL,
- my: STD_MIL,
- ng: STD,
- nz: [
- ...STD_MIL,
- 'co',
- 'gen',
- 'geek',
- 'kiwi',
- 'maori',
- 'school',
- 'govt',
- 'health',
- 'iwi',
- 'parliament'
- ],
-
- ph: STD,
- pk: STD,
- pl: STD,
- ro: STD,
- rs: ['co', 'net', 'org', 'gov', 'edu'],
- ru: STD,
- sa: STD,
- sg: [...STD, 'per'],
-
- si: STD,
- th: ['co', 'go', 'or', 'in', 'ac', 'mi', 'net'],
- tr: STD,
- tw: STD,
- ua: STD,
- uk: ['co', 'org', 'net', 'ac', 'gov', 'mil', 'nhs', 'police', 'mod', 'ltd', 'plc', 'me', 'sch'],
- ve: STD,
- za: ['co', 'org', 'net', 'web', 'law', 'mil']
-};
-const WILDCARD_BASES: Record<string, readonly string[]> = {
- br: ['nom', 'blog'],
- jp: [
- 'kobe',
- 'kyoto',
- 'nagoya',
- 'osaka',
- 'sapporo',
- 'sendai',
- 'tokyo',
- 'yokohama',
- 'aichi',
- 'akita',
- 'aomori',
- 'chiba',
- 'ehime',
- 'fukui',
- 'fukuoka',
- 'fukushima',
- 'gifu',
- 'gunma',
- 'hiroshima',
- 'hokkaido',
- 'hyogo',
- 'ibaraki',
- 'ishikawa',
- 'iwate',
- 'kagawa',
- 'kagoshima',
- 'kanagawa',
- 'kochi',
- 'kumamoto',
- 'mie',
- 'miyagi',
- 'miyazaki',
- 'nagano',
- 'nara',
- 'niigata',
- 'oita',
- 'okayama',
- 'okinawa',
- 'saga',
- 'saitama',
- 'shiga',
- 'shimane',
- 'shizuoka',
- 'tochigi',
- 'tokushima',
- 'tottori',
- 'toyama',
- 'wakayama',
- 'yamagata',
- 'yamaguchi',
- 'yamanashi'
- ]
-};
-
-function buildSuffixSet(suffixes: Record<string, readonly string[]>): Set<string> {
- const set = new Set<string>();
-
- for (const [tld, parts] of Object.entries(suffixes)) {
- for (const part of parts) {
- set.add(`${part}.${tld}`);
- }
- }
-
- return set;
-}
-
-export const TWO_PART_PUBLIC_SUFFIXES = buildSuffixSet(ccTLD_PREFIXES);
-export const WILDCARD_PUBLIC_SUFFIXES = buildSuffixSet(WILDCARD_BASES);
-
-// Matches one or more trailing "/" characters at the end of a URL/path.
-export const TRAILING_SLASHES_REGEX = /\/+$/;
+++ /dev/null
-export const DEFAULT_MOBILE_BREAKPOINT = 768;
--- /dev/null
+/**
+ * Constants for the working-directory picker's glob search.
+ *
+ * The picker glob-matches home-relative names client-side. Character classes
+ * are built case-insensitively and the reserved glob metacharacters are
+ * escaped (passed through literally) so a query never changes matching.
+ */
+
+/** Label shown for the working-directory picker / `/cwd` slash command. */
+export const SET_WORKING_DIRECTORY_LABEL = 'Set working directory';
+
+export const GLOB = {
+ /** `C:`, the drive part of a Windows absolute path. */
+ DRIVE_PREFIX_REGEX: /^[A-Za-z]:/,
+ /** `C:` or `C:/`, the root of a Windows drive-absolute path. */
+ DRIVE_ROOT_REGEX: /^[A-Za-z]:\/?/,
+ /** Character that ends a glob character-class fragment. */
+ RANGE_CLOSE: ']',
+ /** Character that starts a glob character-class fragment. */
+ RANGE_OPEN: '[',
+ /** Query characters that carry glob meaning and are passed through literally. */
+ SPECIAL_CHARS: '*?[]',
+ /** `//host/share` or `//host/share/`, the root of a UNC path. */
+ UNC_ROOT_REGEX: /^\/\/[^/]+\/[^/]+\/?/,
+ /** Wildcard character in a glob pattern. */
+ WILDCARD: '*',
+ /** Separator Windows accepts alongside `/`, and a legal POSIX filename character. */
+ WINDOWS_SEPARATOR: '\\'
+} as const;
+
+export const SEARCH = {
+ // Search tuning for the picker's file_glob_search calls.
+ DEBOUNCE_MS: 180,
+ LIMIT: 100,
+ // Home-relative globs descend deeper than path navigation, which only
+ // needs the direct children of the parent.
+ MAX_DEPTH: 6,
+ MAX_RESULTS_SHOWN: 20,
+ NATIVE_LIMIT: 20,
+ // Native folder-picker resolution searches a shallow, bounded window.
+ NATIVE_MAX_DEPTH: 4,
+ PATH_NAV_MAX_DEPTH: 1
+} as const;
+
+export const FILE_GLOB_SEARCH_PICKERS = {
+ /** Depth the pickers fall back to when the user setting is invalid. */
+ DEFAULT_SEARCH_DEPTH: 10,
+ /** Upper bound the mention search depth setting accepts. The server itself imposes no depth cap (0 = unlimited); this is a UI sanity bound. */
+ MAX_SEARCH_DEPTH: 32
+} as const;
+++ /dev/null
-/**
- * Constants for the working-directory picker's glob search.
- *
- * The picker glob-matches home-relative names client-side. Character classes
- * are built case-insensitively and the reserved glob metacharacters are
- * escaped (passed through literally) so a query never changes matching.
- */
-
-export const GLOB_WILDCARD = '*';
-
-/** Label shown for the working-directory picker / `/cwd` slash command. */
-export const SET_WORKING_DIRECTORY_LABEL = 'Set working directory';
-
-/** Character that starts and ends a glob character-class fragment. */
-export const GLOB_RANGE_OPEN = '[';
-export const GLOB_RANGE_CLOSE = ']';
-
-/** Query characters that carry glob meaning and are passed through literally. */
-export const GLOB_SPECIAL_CHARS = '*?[]';
-
-/** Separator Windows accepts alongside `/`, and a legal POSIX filename character. */
-export const WINDOWS_SEPARATOR = '\\';
-
-/** `C:`, the drive part of a Windows absolute path. */
-export const DRIVE_PREFIX_REGEX = /^[A-Za-z]:/;
-
-/** `C:` or `C:/`, the root of a Windows drive-absolute path. */
-export const DRIVE_ROOT_REGEX = /^[A-Za-z]:\/?/;
-
-/** `//host/share` or `//host/share/`, the root of a UNC path. */
-export const UNC_ROOT_REGEX = /^\/\/[^/]+\/[^/]+\/?/;
-
-// Search tuning for the picker's file_glob_search calls.
-export const SEARCH_DEBOUNCE_MS = 180;
-export const SEARCH_LIMIT = 100;
-export const MAX_RESULTS_SHOWN = 20;
-// Home-relative globs descend deeper than path navigation, which only
-// needs the direct children of the parent.
-export const SEARCH_MAX_DEPTH = 6;
-export const PATH_NAV_MAX_DEPTH = 1;
-// Native folder-picker resolution searches a shallow, bounded window.
-export const NATIVE_MAX_DEPTH = 4;
-export const NATIVE_LIMIT = 20;
-
-/** Upper bound the mention search depth setting accepts. The server itself imposes no depth cap (0 = unlimited); this is a UI sanity bound. */
-export const FILE_GLOB_SEARCH_PICKERS_MAX_SEARCH_DEPTH = 32;
-
-/** Depth the pickers fall back to when the user setting is invalid. */
-export const FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH = 10;
MCP_RESOURCES_CLICK = 'onMcpResourcesClick'
}
+/**
+ * Human-readable labels used when embedding attachments in outgoing messages.
+ */
+export enum AttachmentLabel {
+ FILE = 'File',
+ PDF_FILE = 'PDF File',
+ MCP_PROMPT = 'MCP Prompt',
+ MCP_RESOURCE = 'MCP Resource'
+}
+
/**
* Visibility conditions for attachment menu items.
*/
export {
+ AttachmentLabel,
AttachmentType,
AttachmentMenuItemId,
AttachmentItemEnabledWhen,
FileExtensionAudio,
FileExtensionPdf,
FileExtensionText,
- MimeTypePrefix,
- MimeTypeIncludes,
- UriPattern,
MimeTypeApplication,
MimeTypeAudio,
MimeTypeVideo,
MimeTypeImage,
+ MimeTypePrefix,
+ MimeTypeIncludes,
+ UriPattern,
MimeTypeText,
SpecialFileType
} from './files.enums';
-import { getChatCommands, PROMPT_TRIGGER_PREFIX } from '$lib/constants';
+import { PROMPT_TRIGGER_PREFIX } from '$lib/constants';
import { ChatFormCommandAction, KeyboardKey } from '$lib/enums';
import type { ChatFormCommand } from '$lib/types';
+import { getChatCommands } from '$lib/utils';
import {
type CommandDismissSnapshot,
findCommandToken,
import { goto } from '$app/navigation';
-import { ROUTES } from '$lib/constants/routes';
+import { ROUTES } from '$lib/constants';
import { KeyboardKey } from '$lib/enums';
interface KeyboardShortcutsCallbacks {
import { browser } from '$app/environment';
-import { SW_CONFIG } from '$lib/constants/pwa';
-import { BUILD_VERSION_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { BUILD_VERSION_LOCALSTORAGE_KEY, SW_CONFIG } from '$lib/constants';
import { versionStore } from '$lib/stores/version.svelte';
import { useRegisterSW } from 'virtual:pwa-register/svelte';
-import { REASONING_EFFORT_LEVELS } from '$lib/constants/reasoning-effort';
-import { REASONING_EFFORT_TOKENS } from '$lib/constants/reasoning-effort-tokens';
+import { REASONING_EFFORT_LEVELS, REASONING_EFFORT_TOKENS } from '$lib/constants';
import { ReasoningEffort } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
-import { ROUTES } from '$lib/constants/routes';
+import { ROUTES } from '$lib/constants';
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
export interface ChatSettings {
API_CHAT,
API_SLOTS,
API_STREAM,
- ATTACHMENT_LABEL_MCP_PROMPT,
- ATTACHMENT_LABEL_MCP_RESOURCE,
- ATTACHMENT_LABEL_PDF_FILE,
CONTROL_ACTION,
+ HEADERS,
LEGACY_AGENTIC_REGEX,
REASONING_EFFORT_TOKENS,
SETTINGS_KEYS,
STREAM_VISIBILITY_KICK_MS
} from '$lib/constants';
import {
+ AttachmentLabel,
AttachmentType,
ContentPartType,
MessageRole,
// server side replay buffer and powers discoverActiveStream on tab reopen. with an explicit
// model the ::model suffix keeps the per model session distinct
if (stream && conversationId) {
- headers['X-Conversation-Id'] = streamIdentity(conversationId, options.model);
+ headers[HEADERS.X_CONVERSATION_ID_HEADER] = streamIdentity(conversationId, options.model);
// persist the pending stream before the fetch: a reload during the model load or
// the prompt processing must still find its way back to the session once it exists
ChatService.saveStreamState(conversationId, 0, options.model ?? null);
for (const textFile of textFiles) {
contentParts.push({
- text: formatAttachmentText('File', textFile.name, textFile.content),
+ text: formatAttachmentText(AttachmentLabel.FILE, textFile.name, textFile.content),
type: ContentPartType.TEXT
});
}
for (const legacyContextFile of legacyContextFiles) {
contentParts.push({
- text: formatAttachmentText('File', legacyContextFile.name, legacyContextFile.content),
+ text: formatAttachmentText(
+ AttachmentLabel.FILE,
+ legacyContextFile.name,
+ legacyContextFile.content
+ ),
type: ContentPartType.TEXT
});
}
}
} else {
contentParts.push({
- text: formatAttachmentText(ATTACHMENT_LABEL_PDF_FILE, pdfFile.name, pdfFile.content),
+ text: formatAttachmentText(AttachmentLabel.PDF_FILE, pdfFile.name, pdfFile.content),
type: ContentPartType.TEXT
});
}
for (const mcpPrompt of mcpPrompts) {
contentParts.push({
text: formatAttachmentText(
- ATTACHMENT_LABEL_MCP_PROMPT,
+ AttachmentLabel.MCP_PROMPT,
mcpPrompt.name,
mcpPrompt.content,
mcpPrompt.serverName
for (const mcpResource of mcpResources) {
contentParts.push({
text: formatAttachmentText(
- ATTACHMENT_LABEL_MCP_RESOURCE,
+ AttachmentLabel.MCP_RESOURCE,
mcpResource.name,
mcpResource.content,
mcpResource.serverName
* - **toolsStore**: Exposes the tool definition when the sandbox is enabled
* - **agenticStore**: Dispatches ToolSource.FRONTEND calls here
*
- * @see buildSandboxToolDefinition in constants/sandbox.ts - tool schema sent to the LLM
+ * @see buildSandboxToolDefinition in utils/sandbox-tool - tool schema sent to the LLM
* @see agenticStore in stores/agentic.svelte.ts - tool dispatch
*/
export { SandboxService } from './sandbox.service';
Tool
} from '@modelcontextprotocol/sdk/types.js';
import {
+ CORS_PROXY,
CORS_PROXY_ENDPOINT,
- CORS_PROXY_HEADER_PREFIX,
DEFAULT_CLIENT_VERSION,
DEFAULT_IMAGE_MIME_TYPE,
DEFAULT_MCP_CONFIG,
- MCP_PARTIAL_REDACT_HEADERS
+ HEADERS
} from '$lib/constants';
import {
MCPConnectionPhase,
const details: DiagnosticRequestDetails = {
body: summarizeRequestBody(body),
credentials: init?.credentials ?? baseInit.credentials,
- headers: sanitizeHeaders(requestHeaders, extraRedactedHeaders, MCP_PARTIAL_REDACT_HEADERS),
+ headers: sanitizeHeaders(requestHeaders, extraRedactedHeaders, HEADERS.PARTIAL_REDACT),
method: getRequestMethod(input, init, baseInit).toUpperCase(),
mode: init?.mode ?? baseInit.mode,
url: getRequestUrl(input)
) {
for (const [key, value] of new Headers(headers).entries()) {
const proxiedKey =
- useProxy && !key.toLowerCase().startsWith(CORS_PROXY_HEADER_PREFIX)
- ? `${CORS_PROXY_HEADER_PREFIX}${key}`
+ useProxy && !key.toLowerCase().startsWith(CORS_PROXY.HEADER_PREFIX)
+ ? `${CORS_PROXY.HEADER_PREFIX}${key}`
: key;
requestHeaders.set(proxiedKey, value);
{
response: {
durationMs,
- headers: sanitizeHeaders(response.headers, undefined, MCP_PARTIAL_REDACT_HEADERS),
+ headers: sanitizeHeaders(response.headers, undefined, HEADERS.PARTIAL_REDACT),
status: response.status,
statusText: response.statusText,
url
headers: sanitizeHeaders(
serverConfig.headers,
Object.keys(serverConfig.headers ?? {}),
- MCP_PARTIAL_REDACT_HEADERS
+ HEADERS.PARTIAL_REDACT
),
serverName,
transportType,
DB_APP_NAME_DEPRECATED,
IDXDB_STORES,
IDXDB_TABLES,
+ LEGACY_AGENTIC_REGEX,
+ LEGACY_REASONING_TAGS,
NEW_TO_DEPRECATED_MAP,
+ SETTINGS_KEYS,
STORAGE_APP_NAME,
STORAGE_APP_NAME_DEPRECATED
} from '$lib/constants';
-import { LEGACY_AGENTIC_REGEX, LEGACY_REASONING_TAGS } from '$lib/constants/agentic';
-import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
import { MessageRole } from '$lib/enums';
import Dexie from 'dexie';
-import {
- API_MODELS,
- MODEL_ACTIVATED_PARAMS_RE,
- MODEL_CUSTOM_QUANTIZATION_PREFIX_RE,
- MODEL_ID_NOT_FOUND,
- MODEL_ID_ORG_SEPARATOR,
- MODEL_ID_QUANTIZATION_SEPARATOR,
- MODEL_ID_SEGMENT_SEPARATOR,
- MODEL_IGNORED_SEGMENTS,
- MODEL_PARAMS_RE,
- MODEL_QUANTIZATION_SEGMENT_RE,
- MODEL_WEIGHT_EXTENSION_RE
-} from '$lib/constants';
+import { API_MODELS, MODEL_ID } from '$lib/constants';
import { ServerModelStatus } from '$lib/enums';
import type { ParsedModelId } from '$lib/types/models';
import { apiFetch, apiPost, normalizeModelName } from '$lib/utils';
};
// strip directory path and weight extension so a bare `-m /path/file.gguf`
// parses like a clean repo id; the HF `org/model` form is preserved
- const source = normalizeModelName(modelId).replace(MODEL_WEIGHT_EXTENSION_RE, '');
+ const source = normalizeModelName(modelId).replace(MODEL_ID.WEIGHT_EXTENSION_RE, '');
// 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`)
- const colonIdx = source.indexOf(MODEL_ID_QUANTIZATION_SEPARATOR);
+ const colonIdx = source.indexOf(MODEL_ID.QUANTIZATION_SEPARATOR);
let modelPath: string;
- if (colonIdx !== MODEL_ID_NOT_FOUND) {
+ if (colonIdx !== MODEL_ID.NOT_FOUND) {
result.quantization = source.slice(colonIdx + 1) || null;
modelPath = source.slice(0, colonIdx);
} else {
}
// 2. Extract org name (e.g. `org/model` -> org = "org")
- const slashIdx = modelPath.indexOf(MODEL_ID_ORG_SEPARATOR);
+ const slashIdx = modelPath.indexOf(MODEL_ID.ORG_SEPARATOR);
let modelStr: string;
- if (slashIdx !== MODEL_ID_NOT_FOUND) {
+ if (slashIdx !== MODEL_ID.NOT_FOUND) {
result.orgName = modelPath.slice(0, slashIdx);
modelStr = modelPath.slice(slashIdx + 1);
} else {
// 3. Handle dot-separated quantization (e.g. `model-name.Q4_K_M`)
const dotIdx = modelStr.lastIndexOf('.');
- if (dotIdx !== MODEL_ID_NOT_FOUND && !result.quantization) {
+ if (dotIdx !== MODEL_ID.NOT_FOUND && !result.quantization) {
const afterDot = modelStr.slice(dotIdx + 1);
- if (MODEL_QUANTIZATION_SEGMENT_RE.test(afterDot)) {
+ if (MODEL_ID.QUANTIZATION_SEGMENT_RE.test(afterDot)) {
result.quantization = afterDot;
modelStr = modelStr.slice(0, dotIdx);
}
}
- const segments = modelStr.split(MODEL_ID_SEGMENT_SEPARATOR);
+ const segments = modelStr.split(MODEL_ID.SEGMENT_SEPARATOR);
// 4. Detect trailing quantization from dash-separated segments
// Handle UD-prefixed quantization (e.g. `UD-Q8_K_XL`) and
const last = segments[segments.length - 1];
const secondLast = segments.length > 2 ? segments[segments.length - 2] : null;
- if (MODEL_QUANTIZATION_SEGMENT_RE.test(last)) {
- if (secondLast && MODEL_CUSTOM_QUANTIZATION_PREFIX_RE.test(secondLast)) {
+ if (MODEL_ID.QUANTIZATION_SEGMENT_RE.test(last)) {
+ if (secondLast && MODEL_ID.CUSTOM_QUANTIZATION_PREFIX_RE.test(secondLast)) {
result.quantization = `${secondLast}-${last}`;
segments.splice(segments.length - 2, 2);
} else {
}
// 5. Find params and activated params
- let paramsIdx = MODEL_ID_NOT_FOUND;
- let activatedParamsIdx = MODEL_ID_NOT_FOUND;
+ let paramsIdx = MODEL_ID.NOT_FOUND;
+ let activatedParamsIdx = MODEL_ID.NOT_FOUND;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
- if (paramsIdx === MODEL_ID_NOT_FOUND && MODEL_PARAMS_RE.test(seg)) {
+ if (paramsIdx === MODEL_ID.NOT_FOUND && MODEL_ID.PARAMS_RE.test(seg)) {
paramsIdx = i;
result.params = seg.toUpperCase();
- } else if (paramsIdx !== MODEL_ID_NOT_FOUND && MODEL_ACTIVATED_PARAMS_RE.test(seg)) {
+ } else if (paramsIdx !== MODEL_ID.NOT_FOUND && MODEL_ID.ACTIVATED_PARAMS_RE.test(seg)) {
activatedParamsIdx = i;
result.activatedParams = seg.toUpperCase();
}
}
// 6. Model name = segments before params; tags = remaining segments after params
- const pivotIdx = paramsIdx !== MODEL_ID_NOT_FOUND ? paramsIdx : segments.length;
+ const pivotIdx = paramsIdx !== MODEL_ID.NOT_FOUND ? paramsIdx : segments.length;
- result.modelName = segments.slice(0, pivotIdx).join(MODEL_ID_SEGMENT_SEPARATOR) || null;
+ result.modelName = segments.slice(0, pivotIdx).join(MODEL_ID.SEGMENT_SEPARATOR) || null;
- if (paramsIdx !== MODEL_ID_NOT_FOUND) {
+ if (paramsIdx !== MODEL_ID.NOT_FOUND) {
result.tags = segments.slice(paramsIdx + 1).filter((_, relIdx) => {
const absIdx = paramsIdx + 1 + relIdx;
if (absIdx === activatedParamsIdx) return false;
- return !MODEL_IGNORED_SEGMENTS.has(segments[absIdx].toUpperCase());
+ return !MODEL_ID.IGNORED_SEGMENTS.has(segments[absIdx].toUpperCase());
});
}
-import { ROUTES } from '$lib/constants/routes';
+import { ROUTES } from '$lib/constants';
export class RouterService {
static chat(id: string): string {
import { base } from '$app/paths';
-import { API_TOOLS, X_RESP_TYPE_HEADER, X_TOOL_CWD_HEADER } from '$lib/constants';
+import { API_TOOLS, HEADERS } from '$lib/constants';
import { ToolResponseField } from '$lib/enums';
import type { ServerBuiltinToolInfo, ToolExecutionResult } from '$lib/types';
import { apiFetch } from '$lib/utils';
): Promise<ToolExecutionResult> {
const result = await apiFetch<Record<string, unknown>>(API_TOOLS.EXECUTE, {
body: JSON.stringify({ params, tool: toolName }),
- headers: cwd ? { [X_TOOL_CWD_HEADER]: cwd } : undefined,
+ headers: cwd ? { [HEADERS.X_TOOL_CWD_HEADER]: cwd } : undefined,
method: 'POST',
signal
});
): Promise<Record<string, unknown>> {
const headers: Record<string, string> = {};
- if (cwd) headers[X_TOOL_CWD_HEADER] = cwd;
+ if (cwd) headers[HEADERS.X_TOOL_CWD_HEADER] = cwd;
- if (respType) headers[X_RESP_TYPE_HEADER] = respType;
+ if (respType) headers[HEADERS.X_RESP_TYPE_HEADER] = respType;
return apiFetch<Record<string, unknown>>(API_TOOLS.EXECUTE, {
body: JSON.stringify({ params, tool: toolName }),
): AsyncGenerator<ToolStreamEvent> {
const headers = getJsonHeaders();
- if (cwd) headers[X_TOOL_CWD_HEADER] = cwd;
+ if (cwd) headers[HEADERS.X_TOOL_CWD_HEADER] = cwd;
const response = await fetch(`${base}${API_TOOLS.EXECUTE}`, {
body: JSON.stringify({ params, stream: true, tool: toolName }),
DEFAULT_AUDIO_EXTENSION,
DEFAULT_IMAGE_EXTENSION,
IMAGE_MIME_TO_EXTENSION,
- MCP_ATTACHMENT_NAME_PREFIX
+ MCP_ATTACHMENT_NAME_PREFIX,
+ MIME_TYPE_PREFIXES
} from '$lib/constants';
import { BuiltInTool, ToolPermissionDecision, ToolSource } from '$lib/enums';
import {
attachmentIndex += 1;
const name = this.buildAttachmentName(mimeType, attachmentIndex);
- if (mimeType.startsWith(MimeTypePrefix.IMAGE)) {
+ if (mimeType.startsWith(MIME_TYPE_PREFIXES.IMAGE)) {
attachments.push({ base64Url: trimmedLine, name, type: AttachmentType.IMAGE });
return `[Attachment saved: ${name}]`;
* @see ChatService in services/chat.service.ts for API operations
*/
-import { CONTENT_TYPE_HEADER } from '$lib/constants';
import {
- INACTIVE_CONVERSATION_STATE_MAX_AGE_MS,
- MAX_INACTIVE_CONVERSATION_STATES,
+ CONVERSATION_ID_SEPARATOR,
+ HEADERS,
+ INACTIVE_CONVERSATION,
+ STREAM_RESUME_RETRY_MS,
SYSTEM_MESSAGE_PLACEHOLDER,
TITLE_GENERATION
} from '$lib/constants';
-import { STREAM_RESUME_RETRY_MS } from '$lib/constants/api-endpoints';
import { MimeTypeApplication } from '$lib/enums';
import {
ContinueIntentKind,
// POST the one conv id we are probing
listResp = await fetch(`./v1/streams/lookup`, {
body: JSON.stringify({ conversation_ids: [convId] }),
- headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON },
+ headers: { ...getAuthHeaders(), [HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON },
method: 'POST'
});
} catch (e) {
// extract the model suffix, the resume calls in handleStreamResponse must reuse the model
// the session was tagged with, not the live dropdown
- const sepIdx = id.indexOf('::');
+ const sepIdx = id.indexOf(CONVERSATION_ID_SEPARATOR);
const attachedModel: string | null = sepIdx === -1 ? null : id.slice(sepIdx + 2);
this.setChatStreaming(convId, existingContent, targetMessageId, attachedModel);
try {
const resp = await fetch('./v1/streams/lookup', {
body: JSON.stringify({ conversation_ids: lookupIds }),
- headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON },
+ headers: { ...getAuthHeaders(), [HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON },
method: 'POST'
});
for (const s of sessions) {
if (s && !s.is_done && typeof s.conversation_id === 'string' && s.conversation_id) {
// strip the optional ::model suffix, the sidebar set is keyed by the bare conv id
- const sepIdx = s.conversation_id.indexOf('::');
+ const sepIdx = s.conversation_id.indexOf(CONVERSATION_ID_SEPARATOR);
const bareId = sepIdx === -1 ? s.conversation_id : s.conversation_id.slice(0, sepIdx);
running.add(bareId);
for (const { convId, lastAccessed } of cleanupCandidates) {
if (
- cleanupCandidates.length - cleanedUp > MAX_INACTIVE_CONVERSATION_STATES ||
- now - lastAccessed > INACTIVE_CONVERSATION_STATE_MAX_AGE_MS
+ cleanupCandidates.length - cleanedUp > INACTIVE_CONVERSATION.MAX_STATES ||
+ now - lastAccessed > INACTIVE_CONVERSATION.MAX_AGE_MS
) {
this.cleanupConversationState(convId);
cleanedUp++;
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import {
- EXPORT_CONV_ID_TRIM_LENGTH,
- EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH,
- EXPORT_CONV_NONALNUM_REPLACEMENT,
- ISO_DATE_TIME_SEPARATOR,
- ISO_DATE_TIME_SEPARATOR_REPLACEMENT,
- ISO_TIME_SEPARATOR,
- ISO_TIME_SEPARATOR_REPLACEMENT,
- ISO_TIMESTAMP_SLICE_LENGTH,
- MULTIPLE_UNDERSCORE_REGEX,
+ EXPORT_CONV,
NEWLINE,
- NON_ALPHANUMERIC_REGEX,
REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY,
- SESSION_HARNESS,
+ ROUTES,
ZIP_MAGIC
} from '$lib/constants';
-import { ROUTES } from '$lib/constants/routes';
import {
FileExtensionText,
MessageRole,
): string {
const conversationName = (conversation.name ?? '').trim().toLowerCase();
const sanitizedName = conversationName
- .replace(NON_ALPHANUMERIC_REGEX, EXPORT_CONV_NONALNUM_REPLACEMENT)
- .replace(MULTIPLE_UNDERSCORE_REGEX, '_')
- .substring(0, EXPORT_CONV_NAME_SUFFIX_MAX_LENGTH);
+ .replace(EXPORT_CONV.NON_ALPHANUMERIC_REGEX, EXPORT_CONV.NONALNUM_REPLACEMENT)
+ .replace(EXPORT_CONV.MULTIPLE_UNDERSCORE_REGEX, '_')
+ .substring(0, EXPORT_CONV.NAME_SUFFIX_MAX_LENGTH);
// If we have messages, use the timestamp of the newest message
const referenceDate = msgs?.length
? new Date(Math.max(...msgs.map((m) => m.timestamp)))
: new Date();
- const iso = referenceDate.toISOString().slice(0, ISO_TIMESTAMP_SLICE_LENGTH);
+ const iso = referenceDate.toISOString().slice(0, EXPORT_CONV.ISO_TIMESTAMP_SLICE);
const formattedDate = iso
- .replace(ISO_DATE_TIME_SEPARATOR, ISO_DATE_TIME_SEPARATOR_REPLACEMENT)
- .replaceAll(ISO_TIME_SEPARATOR, ISO_TIME_SEPARATOR_REPLACEMENT);
- const trimmedConvId = conversation.id?.slice(0, EXPORT_CONV_ID_TRIM_LENGTH) ?? '';
+ .replace(EXPORT_CONV.ISO_DATE_TIME_SEPARATOR, EXPORT_CONV.ISO_DATE_TIME_SEPARATOR_REPLACEMENT)
+ .replaceAll(EXPORT_CONV.ISO_TIME_SEPARATOR, EXPORT_CONV.ISO_TIME_SEPARATOR_REPLACEMENT);
+ const trimmedConvId = conversation.id?.slice(0, EXPORT_CONV.ID_TRIM_LENGTH) ?? '';
return `${formattedDate}_conv_${trimmedConvId}_${sanitizedName}${FileExtensionText.JSONL}`;
}
serializeSessionToJsonl(data: ExportedConversation): string {
const { conv, messages } = data;
const sessionLine = JSON.stringify({
- harness: SESSION_HARNESS,
+ harness: EXPORT_CONV.HARNESS,
type: SessionRecordType.SESSION,
...conv
});
files[entryName] = strToU8(this.serializeSessionToJsonl(session));
}
- const archiveName = `${new Date().toISOString().split(ISO_DATE_TIME_SEPARATOR)[0]}_conversations${FileExtensionText.ZIP}`;
+ const archiveName = `${new Date().toISOString().split(EXPORT_CONV.ISO_DATE_TIME_SEPARATOR)[0]}_conversations${FileExtensionText.ZIP}`;
const zipped = zipSync(files);
const blob = new Blob([zipped], { type: MimeTypeApplication.ZIP });
import {
BINARY_CONTENT_LABEL,
MCP_RESOURCE_ATTACHMENT_ID_PREFIX,
- MCP_RESOURCE_CACHE_MAX_ENTRIES,
- MCP_RESOURCE_CACHE_TTL_MS,
+ MCP_RESOURCE_CACHE,
NEWLINE,
RESOURCE_UNKNOWN_TYPE
} from '$lib/constants';
*/
cacheResourceContent(resource: MCPResourceInfo, content: MCPResourceContent[]): void {
// Enforce cache size limit
- if (this._cachedResources.size >= MCP_RESOURCE_CACHE_MAX_ENTRIES) {
+ if (this._cachedResources.size >= MCP_RESOURCE_CACHE.MAX_ENTRIES) {
// Remove oldest entry
const oldestKey = this._cachedResources.keys().next().value;
// Check if cache is still valid
const age = Date.now() - cached.fetchedAt.getTime();
- if (age > MCP_RESOURCE_CACHE_TTL_MS && !cached.subscribed) {
+ if (age > MCP_RESOURCE_CACHE.TTL_MS && !cached.subscribed) {
// Cache expired and not subscribed, remove it
this._cachedResources.delete(uri);
import { browser } from '$app/environment';
import { SETTINGS_KEYS } from '$lib/constants';
import {
- DEFAULT_CACHE_TTL_MS,
+ CACHE,
DEFAULT_MCP_CONFIG,
EXPECTED_THEMED_ICON_PAIR_COUNT,
MCP_ALLOWED_ICON_MIME_TYPES,
- MCP_RECONNECT_ATTEMPT_TIMEOUT_MS,
- MCP_RECONNECT_BACKOFF_MULTIPLIER,
- MCP_RECONNECT_INITIAL_DELAY,
- MCP_RECONNECT_MAX_DELAY,
+ MCP_RECONNECT,
MCP_SERVER_ID_PREFIX
} from '$lib/constants';
import {
}
this.reconnectingServers.add(serverName);
- let backoff = MCP_RECONNECT_INITIAL_DELAY;
+ let backoff = MCP_RECONNECT.INITIAL_DELAY;
// Flag set by the phase callback when a DISCONNECTED event fires while
// reconnectingServers still holds this server (see JSDoc above).
let needsReconnect = false;
() =>
reject(
new Error(
- `Reconnect attempt timed out after ${MCP_RECONNECT_ATTEMPT_TIMEOUT_MS}ms`
+ `Reconnect attempt timed out after ${MCP_RECONNECT.ATTEMPT_TIMEOUT_MS}ms`
)
),
- MCP_RECONNECT_ATTEMPT_TIMEOUT_MS
+ MCP_RECONNECT.ATTEMPT_TIMEOUT_MS
)
);
break;
} catch (error) {
console.warn(`[MCPStore][${serverName}] Reconnection failed:`, error);
- backoff = Math.min(backoff * MCP_RECONNECT_BACKOFF_MULTIPLIER, MCP_RECONNECT_MAX_DELAY);
+ backoff = Math.min(backoff * MCP_RECONNECT.BACKOFF_MULTIPLIER, MCP_RECONNECT.MAX_DELAY);
}
}
} finally {
// Cache is valid for 5 minutes
const age = Date.now() - serverRes.lastFetched.getTime();
- return age < DEFAULT_CACHE_TTL_MS;
+ return age < CACHE.DEFAULT_TTL_MS;
});
if (allServersCached) {
import {
API_MODELS,
FAVORITE_MODELS_LOCALSTORAGE_KEY,
- MODEL_PROPS_CACHE_MAX_ENTRIES,
- MODEL_PROPS_CACHE_TTL_MS,
+ MODEL_PROPS_CACHE,
SSE_DATA_PREFIX,
SSE_LINE_SEPARATOR,
SSE_RECORD_SEPARATOR
* TTL: 10 minutes — props don't change frequently.
*/
private modelPropsCache = new TTLCache<string, ApiLlamaCppServerProps>({
- maxEntries: MODEL_PROPS_CACHE_MAX_ENTRIES,
- ttlMs: MODEL_PROPS_CACHE_TTL_MS
+ maxEntries: MODEL_PROPS_CACHE.MAX_ENTRIES,
+ ttlMs: MODEL_PROPS_CACHE.TTL_MS
});
private modelPropsFetching = $state<Set<string>>(new Set());
import {
buildReadMediaToolDefinition,
- buildSandboxToolDefinition,
DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY,
HOME_TILDE,
TOOL_GROUP_LABELS,
import { modelsStore, selectedModelName } from '$lib/stores/models.svelte';
import { config } from '$lib/stores/settings.svelte';
import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types';
+import { buildSandboxToolDefinition } from '$lib/utils';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
/** Stable selection identity for a tool, shared by the disabled set and the permission store */
import { browser } from '$app/environment';
-import { DEFAULT_MOBILE_BREAKPOINT } from '$lib/constants/viewport';
+import { DEFAULT_MOBILE_BREAKPOINT } from '$lib/constants';
import { MediaQuery } from 'svelte/reactivity';
export const viewport = $state({
import type { ApiChatCompletionToolCall } from './api';
import type { DatabaseMessage, DatabaseMessageExtra } from './database';
-import type { ChatFormCommandAction, ErrorDialogType, FileMentionEntryType } from '$lib/enums';
+import type {
+ AttachmentAction,
+ AttachmentItemEnabledWhen,
+ AttachmentItemVisibleWhen,
+ AttachmentMenuItemId,
+ ChatFormCommandAction,
+ ErrorDialogType,
+ FileMentionEntryType
+} from '$lib/enums';
+import type { Component } from 'svelte';
+
+/**
+ * A single item in the chat form attachment menu.
+ */
+export interface AttachmentMenuItem {
+ /** Unique identifier for the item */
+ id: AttachmentMenuItemId;
+ /** Display label */
+ label: string;
+ /** Lucide icon component */
+ icon: Component;
+ /** Extra CSS class applied to the item (e.g. for test selectors) */
+ class?: string;
+ /** Whether the item requires a specific modality to be enabled */
+ enabledWhen?: AttachmentItemEnabledWhen;
+ /** Tooltip shown when the item is disabled */
+ disabledTooltip?: string;
+ /** Callback key on the Props interface to invoke when clicked */
+ action: AttachmentAction;
+ /** Whether the item is only shown when a specific capability is present */
+ visibleWhen?: AttachmentItemVisibleWhen;
+ /** Whether this item has a tooltip even when enabled (uses dynamic text) */
+ hasEnabledTooltip?: boolean;
+}
export interface ChatUploadedFile {
id: string;
* command whose backing capability is unavailable (e.g. `/prompt` when no
* MCP server exposes prompts): visible but greyed out and not selectable.
*/
+export interface ChatCommandsOptions {
+ /** Gates `/model`. */
+ showModelSelector: boolean;
+ /** Gates `/prompt`. */
+ hasPrompts: () => boolean;
+ /** Gates `/cwd`. */
+ hasCwdTools: () => boolean;
+}
+
+/** Protocol-level verbs accepted by the realtime inference control endpoint. Mirrors `CONTROL_ACTION`. */
+export type ControlAction = 'reasoning_end';
+
export interface ChatFormCommand {
name: string;
description: string;
// Chat types
export type {
+ AttachmentMenuItem,
ChatUploadedFile,
ChatAttachmentDisplayItem,
ChatMessageSiblingInfo,
AttachmentDisplayItemsOptions,
FileProcessingResult,
FileMentionEntry,
- ChatFormCommand
+ ChatFormCommand,
+ ChatCommandsOptions,
+ ControlAction
} from './chat.d';
// Database types
SteeringMessage
} from './agentic';
+// Navigation types
+export type { DesktopIconStripItem } from './navigation';
+
// Tools types
-export type { ToolEntry, ToolGroup } from './tools';
+export type { ToolEntry, ToolGroup, BuiltinToolUiEntry } from './tools';
// Reasoning
export type { ReasoningEffortLevel } from './reasoning';
--- /dev/null
+import type { Component } from 'svelte';
+
+/**
+ * A single clickable action in the desktop sidebar icon strip.
+ */
+export interface DesktopIconStripItem {
+ icon: Component;
+ tooltip: string;
+ route?: string;
+ activeRouteId?: string;
+ activeRoutePrefix?: string;
+ activeUrlIncludes?: string;
+ keys?: string[];
+}
import type { OpenAIToolDefinition } from './mcp';
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.
+ */
+export interface BuiltinToolUiEntry {
+ icon: Component;
+ label: string;
+ source: ToolSource.BUILTIN | ToolSource.FRONTEND;
+}
export interface ToolEntry {
source: ToolSource;
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 {
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));
*/
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;
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';
/**
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';
const currentConfig = config();
const apiKey = currentConfig.apiKey?.toString().trim();
- return apiKey ? { [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey}` } : {};
+ return apiKey ? { [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey}` } : {};
}
/**
*/
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.
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);
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)
) {
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';
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 });
--- /dev/null
+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;
+}
-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
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;
}
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 {
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';
/**
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);
--- /dev/null
+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'
+ }
+ ];
+}
-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 {
* 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. */
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;
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);
* 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();
*/
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.
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;
}
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;
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';
options: GlobSearchChildOptions = {}
): Promise<GlobSearchChildResult> {
const {
- childMaxDepth = PATH_NAV_MAX_DEPTH,
+ childMaxDepth = SEARCH.PATH_NAV_MAX_DEPTH,
descendOnTrailingSeparator = false,
type = GlobSearchType.ALL
} = options;
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()
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
-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
const { heicTo } = await getHeicTo();
const jpegBlob = await heicTo({
blob: file,
- quality: HEIC_JPEG_QUALITY,
+ quality: IMAGE.HEIC_JPEG_QUALITY,
type: MimeTypeImage.JPEG
});
// 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
-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';
/**
}
// 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);
* @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;
}
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);
}
// 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;
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;
}
// 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;
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';
}
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;
}
* @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;
}
/**
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)
);
}
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);
}
/**
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;
}
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';
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.
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, '');
--- /dev/null
+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);
-import { SVG_MAX_BYTES, SVG_SANITIZE_CONFIG, SVG_TAG_PREFIX } from '$lib/constants';
+import { SVG } from '$lib/constants';
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;
}
+import { CONVERSATION_ID_SEPARATOR } from '$lib/constants';
+
/**
* Build the conversation identity used by the server side replay buffer.
*
if (!model) return conversationId;
- return `${conversationId}::${model}`;
+ return `${conversationId}${CONVERSATION_ID_SEPARATOR}${model}`;
}
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';
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);
}
}
);
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;
* 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;
}
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 {
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 };
}
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { ServerErrorSplash } from '$lib/components/app';
- import { APP_NAME } from '$lib/constants';
- import { ROUTES } from '$lib/constants/routes';
+ import { APP_NAME, ROUTES } from '$lib/constants';
let error = $derived($page.error);
let status = $derived($page.status);
import { SidebarNavigation } from '$lib/components/app';
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
import * as Tooltip from '$lib/components/ui/tooltip';
- import { AUTHORIZATION_HEADER, BEARER_PREFIX, TOOLTIP_DELAY_DURATION } from '$lib/constants';
- import { SETTINGS_KEYS } from '$lib/constants';
- import { FAVICON_PATHS, FAVICON_SELECTORS } from '$lib/constants/pwa';
- import { ROUTES } from '$lib/constants/routes';
+ import {
+ FAVICON_PATHS,
+ FAVICON_SELECTORS,
+ HEADERS,
+ ROUTES,
+ SETTINGS_KEYS,
+ TOOLTIP_DELAY_DURATION
+ } from '$lib/constants';
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
import { usePwa } from '$lib/hooks/use-pwa.svelte';
import { RouterService } from '$lib/services/router.service';
page.status !== 403
) {
const headers: Record<string, string> = {
- [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey.trim()}`,
- 'Content-Type': 'application/json'
+ 'Content-Type': 'application/json',
+ [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey.trim()}`
};
fetch(`${base}/props`, { headers })
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app';
- import { ROUTES } from '$lib/constants/routes';
+ import { ROUTES } from '$lib/constants';
import { RouterService } from '$lib/services/router.service';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversations, conversationsStore } from '$lib/stores/conversations.svelte';
-import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
import { settingsStore } from '$lib/stores/settings.svelte';
import { validateApiKey } from '$lib/utils/api-key-validation';
import { beforeEach, describe, expect, it } from 'vitest';
// consumes the same case locally; see chat-form-contenteditable).
import ChatFormTestWrapper from './components/ChatFormTestWrapper.svelte';
-import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
+import { SETTINGS_KEYS } from '$lib/constants';
import { settingsStore } from '$lib/stores/settings.svelte';
import { tick } from 'svelte';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-registry';
-import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { CONFIG_LOCALSTORAGE_KEY, SETTING_CONFIG_DEFAULT } from '$lib/constants';
import { ParameterSyncService } from '$lib/services/parameter-sync.service';
import { serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
// returns to its own default. The toggle is removed from the persisted
// config so it does not stay orphaned in localStorage.
-import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
import { MigrationService } from '$lib/services/migration.service';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { beforeEach, describe, expect, it } from 'vitest';
-import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
import { serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { beforeEach, describe, expect, it } from 'vitest';
-import { LEGACY_AGENTIC_REGEX } from '$lib/constants/agentic';
+import { LEGACY_AGENTIC_REGEX } from '$lib/constants';
import { describe, expect, it } from 'vitest';
/**
-import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
-import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
+import { CONFIG_LOCALSTORAGE_KEY, SETTINGS_KEYS } from '$lib/constants';
import type { DatabaseConversation } from '$lib/types/database';
import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
-import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
+import { SETTINGS_KEYS } from '$lib/constants';
import { describe, expect, it } from 'vitest';
/**
*/
describe('MCP_SERVERS default value', () => {
it('does not preload any servers in the MCP_SERVERS setting default', async () => {
- const { SETTING_CONFIG_DEFAULT } = await import('$lib/constants/settings-registry');
+ const { SETTING_CONFIG_DEFAULT } = await import('$lib/constants');
expect(SETTING_CONFIG_DEFAULT[SETTINGS_KEYS.MCP_SERVERS]).toBe('[]');
}, 15000);
import { Client } from '@modelcontextprotocol/sdk/client';
-import { CORS_PROXY_HEADER_PREFIX } from '$lib/constants';
+import { CORS_PROXY } from '$lib/constants';
import { MCPConnectionPhase, MCPTransportType } from '$lib/enums';
import { MCPService } from '$lib/services/mcp.service';
import type { MCPConnectionLog, MCPServerConfig } from '$lib/types';
it('wraps dynamic request headers when using the CORS proxy', async () => {
const logs: MCPConnectionLog[] = [];
- const proxiedAuthToken = `${CORS_PROXY_HEADER_PREFIX}x-auth-token`;
- const proxiedContentType = `${CORS_PROXY_HEADER_PREFIX}content-type`;
- const proxiedSessionId = `${CORS_PROXY_HEADER_PREFIX}mcp-session-id`;
+ const proxiedAuthToken = `${CORS_PROXY.HEADER_PREFIX}x-auth-token`;
+ const proxiedContentType = `${CORS_PROXY.HEADER_PREFIX}content-type`;
+ const proxiedSessionId = `${CORS_PROXY.HEADER_PREFIX}mcp-session-id`;
const response = new Response('{}', {
headers: { 'content-type': 'application/json' },
status: 200
-import { MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
+import { MCP_SERVER_ID_PREFIX } from '$lib/constants';
import { parseMcpServerSettings } from '$lib/utils/mcp';
import { describe, expect, it, vi } from 'vitest';
-import { CORS_PROXY_HEADER_PREFIX } from '$lib/constants';
+import { CORS_PROXY } from '$lib/constants';
import { sanitizeHeaders } from '$lib/utils/api-headers';
import { describe, expect, it } from 'vitest';
});
it('redacts proxied sensitive and custom target headers', () => {
- const proxiedAuthorization = `${CORS_PROXY_HEADER_PREFIX}authorization`;
- const proxiedSessionId = `${CORS_PROXY_HEADER_PREFIX}mcp-session-id`;
- const proxiedVendorKey = `${CORS_PROXY_HEADER_PREFIX}x-vendor-key`;
+ const proxiedAuthorization = `${CORS_PROXY.HEADER_PREFIX}authorization`;
+ const proxiedSessionId = `${CORS_PROXY.HEADER_PREFIX}mcp-session-id`;
+ const proxiedVendorKey = `${CORS_PROXY.HEADER_PREFIX}x-vendor-key`;
const headers = new Headers({
[proxiedAuthorization]: 'Bearer secret',
[proxiedSessionId]: 'session-12345',
-import { URI_TEMPLATE_OPERATORS } from '../../src/lib/constants/uri-template';
+import { URI_TEMPLATE_SYMBOLS } from '../../src/lib/constants/uri-template.constants';
import {
expandTemplate,
extractTemplateVariables,
it('extracts variables with operators', () => {
const vars = extractTemplateVariables('http://example.com{+path}');
- expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_OPERATORS.RESERVED }]);
+ expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_SYMBOLS.RESERVED }]);
});
it('extracts comma-separated variable lists', () => {
it('handles fragment expansion', () => {
const vars = extractTemplateVariables('http://example.com/page{#section}');
- expect(vars).toEqual([{ name: 'section', operator: URI_TEMPLATE_OPERATORS.FRAGMENT }]);
+ expect(vars).toEqual([{ name: 'section', operator: URI_TEMPLATE_SYMBOLS.FRAGMENT }]);
});
it('handles path segment expansion', () => {
const vars = extractTemplateVariables('http://example.com{/path}');
- expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_OPERATORS.PATH_SEGMENT }]);
+ expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_SYMBOLS.PATH_SEGMENT }]);
});
it('returns empty array for template without variables', () => {
-import { GLOB_WILDCARD, PATH_NAV_MAX_DEPTH } from '$lib/constants';
+import { GLOB, SEARCH } from '$lib/constants';
import {
buildCaseInsensitiveGlob,
buildGlobSearchArgs,
expect(args.path).toBe('~');
expect(args.include).toBe(buildCaseInsensitiveGlob('proj'));
- expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
+ expect(args.maxDepth).toBe(SEARCH.PATH_NAV_MAX_DEPTH);
expect(args.rankQuery).toBe('proj');
expect(args.last).toBe('proj');
});
const args = buildGlobSearchArgs('~/', '/home', DEPTH);
expect(args.path).toBe('~');
- expect(args.include).toBe(GLOB_WILDCARD);
- expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
+ expect(args.include).toBe(GLOB.WILDCARD);
+ expect(args.maxDepth).toBe(SEARCH.PATH_NAV_MAX_DEPTH);
});
it('navigates an absolute path under its root', () => {
expect(args.path).toBe('/usr/local');
expect(args.include).toBe(buildCaseInsensitiveGlob('bin'));
- expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
+ expect(args.maxDepth).toBe(SEARCH.PATH_NAV_MAX_DEPTH);
expect(args.rankQuery).toBe('bin');
expect(args.last).toBe('bin');
});
import { nerdamerPlugin } from './scripts/vite-plugin-nerdamer';
import { relativizeBasePlugin } from './scripts/vite-plugin-relativize-base';
import { splashScreenPlugin } from './scripts/vite-plugin-splash-screen';
-import { SVELTEKIT_PWA_OPTIONS } from './src/lib/constants/pwa';
+import { SVELTEKIT_PWA_OPTIONS } from './src/lib/constants/pwa.constants';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';