refactor: Cleanup
This commit is contained in:
parent
d22760598d
commit
2bc1111f8d
|
|
@ -5,6 +5,7 @@
|
|||
import { ChatMessageStatsView } from '$lib/enums';
|
||||
import type { ChatMessageAgenticTimings } from '$lib/types/chat';
|
||||
import { formatPerformanceTime } from '$lib/utils';
|
||||
import { MS_PER_SECOND, DEFAULT_PERFORMANCE_TIME } from '$lib/constants/formatters';
|
||||
|
||||
interface Props {
|
||||
predictedTokens?: number;
|
||||
|
|
@ -65,14 +66,16 @@
|
|||
predictedMs > 0
|
||||
);
|
||||
|
||||
let tokensPerSecond = $derived(hasGenerationStats ? (predictedTokens! / predictedMs!) * 1000 : 0);
|
||||
let tokensPerSecond = $derived(
|
||||
hasGenerationStats ? (predictedTokens! / predictedMs!) * MS_PER_SECOND : 0
|
||||
);
|
||||
let formattedTime = $derived(
|
||||
predictedMs !== undefined ? formatPerformanceTime(predictedMs) : '0s'
|
||||
predictedMs !== undefined ? formatPerformanceTime(predictedMs) : DEFAULT_PERFORMANCE_TIME
|
||||
);
|
||||
|
||||
let promptTokensPerSecond = $derived(
|
||||
promptTokens !== undefined && promptMs !== undefined && promptMs > 0
|
||||
? (promptTokens / promptMs) * 1000
|
||||
? (promptTokens / promptMs) * MS_PER_SECOND
|
||||
: undefined
|
||||
);
|
||||
|
||||
|
|
@ -94,12 +97,12 @@
|
|||
|
||||
let agenticToolsPerSecond = $derived(
|
||||
hasAgenticStats && agenticTimings!.toolsMs > 0
|
||||
? (agenticTimings!.toolCallsCount / agenticTimings!.toolsMs) * 1000
|
||||
? (agenticTimings!.toolCallsCount / agenticTimings!.toolsMs) * MS_PER_SECOND
|
||||
: 0
|
||||
);
|
||||
|
||||
let formattedAgenticToolsTime = $derived(
|
||||
hasAgenticStats ? formatPerformanceTime(agenticTimings!.toolsMs) : '0s'
|
||||
hasAgenticStats ? formatPerformanceTime(agenticTimings!.toolsMs) : DEFAULT_PERFORMANCE_TIME
|
||||
);
|
||||
|
||||
let agenticTotalTimeMs = $derived(
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@
|
|||
import { config, settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { setMode } from 'mode-watcher';
|
||||
import { ColorMode } from '$lib/enums/ui';
|
||||
import { SettingsFieldType } from '$lib/enums/settings';
|
||||
import type { Component } from 'svelte';
|
||||
import { NUMERIC_FIELDS, POSITIVE_INTEGER_FIELDS } from '$lib/constants/settings-fields';
|
||||
import { SETTINGS_COLOR_MODES_CONFIG } from '$lib/constants/settings-config';
|
||||
import { SETTINGS_SECTION_TITLES } from '$lib/constants/settings-sections';
|
||||
import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
|
||||
import type { SettingsSectionTitle } from '$lib/constants/settings-sections';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -43,42 +45,42 @@
|
|||
icon: Settings,
|
||||
fields: [
|
||||
{
|
||||
key: 'theme',
|
||||
key: SETTINGS_KEYS.THEME,
|
||||
label: 'Theme',
|
||||
type: 'select',
|
||||
type: SettingsFieldType.SELECT,
|
||||
options: SETTINGS_COLOR_MODES_CONFIG
|
||||
},
|
||||
{ key: 'apiKey', label: 'API Key', type: 'input' },
|
||||
{ key: SETTINGS_KEYS.API_KEY, label: 'API Key', type: SettingsFieldType.INPUT },
|
||||
{
|
||||
key: 'systemMessage',
|
||||
key: SETTINGS_KEYS.SYSTEM_MESSAGE,
|
||||
label: 'System Message',
|
||||
type: 'textarea'
|
||||
type: SettingsFieldType.TEXTAREA
|
||||
},
|
||||
{
|
||||
key: 'pasteLongTextToFileLen',
|
||||
key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
|
||||
label: 'Paste long text to file length',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'copyTextAttachmentsAsPlainText',
|
||||
key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
|
||||
label: 'Copy text attachments as plain text',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'enableContinueGeneration',
|
||||
key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
|
||||
label: 'Enable "Continue" button',
|
||||
type: 'checkbox',
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
isExperimental: true
|
||||
},
|
||||
{
|
||||
key: 'pdfAsImage',
|
||||
key: SETTINGS_KEYS.PDF_AS_IMAGE,
|
||||
label: 'Parse PDF as image',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'askForTitleConfirmation',
|
||||
key: SETTINGS_KEYS.ASK_FOR_TITLE_CONFIRMATION,
|
||||
label: 'Ask for confirmation before changing conversation title',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -87,45 +89,45 @@
|
|||
icon: Monitor,
|
||||
fields: [
|
||||
{
|
||||
key: 'showMessageStats',
|
||||
key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
|
||||
label: 'Show message generation statistics',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'showThoughtInProgress',
|
||||
key: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS,
|
||||
label: 'Show thought in progress',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'keepStatsVisible',
|
||||
key: SETTINGS_KEYS.KEEP_STATS_VISIBLE,
|
||||
label: 'Keep stats visible after generation',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'autoMicOnEmpty',
|
||||
key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
|
||||
label: 'Show microphone on empty input',
|
||||
type: 'checkbox',
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
isExperimental: true
|
||||
},
|
||||
{
|
||||
key: 'renderUserContentAsMarkdown',
|
||||
key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN,
|
||||
label: 'Render user content as Markdown',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'disableAutoScroll',
|
||||
key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL,
|
||||
label: 'Disable automatic scroll',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'alwaysShowSidebarOnDesktop',
|
||||
key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP,
|
||||
label: 'Always show sidebar on desktop',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'autoShowSidebarOnNewChat',
|
||||
key: SETTINGS_KEYS.AUTO_SHOW_SIDEBAR_ON_NEW_CHAT,
|
||||
label: 'Auto-show sidebar on new chat',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -134,64 +136,64 @@
|
|||
icon: Funnel,
|
||||
fields: [
|
||||
{
|
||||
key: 'temperature',
|
||||
key: SETTINGS_KEYS.TEMPERATURE,
|
||||
label: 'Temperature',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dynatemp_range',
|
||||
key: SETTINGS_KEYS.DYNATEMP_RANGE,
|
||||
label: 'Dynamic temperature range',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dynatemp_exponent',
|
||||
key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
|
||||
label: 'Dynamic temperature exponent',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'top_k',
|
||||
key: SETTINGS_KEYS.TOP_K,
|
||||
label: 'Top K',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'top_p',
|
||||
key: SETTINGS_KEYS.TOP_P,
|
||||
label: 'Top P',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'min_p',
|
||||
key: SETTINGS_KEYS.MIN_P,
|
||||
label: 'Min P',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'xtc_probability',
|
||||
key: SETTINGS_KEYS.XTC_PROBABILITY,
|
||||
label: 'XTC probability',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'xtc_threshold',
|
||||
key: SETTINGS_KEYS.XTC_THRESHOLD,
|
||||
label: 'XTC threshold',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'typ_p',
|
||||
key: SETTINGS_KEYS.TYP_P,
|
||||
label: 'Typical P',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'max_tokens',
|
||||
key: SETTINGS_KEYS.MAX_TOKENS,
|
||||
label: 'Max tokens',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'samplers',
|
||||
key: SETTINGS_KEYS.SAMPLERS,
|
||||
label: 'Samplers',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'backend_sampling',
|
||||
key: SETTINGS_KEYS.BACKEND_SAMPLING,
|
||||
label: 'Backend sampling',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -200,44 +202,44 @@
|
|||
icon: AlertTriangle,
|
||||
fields: [
|
||||
{
|
||||
key: 'repeat_last_n',
|
||||
key: SETTINGS_KEYS.REPEAT_LAST_N,
|
||||
label: 'Repeat last N',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'repeat_penalty',
|
||||
key: SETTINGS_KEYS.REPEAT_PENALTY,
|
||||
label: 'Repeat penalty',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'presence_penalty',
|
||||
key: SETTINGS_KEYS.PRESENCE_PENALTY,
|
||||
label: 'Presence penalty',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'frequency_penalty',
|
||||
key: SETTINGS_KEYS.FREQUENCY_PENALTY,
|
||||
label: 'Frequency penalty',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dry_multiplier',
|
||||
key: SETTINGS_KEYS.DRY_MULTIPLIER,
|
||||
label: 'DRY multiplier',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dry_base',
|
||||
key: SETTINGS_KEYS.DRY_BASE,
|
||||
label: 'DRY base',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dry_allowed_length',
|
||||
key: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
|
||||
label: 'DRY allowed length',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'dry_penalty_last_n',
|
||||
key: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
|
||||
label: 'DRY penalty last N',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -251,24 +253,24 @@
|
|||
icon: McpLogo,
|
||||
fields: [
|
||||
{
|
||||
key: 'agenticMaxTurns',
|
||||
key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
|
||||
label: 'Agentic loop max turns',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'alwaysShowAgenticTurns',
|
||||
key: SETTINGS_KEYS.ALWAYS_SHOW_AGENTIC_TURNS,
|
||||
label: 'Always show agentic turns in conversation',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'agenticMaxToolPreviewLines',
|
||||
key: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
|
||||
label: 'Max lines per tool preview',
|
||||
type: 'input'
|
||||
type: SettingsFieldType.INPUT
|
||||
},
|
||||
{
|
||||
key: 'showToolCallInProgress',
|
||||
key: SETTINGS_KEYS.SHOW_TOOL_CALL_IN_PROGRESS,
|
||||
label: 'Show tool call in progress',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -277,24 +279,19 @@
|
|||
icon: Code,
|
||||
fields: [
|
||||
{
|
||||
key: 'disableReasoningParsing',
|
||||
key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
|
||||
label: 'Disable reasoning content parsing',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'showRawOutputSwitch',
|
||||
key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
|
||||
label: 'Enable raw output toggle',
|
||||
type: 'checkbox'
|
||||
type: SettingsFieldType.CHECKBOX
|
||||
},
|
||||
{
|
||||
key: 'showRawOutputSwitch',
|
||||
label: 'Enable raw output toggle',
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
key: 'custom',
|
||||
key: SETTINGS_KEYS.CUSTOM,
|
||||
label: 'Custom JSON',
|
||||
type: 'textarea'
|
||||
type: SettingsFieldType.TEXTAREA
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
import * as Select from '$lib/components/ui/select';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
|
||||
import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
|
||||
import { SettingsFieldType } from '$lib/enums/settings';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
|
||||
import type { Component } from 'svelte';
|
||||
|
|
@ -31,7 +33,7 @@
|
|||
|
||||
{#each fields as field (field.key)}
|
||||
<div class="space-y-2">
|
||||
{#if field.type === 'input'}
|
||||
{#if field.type === SettingsFieldType.INPUT}
|
||||
{@const paramInfo = getParameterSourceInfo(field.key)}
|
||||
{@const currentValue = String(localConfig[field.key] ?? '')}
|
||||
{@const propsDefault = paramInfo?.serverDefault}
|
||||
|
|
@ -98,7 +100,7 @@
|
|||
{@html field.help || SETTING_CONFIG_INFO[field.key]}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if field.type === 'textarea'}
|
||||
{:else if field.type === SettingsFieldType.TEXTAREA}
|
||||
<Label for={field.key} class="block flex items-center gap-1.5 text-sm font-medium">
|
||||
{field.label}
|
||||
|
||||
|
|
@ -121,7 +123,7 @@
|
|||
</p>
|
||||
{/if}
|
||||
|
||||
{#if field.key === 'systemMessage'}
|
||||
{#if field.key === SETTINGS_KEYS.SYSTEM_MESSAGE}
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="showSystemMessage"
|
||||
|
|
@ -134,7 +136,7 @@
|
|||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if field.type === 'select'}
|
||||
{:else if field.type === SettingsFieldType.SELECT}
|
||||
{@const selectedOption = field.options?.find(
|
||||
(opt: { value: string; label: string; icon?: Component }) =>
|
||||
opt.value === localConfig[field.key]
|
||||
|
|
@ -166,7 +168,7 @@
|
|||
type="single"
|
||||
value={currentValue}
|
||||
onValueChange={(value) => {
|
||||
if (field.key === 'theme' && value && onThemeChange) {
|
||||
if (field.key === SETTINGS_KEYS.THEME && value && onThemeChange) {
|
||||
onThemeChange(value);
|
||||
} else {
|
||||
onConfigChange(field.key, value);
|
||||
|
|
@ -222,7 +224,7 @@
|
|||
{field.help || SETTING_CONFIG_INFO[field.key]}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if field.type === 'checkbox'}
|
||||
{:else if field.type === SettingsFieldType.CHECKBOX}
|
||||
<div class="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id={field.key}
|
||||
|
|
|
|||
|
|
@ -367,8 +367,10 @@
|
|||
if (appendMode && index < previousBlockCount) {
|
||||
const prevBlock = renderedBlocks[index];
|
||||
const currentHash = getMdastNodeHash(child, index);
|
||||
|
||||
if (prevBlock?.contentHash === currentHash) {
|
||||
nextBlocks.push(prevBlock);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -379,6 +381,7 @@
|
|||
{ position: (child as { position?: unknown }).position } as HastRootContent,
|
||||
index
|
||||
);
|
||||
|
||||
nextBlocks.push({ id, html, contentHash: hash });
|
||||
}
|
||||
|
||||
|
|
@ -390,6 +393,7 @@
|
|||
previousContent = prefixMarkdown;
|
||||
unstableBlockHtml = '';
|
||||
incompleteCodeBlock = incompleteBlock;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +420,7 @@
|
|||
const currentHash = getMdastNodeHash(child, index);
|
||||
if (prevBlock?.contentHash === currentHash) {
|
||||
nextBlocks.push(prevBlock);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return 'json';
|
||||
if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return 'javascript';
|
||||
if (extra.mimeType?.includes(MimeTypeIncludes.TYPESCRIPT)) return 'typescript';
|
||||
// Try to detect from URI/name
|
||||
|
||||
const name = extra.name || extra.uri || '';
|
||||
return getLanguageFromFilename(name) || 'plaintext';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export function buildResourceTree(
|
|||
}
|
||||
|
||||
const fileName = pathParts[pathParts.length - 1] || resource.name;
|
||||
|
||||
current.children.set(resource.uri, {
|
||||
name: fileName,
|
||||
resource: { ...resource, serverName },
|
||||
|
|
@ -71,7 +72,6 @@ export function buildResourceTree(
|
|||
});
|
||||
}
|
||||
|
||||
// Clean up empty folders that don't match
|
||||
function cleanupEmptyFolders(node: ResourceTreeNode): boolean {
|
||||
if (node.resource) return true;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
|
|||
|
||||
export const NEWLINE_SEPARATOR = '\n';
|
||||
|
||||
export const TURN_LIMIT_MESSAGE = '\n\n```\nTurn limit reached\n```\n';
|
||||
|
||||
export const LLM_ERROR_BLOCK_START = '\n\n```\nUpstream LLM error:\n';
|
||||
export const LLM_ERROR_BLOCK_END = '\n```\n';
|
||||
|
||||
export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
|
||||
enabled: true,
|
||||
maxTurns: 100,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,18 @@ export const MODEL_PROPS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|||
*/
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,3 +3,6 @@ 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';
|
||||
|
|
|
|||
|
|
@ -12,6 +12,21 @@ 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';
|
||||
|
||||
/**
|
||||
* Mapping from image MIME types to file extensions.
|
||||
* Used for generating attachment filenames from MIME types.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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 = {
|
||||
// General
|
||||
THEME: 'theme',
|
||||
API_KEY: 'apiKey',
|
||||
SYSTEM_MESSAGE: 'systemMessage',
|
||||
PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
|
||||
COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
|
||||
ENABLE_CONTINUE_GENERATION: 'enableContinueGeneration',
|
||||
PDF_AS_IMAGE: 'pdfAsImage',
|
||||
ASK_FOR_TITLE_CONFIRMATION: 'askForTitleConfirmation',
|
||||
// Display
|
||||
SHOW_MESSAGE_STATS: 'showMessageStats',
|
||||
SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
|
||||
KEEP_STATS_VISIBLE: 'keepStatsVisible',
|
||||
AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
|
||||
RENDER_USER_CONTENT_AS_MARKDOWN: 'renderUserContentAsMarkdown',
|
||||
DISABLE_AUTO_SCROLL: 'disableAutoScroll',
|
||||
ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
|
||||
AUTO_SHOW_SIDEBAR_ON_NEW_CHAT: 'autoShowSidebarOnNewChat',
|
||||
// Sampling
|
||||
TEMPERATURE: 'temperature',
|
||||
DYNATEMP_RANGE: 'dynatemp_range',
|
||||
DYNATEMP_EXPONENT: 'dynatemp_exponent',
|
||||
TOP_K: 'top_k',
|
||||
TOP_P: 'top_p',
|
||||
MIN_P: 'min_p',
|
||||
XTC_PROBABILITY: 'xtc_probability',
|
||||
XTC_THRESHOLD: 'xtc_threshold',
|
||||
TYP_P: 'typ_p',
|
||||
MAX_TOKENS: 'max_tokens',
|
||||
SAMPLERS: 'samplers',
|
||||
BACKEND_SAMPLING: 'backend_sampling',
|
||||
// Penalties
|
||||
REPEAT_LAST_N: 'repeat_last_n',
|
||||
REPEAT_PENALTY: 'repeat_penalty',
|
||||
PRESENCE_PENALTY: 'presence_penalty',
|
||||
FREQUENCY_PENALTY: 'frequency_penalty',
|
||||
DRY_MULTIPLIER: 'dry_multiplier',
|
||||
DRY_BASE: 'dry_base',
|
||||
DRY_ALLOWED_LENGTH: 'dry_allowed_length',
|
||||
DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
|
||||
// MCP
|
||||
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
|
||||
ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns',
|
||||
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
|
||||
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
|
||||
// Developer
|
||||
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
|
||||
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
|
||||
CUSTOM: 'custom'
|
||||
} as const;
|
||||
|
|
@ -44,7 +44,7 @@ export { ModelModality } from './model';
|
|||
|
||||
export { ServerRole, ServerModelStatus } from './server';
|
||||
|
||||
export { ParameterSource, SyncableParameterType } from './settings';
|
||||
export { ParameterSource, SyncableParameterType, SettingsFieldType } from './settings';
|
||||
|
||||
export { ColorMode, McpPromptVariant, UrlPrefix } from './ui';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,3 +14,13 @@ export enum SyncableParameterType {
|
|||
STRING = 'string',
|
||||
BOOLEAN = 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings field type - defines the input type for settings fields
|
||||
*/
|
||||
export enum SettingsFieldType {
|
||||
INPUT = 'input',
|
||||
TEXTAREA = 'textarea',
|
||||
CHECKBOX = 'checkbox',
|
||||
SELECT = 'select'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,21 @@ import { config } from '$lib/stores/settings.svelte';
|
|||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { isAbortError } from '$lib/utils';
|
||||
import { DEFAULT_AGENTIC_CONFIG, AGENTIC_TAGS } from '$lib/constants/agentic';
|
||||
import { IMAGE_MIME_TO_EXTENSION } from '$lib/constants/mcp-resource';
|
||||
import { AttachmentType, ContentPartType, MessageRole } from '$lib/enums';
|
||||
import {
|
||||
DEFAULT_AGENTIC_CONFIG,
|
||||
AGENTIC_TAGS,
|
||||
NEWLINE_SEPARATOR,
|
||||
TURN_LIMIT_MESSAGE,
|
||||
LLM_ERROR_BLOCK_START,
|
||||
LLM_ERROR_BLOCK_END
|
||||
} from '$lib/constants/agentic';
|
||||
import {
|
||||
IMAGE_MIME_TO_EXTENSION,
|
||||
DATA_URI_BASE64_REGEX,
|
||||
MCP_ATTACHMENT_NAME_PREFIX,
|
||||
DEFAULT_IMAGE_EXTENSION
|
||||
} from '$lib/constants/mcp-resource';
|
||||
import { AttachmentType, ContentPartType, MessageRole, MimeTypePrefix } from '$lib/enums';
|
||||
import type {
|
||||
AgenticFlowParams,
|
||||
AgenticFlowResult,
|
||||
|
|
@ -425,7 +437,7 @@ class AgenticStore {
|
|||
return;
|
||||
}
|
||||
const normalizedError = error instanceof Error ? error : new Error('LLM stream error');
|
||||
onChunk?.(`\n\n\`\`\`\nUpstream LLM error:\n${normalizedError.message}\n\`\`\`\n`);
|
||||
onChunk?.(`${LLM_ERROR_BLOCK_START}${normalizedError.message}${LLM_ERROR_BLOCK_END}`);
|
||||
onComplete?.(
|
||||
'',
|
||||
undefined,
|
||||
|
|
@ -573,7 +585,7 @@ class AgenticStore {
|
|||
if (turnStats.toolCalls.length > 0) agenticTimings.perTurn!.push(turnStats);
|
||||
}
|
||||
|
||||
onChunk?.('\n\n```\nTurn limit reached\n```\n');
|
||||
onChunk?.(TURN_LIMIT_MESSAGE);
|
||||
onComplete?.('', undefined, this.buildFinalTimings(capturedTimings, agenticTimings), undefined);
|
||||
}
|
||||
|
||||
|
|
@ -611,7 +623,7 @@ class AgenticStore {
|
|||
}
|
||||
|
||||
let output = `\n${AGENTIC_TAGS.TOOL_ARGS_END}`;
|
||||
const lines = result.split('\n');
|
||||
const lines = result.split(NEWLINE_SEPARATOR);
|
||||
const trimmedLines = lines.length > maxLines ? lines.slice(-maxLines) : lines;
|
||||
|
||||
output += `\n${trimmedLines.join('\n')}\n${AGENTIC_TAGS.TOOL_CALL_END}\n`;
|
||||
|
|
@ -626,14 +638,14 @@ class AgenticStore {
|
|||
return { cleanedResult: result, attachments: [] };
|
||||
}
|
||||
|
||||
const lines = result.split('\n');
|
||||
const lines = result.split(NEWLINE_SEPARATOR);
|
||||
const attachments: DatabaseMessageExtra[] = [];
|
||||
let attachmentIndex = 0;
|
||||
|
||||
const cleanedLines = lines.map((line) => {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
const match = trimmedLine.match(/^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/);
|
||||
const match = trimmedLine.match(DATA_URI_BASE64_REGEX);
|
||||
if (!match) {
|
||||
return line;
|
||||
}
|
||||
|
|
@ -648,7 +660,7 @@ class AgenticStore {
|
|||
attachmentIndex += 1;
|
||||
const name = this.buildAttachmentName(mimeType, attachmentIndex);
|
||||
|
||||
if (mimeType.startsWith('image/')) {
|
||||
if (mimeType.startsWith(MimeTypePrefix.IMAGE)) {
|
||||
attachments.push({ type: AttachmentType.IMAGE, name, base64Url: trimmedLine });
|
||||
|
||||
return `[Attachment saved: ${name}]`;
|
||||
|
|
@ -657,12 +669,12 @@ class AgenticStore {
|
|||
return line;
|
||||
});
|
||||
|
||||
return { cleanedResult: cleanedLines.join('\n'), attachments };
|
||||
return { cleanedResult: cleanedLines.join(NEWLINE_SEPARATOR), attachments };
|
||||
}
|
||||
|
||||
private buildAttachmentName(mimeType: string, index: number): string {
|
||||
const extension = IMAGE_MIME_TO_EXTENSION[mimeType] ?? 'img';
|
||||
return `mcp-attachment-${Date.now()}-${index}.${extension}`;
|
||||
const extension = IMAGE_MIME_TO_EXTENSION[mimeType] ?? DEFAULT_IMAGE_EXTENSION;
|
||||
return `${MCP_ATTACHMENT_NAME_PREFIX}-${Date.now()}-${index}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { AttachmentType } from '$lib/enums';
|
||||
import { MCP_RESOURCE_CACHE_MAX_ENTRIES, MCP_RESOURCE_CACHE_TTL_MS } from '$lib/constants/cache';
|
||||
import { MCP_RESOURCE_ATTACHMENT_ID_PREFIX } from '$lib/constants/mcp-resource';
|
||||
import type {
|
||||
MCPResource,
|
||||
MCPResourceTemplate,
|
||||
|
|
@ -24,11 +26,8 @@ import type {
|
|||
MCPServerResources
|
||||
} from '$lib/types';
|
||||
|
||||
const MAX_CACHED_RESOURCES = 50;
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
function generateAttachmentId(): string {
|
||||
return `res-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
return `${MCP_RESOURCE_ATTACHMENT_ID_PREFIX}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
class MCPResourceStore {
|
||||
|
|
@ -241,9 +240,10 @@ class MCPResourceStore {
|
|||
*/
|
||||
cacheResourceContent(resource: MCPResourceInfo, content: MCPResourceContent[]): void {
|
||||
// Enforce cache size limit
|
||||
if (this._cachedResources.size >= MAX_CACHED_RESOURCES) {
|
||||
if (this._cachedResources.size >= MCP_RESOURCE_CACHE_MAX_ENTRIES) {
|
||||
// Remove oldest entry
|
||||
const oldestKey = this._cachedResources.keys().next().value;
|
||||
|
||||
if (oldestKey) {
|
||||
this._cachedResources.delete(oldestKey);
|
||||
}
|
||||
|
|
@ -267,7 +267,8 @@ class MCPResourceStore {
|
|||
|
||||
// Check if cache is still valid
|
||||
const age = Date.now() - cached.fetchedAt.getTime();
|
||||
if (age > 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ function parseServerSettings(rawServers: unknown): MCPServerSettingsEntry[] {
|
|||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
console.warn('[MCP] Failed to parse mcpServers JSON:', error);
|
||||
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
|
|
@ -91,6 +92,7 @@ function parseServerSettings(rawServers: unknown): MCPServerSettingsEntry[] {
|
|||
return parsed.map((entry, index) => {
|
||||
const url = typeof entry?.url === 'string' ? entry.url.trim() : '';
|
||||
const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined;
|
||||
|
||||
return {
|
||||
id: generateMcpServerId((entry as { id?: unknown })?.id, index),
|
||||
enabled: Boolean((entry as { enabled?: unknown })?.enabled),
|
||||
|
|
@ -121,6 +123,7 @@ function buildServerConfig(
|
|||
console.warn('[MCP] Failed to parse custom headers JSON:', entry.headers);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: entry.url,
|
||||
transport: detectMcpTransportFromUrl(entry.url),
|
||||
|
|
@ -141,8 +144,10 @@ function checkServerEnabled(
|
|||
|
||||
if (perChatOverrides) {
|
||||
const override = perChatOverrides.find((o) => o.serverId === server.id);
|
||||
|
||||
return override?.enabled ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -156,11 +161,13 @@ function buildMcpClientConfigInternal(
|
|||
}
|
||||
|
||||
const servers: Record<string, MCPServerConfig> = {};
|
||||
|
||||
for (const [index, entry] of rawServers.entries()) {
|
||||
if (!checkServerEnabled(entry, perChatOverrides)) continue;
|
||||
const normalized = buildServerConfig(entry);
|
||||
if (normalized) servers[generateMcpServerId(entry.id, index)] = normalized;
|
||||
}
|
||||
|
||||
if (Object.keys(servers).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -228,27 +235,34 @@ class MCPStore {
|
|||
get isInitializing(): boolean {
|
||||
return this._isInitializing;
|
||||
}
|
||||
|
||||
get isInitialized(): boolean {
|
||||
return this.connections.size > 0;
|
||||
}
|
||||
|
||||
get error(): string | null {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
get toolCount(): number {
|
||||
return this._toolCount;
|
||||
}
|
||||
|
||||
get connectedServerCount(): number {
|
||||
return this._connectedServers.length;
|
||||
}
|
||||
|
||||
get connectedServerNames(): string[] {
|
||||
return this._connectedServers;
|
||||
}
|
||||
|
||||
get isEnabled(): boolean {
|
||||
const mcpConfig = buildMcpClientConfigInternal(config());
|
||||
return (
|
||||
mcpConfig !== null && mcpConfig !== undefined && Object.keys(mcpConfig.servers).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
get availableTools(): string[] {
|
||||
return Array.from(this.toolsIndex.keys());
|
||||
}
|
||||
|
|
@ -259,29 +273,45 @@ class MCPStore {
|
|||
toolCount?: number;
|
||||
connectedServers?: string[];
|
||||
}): void {
|
||||
if (state.isInitializing !== undefined) this._isInitializing = state.isInitializing;
|
||||
if (state.error !== undefined) this._error = state.error;
|
||||
if (state.toolCount !== undefined) this._toolCount = state.toolCount;
|
||||
if (state.connectedServers !== undefined) this._connectedServers = state.connectedServers;
|
||||
if (state.isInitializing !== undefined) {
|
||||
this._isInitializing = state.isInitializing;
|
||||
}
|
||||
|
||||
if (state.error !== undefined) {
|
||||
this._error = state.error;
|
||||
}
|
||||
|
||||
if (state.toolCount !== undefined) {
|
||||
this._toolCount = state.toolCount;
|
||||
}
|
||||
|
||||
if (state.connectedServers !== undefined) {
|
||||
this._connectedServers = state.connectedServers;
|
||||
}
|
||||
}
|
||||
|
||||
updateHealthCheck(serverId: string, state: HealthCheckState): void {
|
||||
this._healthChecks = { ...this._healthChecks, [serverId]: state };
|
||||
}
|
||||
|
||||
getHealthCheckState(serverId: string): HealthCheckState {
|
||||
return this._healthChecks[serverId] ?? { status: 'idle' };
|
||||
}
|
||||
|
||||
hasHealthCheck(serverId: string): boolean {
|
||||
return serverId in this._healthChecks && this._healthChecks[serverId].status !== 'idle';
|
||||
}
|
||||
|
||||
clearHealthCheck(serverId: string): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [serverId]: _removed, ...rest } = this._healthChecks;
|
||||
this._healthChecks = rest;
|
||||
}
|
||||
|
||||
clearAllHealthChecks(): void {
|
||||
this._healthChecks = {};
|
||||
}
|
||||
|
||||
clearError(): void {
|
||||
this._error = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import type { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
|||
import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
|
||||
import type { OpenAIToolDefinition } from './mcp';
|
||||
import type { DatabaseMessageExtra } from './database';
|
||||
import type { ParameterSource, SyncableParameterType } from '$lib/enums';
|
||||
import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums';
|
||||
|
||||
export type SettingsConfigValue = string | number | boolean;
|
||||
|
||||
export interface SettingsFieldConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'input' | 'textarea' | 'checkbox' | 'select';
|
||||
type: SettingsFieldType;
|
||||
isExperimental?: boolean;
|
||||
help?: string;
|
||||
options?: Array<{ value: string; label: string; icon?: typeof import('@lucide/svelte').Icon }>;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import {
|
|||
CODE_FILE_EXTENSION_REGEX,
|
||||
TEXT_FILE_EXTENSION_REGEX,
|
||||
PROTOCOL_PREFIX_REGEX,
|
||||
FILE_EXTENSION_REGEX
|
||||
FILE_EXTENSION_REGEX,
|
||||
DISPLAY_NAME_SEPARATOR_REGEX
|
||||
} from '$lib/constants/mcp-resource';
|
||||
import {
|
||||
Database,
|
||||
|
|
@ -159,7 +160,7 @@ export function parseResourcePath(uri: string): string[] {
|
|||
export function getDisplayName(pathPart: string): string {
|
||||
const withoutExt = pathPart.replace(FILE_EXTENSION_REGEX, '');
|
||||
return withoutExt
|
||||
.split(/[-_]/)
|
||||
.split(DISPLAY_NAME_SEPARATOR_REGEX)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue