]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: Constants refactor (#26908)
authorAleksander Grygier <redacted>
Thu, 13 Aug 2026 04:53:56 +0000 (06:53 +0200)
committerGitHub <redacted>
Thu, 13 Aug 2026 04:53:56 +0000 (06:53 +0200)
* refactor: Constants

* refactor: Constants/Enums cleanup

* refactor: Constant objects instead of multiple single value constants

* refactor: Cleanup constants

256 files changed:
tools/ui/pwa-assets-dark.config.ts
tools/ui/pwa-assets.config.ts
tools/ui/scripts/vite-plugin-build-info.ts
tools/ui/scripts/vite-plugin-relativize-base.ts
tools/ui/scripts/vite-plugin-splash-screen.ts
tools/ui/src/lib/components/app/actions/ActionIconCopyToClipboard.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailFile.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItemPdf.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddButton.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpServersSubmenu.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddReasoningSubmenu.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadMedia.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-media.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionCard/ChatMessageActionCard.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenActionScrollDown.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenServerError.svelte
tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte
tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/code-block-utils.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-code-blocks.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-svg-blocks.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/file-badge.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/svg-pre.ts
tools/ui/src/lib/components/app/content/MermaidPreview.svelte
tools/ui/src/lib/components/app/content/MermaidPreviewControls.svelte
tools/ui/src/lib/components/app/content/SyntaxHighlightedCode.svelte
tools/ui/src/lib/components/app/dialogs/DialogMcpResourcePreview.svelte
tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte
tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte
tools/ui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte
tools/ui/src/lib/components/app/forms/SearchInput.svelte
tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte
tools/ui/src/lib/components/app/mcp/McpResourcePreview.svelte
tools/ui/src/lib/components/app/mcp/McpResourcesBrowser/McpResourcesBrowserHeader.svelte
tools/ui/src/lib/components/app/mcp/McpResourcesBrowser/McpResourcesBrowserServerItem.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte
tools/ui/src/lib/components/app/mcp/McpServerForm.svelte
tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationConversationItem.svelte
tools/ui/src/lib/components/app/server/ServerErrorSplash.svelte
tools/ui/src/lib/components/app/server/ServerStatus.svelte
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatFields.svelte
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatImportExportSection.svelte
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatToolsTab.svelte
tools/ui/src/lib/components/app/settings/SettingsChatDesktopSidebar.svelte
tools/ui/src/lib/components/app/settings/SettingsChatMobileHeader.svelte
tools/ui/src/lib/components/pwa/PwaMetaTags.svelte
tools/ui/src/lib/constants/agentic.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/agentic.ts [deleted file]
tools/ui/src/lib/constants/api-endpoints.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/api-endpoints.ts [deleted file]
tools/ui/src/lib/constants/app.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/app.ts [deleted file]
tools/ui/src/lib/constants/attachment-labels.ts [deleted file]
tools/ui/src/lib/constants/attachment-menu.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/attachment-menu.ts [deleted file]
tools/ui/src/lib/constants/auto-scroll.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/auto-scroll.ts [deleted file]
tools/ui/src/lib/constants/binary-detection.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/binary-detection.ts [deleted file]
tools/ui/src/lib/constants/built-in-tools.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/built-in-tools.ts [deleted file]
tools/ui/src/lib/constants/cache.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/cache.ts [deleted file]
tools/ui/src/lib/constants/chat-commands.ts [deleted file]
tools/ui/src/lib/constants/chat-form.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/chat-form.ts [deleted file]
tools/ui/src/lib/constants/cli-flags.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/cli-flags.ts [deleted file]
tools/ui/src/lib/constants/code-block.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/code-blocks.ts [deleted file]
tools/ui/src/lib/constants/code.ts [deleted file]
tools/ui/src/lib/constants/content-detection.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/context-gauge-popup.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/context-gauge-popup.ts [deleted file]
tools/ui/src/lib/constants/context-keys.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/context-keys.ts [deleted file]
tools/ui/src/lib/constants/control-actions.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/control-actions.ts [deleted file]
tools/ui/src/lib/constants/conversation-import.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/conversation-import.ts [deleted file]
tools/ui/src/lib/constants/css-classes.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/css-classes.ts [deleted file]
tools/ui/src/lib/constants/database.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/database.ts [deleted file]
tools/ui/src/lib/constants/diagram-blocks.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/diagram-blocks.ts [deleted file]
tools/ui/src/lib/constants/error.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/error.ts [deleted file]
tools/ui/src/lib/constants/floating-ui-constraints.ts [deleted file]
tools/ui/src/lib/constants/formatters.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/formatters.ts [deleted file]
tools/ui/src/lib/constants/headers.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/icons.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/icons.ts [deleted file]
tools/ui/src/lib/constants/image-size.ts [deleted file]
tools/ui/src/lib/constants/image.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/index.ts
tools/ui/src/lib/constants/jpeg-exif.ts [deleted file]
tools/ui/src/lib/constants/key-value-pairs.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/key-value-pairs.ts [deleted file]
tools/ui/src/lib/constants/latex-protection.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/latex-protection.ts [deleted file]
tools/ui/src/lib/constants/literal-html.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/literal-html.ts [deleted file]
tools/ui/src/lib/constants/markdown.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/markdown.ts [deleted file]
tools/ui/src/lib/constants/max-bundle-size.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/max-bundle-size.ts [deleted file]
tools/ui/src/lib/constants/mcp-form.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/mcp-form.ts [deleted file]
tools/ui/src/lib/constants/mcp-resource.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/mcp-resource.ts [deleted file]
tools/ui/src/lib/constants/mcp.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/mcp.ts [deleted file]
tools/ui/src/lib/constants/mention-badge.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/mention-badge.ts [deleted file]
tools/ui/src/lib/constants/mermaid-blocks.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/mermaid-blocks.ts [deleted file]
tools/ui/src/lib/constants/message-export.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/message-export.ts [deleted file]
tools/ui/src/lib/constants/model-id.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/model-id.ts [deleted file]
tools/ui/src/lib/constants/model-loading.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/model-loading.ts [deleted file]
tools/ui/src/lib/constants/path-display.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/path-display.ts [deleted file]
tools/ui/src/lib/constants/precision.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/precision.ts [deleted file]
tools/ui/src/lib/constants/processing-info.ts [deleted file]
tools/ui/src/lib/constants/pwa.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/pwa.ts [deleted file]
tools/ui/src/lib/constants/reasoning-effort-tokens.ts [deleted file]
tools/ui/src/lib/constants/reasoning-effort.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/reasoning-effort.ts [deleted file]
tools/ui/src/lib/constants/recommended-mcp-servers.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/recommended-mcp-servers.ts [deleted file]
tools/ui/src/lib/constants/routes.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/routes.ts [deleted file]
tools/ui/src/lib/constants/sandbox.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/sandbox.ts [deleted file]
tools/ui/src/lib/constants/settings-keys.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/settings-keys.ts [deleted file]
tools/ui/src/lib/constants/settings-registry.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/settings-registry.ts [deleted file]
tools/ui/src/lib/constants/special-characters.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/sse.ts [deleted file]
tools/ui/src/lib/constants/storage.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/storage.ts [deleted file]
tools/ui/src/lib/constants/stream.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/stream.ts [deleted file]
tools/ui/src/lib/constants/supported-file-types.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/supported-file-types.ts [deleted file]
tools/ui/src/lib/constants/svg-blocks.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/svg-blocks.ts [deleted file]
tools/ui/src/lib/constants/table-html-restorer.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/table-html-restorer.ts [deleted file]
tools/ui/src/lib/constants/title-generation.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/title-generation.ts [deleted file]
tools/ui/src/lib/constants/tools.ts [deleted file]
tools/ui/src/lib/constants/tooltip-config.ts [deleted file]
tools/ui/src/lib/constants/ui.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/ui.ts [deleted file]
tools/ui/src/lib/constants/uri-template.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/uri-template.ts [deleted file]
tools/ui/src/lib/constants/url.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/url.ts [deleted file]
tools/ui/src/lib/constants/viewport.ts [deleted file]
tools/ui/src/lib/constants/working-directory.constants.ts [new file with mode: 0644]
tools/ui/src/lib/constants/working-directory.ts [deleted file]
tools/ui/src/lib/enums/attachment.enums.ts
tools/ui/src/lib/enums/index.ts
tools/ui/src/lib/hooks/use-chat-form-pickers.svelte.ts
tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts
tools/ui/src/lib/hooks/use-pwa.svelte.ts
tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts
tools/ui/src/lib/hooks/use-settings-navigation.svelte.ts
tools/ui/src/lib/services/chat.service.ts
tools/ui/src/lib/services/index.ts
tools/ui/src/lib/services/mcp.service.ts
tools/ui/src/lib/services/migration.service.ts
tools/ui/src/lib/services/models.service.ts
tools/ui/src/lib/services/router.service.ts
tools/ui/src/lib/services/tools.service.ts
tools/ui/src/lib/stores/agentic.svelte.ts
tools/ui/src/lib/stores/chat.svelte.ts
tools/ui/src/lib/stores/conversations.svelte.ts
tools/ui/src/lib/stores/mcp-resources.svelte.ts
tools/ui/src/lib/stores/mcp.svelte.ts
tools/ui/src/lib/stores/models.svelte.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/stores/viewport.svelte.ts
tools/ui/src/lib/types/chat.d.ts
tools/ui/src/lib/types/index.ts
tools/ui/src/lib/types/navigation.d.ts [new file with mode: 0644]
tools/ui/src/lib/types/tools.d.ts
tools/ui/src/lib/utils/agentic.ts
tools/ui/src/lib/utils/api-fetch.ts
tools/ui/src/lib/utils/api-headers.ts
tools/ui/src/lib/utils/api-key-validation.ts
tools/ui/src/lib/utils/built-in-tools.ts [new file with mode: 0644]
tools/ui/src/lib/utils/cache-ttl.ts
tools/ui/src/lib/utils/cap-img-size.ts
tools/ui/src/lib/utils/chat-commands.ts [new file with mode: 0644]
tools/ui/src/lib/utils/code.ts
tools/ui/src/lib/utils/contenteditable-tokenizer.ts
tools/ui/src/lib/utils/cors-proxy.ts
tools/ui/src/lib/utils/glob-search.ts
tools/ui/src/lib/utils/heic-to-jpeg.ts
tools/ui/src/lib/utils/index.ts
tools/ui/src/lib/utils/jpeg-orientation.ts
tools/ui/src/lib/utils/mcp.ts
tools/ui/src/lib/utils/mention-badge.ts
tools/ui/src/lib/utils/path-display.ts
tools/ui/src/lib/utils/sandbox-tool.ts [new file with mode: 0644]
tools/ui/src/lib/utils/sanitize-svg.ts
tools/ui/src/lib/utils/stream-identity.ts
tools/ui/src/lib/utils/uri-template.ts
tools/ui/src/lib/utils/working-directory.ts
tools/ui/src/routes/+error.svelte
tools/ui/src/routes/+layout.svelte
tools/ui/src/routes/search/+page.svelte
tools/ui/tests/client/apikey-splash.svelte.test.ts
tools/ui/tests/client/chat-form-enter-code-block.svelte.test.ts
tools/ui/tests/client/settings-registry-invariants.svelte.test.ts
tools/ui/tests/client/settings-render-keys-migration.svelte.test.ts
tools/ui/tests/client/ui-settings-sync.svelte.test.ts
tools/ui/tests/unit/agentic-strip.test.ts
tools/ui/tests/unit/mcp-override-fallback.test.ts
tools/ui/tests/unit/mcp-servers-default.test.ts
tools/ui/tests/unit/mcp-service.test.ts
tools/ui/tests/unit/parse-mcp-server-settings.test.ts
tools/ui/tests/unit/sanitize-headers.test.ts
tools/ui/tests/unit/uri-template.test.ts
tools/ui/tests/unit/working-directory.test.ts
tools/ui/vite.config.ts

index 1446b47a82a4b67953b1d72cc334e15b855e3ed3..4d8114ee76dfa48c041c7f07e6cb14ce74000771 100644 (file)
@@ -1,5 +1,5 @@
 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, {
index 5fed0a595743851d66916d3c8f48a8c5d7312f86..f9f8662a20af005efa095ef2a5b29f426fd9c8d9 100644 (file)
@@ -4,7 +4,7 @@ import {
        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,
index 80023863057b24e20d1dac09d6e650045c04a6a3..ec864e8d03f93776f01ed2f2dec09a65a6ab3d9a 100644 (file)
@@ -1,4 +1,4 @@
-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';
index f8eac1d66555e8665659ae1288342a5a3e433e9b..0e47741ae94dc0ca6af0165ccc3f55b22bb38d44 100644 (file)
@@ -1,4 +1,4 @@
-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';
index 45d931bab36c5710b8692fa9044cb013c032adae..62b7a063acdeea9dbf97e596fa7d11871b392625 100644 (file)
@@ -1,5 +1,10 @@
-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';
index 7655f18e804e6ab4caaf2cd1d0b9f88d32c6ba6d..2d54df89d67c8d38d334a1be07c4bc4be0067a09 100644 (file)
@@ -1,7 +1,7 @@
 <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';
index da8dd3b8689ffe9617953923414662b07f515a94..abdba0e2e043dc6e5f9c76bda3353706a9d97d6c 100644 (file)
@@ -1,7 +1,7 @@
 <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,
index 69b1724fb22c05a74a22b9bdb648e29195bdd4fe..4be156edbaeae1d281cf63bc2fef748fae72d052 100644 (file)
@@ -3,7 +3,7 @@
        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';
index 994a35f201b297f78629b993d6e816f8c7a554bb..60b72077d884031b0a1f7795bb7ba4d1051c991f 100644 (file)
@@ -1,7 +1,7 @@
 <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;
index 39a314c7dd385891492942d4e06aaf275f6224c7..ae76dedcad7a8cca8bfe7bf000a7e11f52986f33 100644 (file)
@@ -2,8 +2,7 @@
        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;
index c17c796a45859d74ef62f1a339beac71dd42357a..99aaee7845f28b9e2609e9fe062e49774a063d47 100644 (file)
@@ -12,9 +12,9 @@
        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 {
index 852bf3fb66b38f2545c75e9dbd9632b37838ae39..178345e8349da561eac8da1434e95f79fd1096cb 100644 (file)
@@ -4,8 +4,7 @@
        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';
index 03b9f9f0a9da476d1c42b8cd6876abefc7870792..a3a0b3a20fc5f1bf14195f5ae7722f3007a1438b 100644 (file)
@@ -2,7 +2,7 @@
        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();
index 73f616f69730d4ff178773e2f5ae6c0e841a74ee..299b22209f904a0b2764a53d0b7eedec9ce392c2 100644 (file)
        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';
index 844376ea65f927a3bc75a42e175066b7d9a7e348..ac3b5712fcc9baae60d4b874b2e50da94e3f3c1b 100644 (file)
@@ -4,8 +4,7 @@
        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';
index dbf73e8dd115ac4e7ce5bec18a7f88c1f687db90..d1dd3fe46c8bf6f78812aa50d343c78d684545a1 100644 (file)
@@ -2,7 +2,7 @@
        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;
index 7207165a1fc6868d21f516a0bf503ce4b5839c19..5dcf3531ae9f8f8cb990ac94a4d02af5416f053b 100644 (file)
@@ -10,8 +10,7 @@
                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 {
index 35977dfc394f11dc1ded9fc177af123dd91b2b52..ec916b3f13b3049dd39e5f3807e5d872e4adb5cf 100644 (file)
@@ -1,5 +1,5 @@
 <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
index c69b6a995dd9d8ebe8f58b9e05671c6e511e927b..86a9fb81cd2f5f07511868a078a085c9b55e734c 100644 (file)
@@ -4,11 +4,7 @@
        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';
@@ -70,7 +66,7 @@
        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);
@@ -80,7 +76,7 @@
 
        const search = useDebouncedSearch({
                canRun: () => isOpen && fileSearchEnabled,
-               debounceMs: SEARCH_DEBOUNCE_MS,
+               debounceMs: SEARCH.DEBOUNCE_MS,
                getQuery: () => trimmedQuery,
                run: async (query, signal, isCurrent) => {
                        try {
index fb0b31817ea604d668ed13f92e5ab5d89de4a15c..27bc40b24a3baa7061d7092d677139e96e439879 100644 (file)
@@ -4,16 +4,7 @@
        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
                        });
index 13702102c4ef83bcd8e5dbda565e1055a772899d..2cc3fb42f20bfe0c8f3b12b72e712679946e9941 100644 (file)
@@ -7,9 +7,7 @@
                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';
index 87208e2daa61e0dcab64230eb821a9f02287aaff..79120cb175a15d6c05c398b18e382fd6a29dd8db 100644 (file)
@@ -7,13 +7,13 @@
        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';
index 907a3cd12ab6902faa571eed38d47d56c9eaf9f0..ef2bb74336ee8a7bbfdaf4658251c0c3ffc5e7ce 100644 (file)
@@ -10,8 +10,7 @@
        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';
index c4aa28a358f1a4e455f5c7c1d16e3c2975c4e79f..a655d86a5f2fa44209478376dcd6d1aa54070f6b 100644 (file)
@@ -2,7 +2,7 @@
        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 {
@@ -32,7 +32,7 @@
                {#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}
index 424c794d0baebf697ea49cacafbb74cf6e6e33ed..426fb454407b7ff3a3b047e814c42109fff0b1e2 100644 (file)
@@ -1,7 +1,7 @@
 <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';
index 2a9cc493fd2b6cbe9b2157e54127908088581ef1..142ca843741fce27f52b6da7c77953ec9d6a2553 100644 (file)
@@ -2,7 +2,7 @@
        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 {
index c292544ac6eb735ebbe53a8ad56d739cf835deee..c43495b6c7d32a8cbb6f230292ec165da930d69d 100644 (file)
 
        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 };
index cbde40df46c9d39947b1eaee745597fc16411f85..0b275f9732e4da83540cecf48c7dd11d0db423cc 100644 (file)
@@ -4,11 +4,7 @@
 // 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';
 
@@ -50,7 +46,9 @@ export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null
        }
 
        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 };
 }
index ab1a4d6aa01bd1670c0a43d420927e6f4543b968..fbfdb33626adefc36ac742116971d42a4c0014f2 100644 (file)
@@ -1,4 +1,4 @@
-import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants/code';
+import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants';
 import {
        PREFIX_FILE,
        PREFIX_MIME,
index cdec283a02433b4dce1e2f4eabe8d58327ef4bd4..df91d5cae038dfb52298b140669b438d3a2169ac 100644 (file)
@@ -4,11 +4,7 @@
 // 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';
 
@@ -36,7 +32,8 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
        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;
index 3a60e674cda0fac6d7a80d490ac5ac988833b067..0ee66829abae01ee4c7d8433905ef54243db9015 100644 (file)
@@ -1,5 +1,5 @@
 <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 {
index ea3743e6c45cd6bc7cf1f10bc13385ff5f996383..ebcc7279a9e2d78f22e934edb3e859857c69197a 100644 (file)
@@ -1,7 +1,7 @@
 <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';
index 15f4813a18fdb462e5951419abe47aa232cd1c45..6fd27212d9e629ea6f378b918e5fe98a78ee1296 100644 (file)
@@ -1,7 +1,7 @@
 <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>
index ae5d91beee5e9f59e6ecb451592c9de22fec84d7..cae37d6f8a8b6da6565aef3fcebde85dc01fa126 100644 (file)
@@ -1,7 +1,7 @@
 <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());
index ec57985acabe589ec4e62f4b661add2644b2f6dc..042a83e0e22b8be7d1354406916fc793cf681417 100644 (file)
@@ -2,7 +2,7 @@
        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';
 
index c768a04471ac18b45f61db7ed30b429a0b11896f..edfa32d48b70d15da33fd60386ffab10175a6b11 100644 (file)
@@ -26,7 +26,7 @@
        } 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>
index cd093655ca845252119f3e5db3508e06211c2646..4eb38e49fd01ee3c3eb747ce55d341c32f7d44c6 100644 (file)
@@ -4,17 +4,11 @@
  */
 
 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';
@@ -65,7 +59,7 @@ export function createButton(
  * 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);
 }
 
 /**
@@ -76,7 +70,7 @@ export function createPreviewButton(
        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);
 }
 
 /**
@@ -120,7 +114,7 @@ export function createSourceView(
                                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'
        };
@@ -134,7 +128,7 @@ export function createBlockHeader(
        id: string,
        idAttribute: string,
        actions: Element[],
-       languageClassName: string = CODE_LANGUAGE_CLASS
+       languageClassName: string = CODE_BLOCK_CLASS.LANGUAGE
 ): Element {
        return {
                children: [
@@ -146,12 +140,12 @@ export function createBlockHeader(
                        },
                        {
                                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'
        };
@@ -185,7 +179,7 @@ export function createWrapper(
        return {
                children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren],
                properties: {
-                       className: [wrapperClass, RELATIVE_CLASS],
+                       className: [wrapperClass, CODE_BLOCK_CLASS.RELATIVE],
                        ...additionalAttributes
                } as Element['properties'],
                tagName: 'div',
index 769e535fd782668da752848814257286b5c95282..54d401c11666de30643828be47ae9a59fc0b2013 100644 (file)
@@ -17,7 +17,7 @@ import {
        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';
@@ -78,8 +78,8 @@ export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
                        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
index 70976ad7c514d4402d10b52b35032bb53c072bdc..e1ec4898e3f95b84f2e28d6062fef37a40c47617 100644 (file)
@@ -19,16 +19,7 @@ import {
        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';
@@ -48,11 +39,11 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
 
                        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) => {
@@ -65,26 +56,26 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                        // 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]
                        );
index e8bc37eca75ce3eec71e28143fdff97e62763982..24a78de46fe19a9afa07715ce0ca00b5b51432f1 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * 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,
index df2cd5cbd64a974a9d8b4cce2004a40dcf580b2d..7baa95ca4180758670d2a72096a79c8e24eaf010 100644 (file)
@@ -1,5 +1,5 @@
 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.
@@ -7,7 +7,7 @@ import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_TAG_PREFIX, XML_LANGUAGE } from '$li
  * 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)
 );
index 417600211933da62acf698b3d7b0dc905de3cc14..77d20ced3f05e4a2606fd9ec0b75ad137501174a 100644 (file)
@@ -1,6 +1,6 @@
 <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 {
@@ -13,7 +13,7 @@
 
        // 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
index 1dd341f5a49c450efae981e19bd665b77b2f6111..938da066c27a5c81225d0503a0a29faa99db9824 100644 (file)
@@ -3,7 +3,7 @@
        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;
index 1261de4a899cec353dd2297a866b8e5919ce7813..829c8460344d5a9087569e10071b01ccb5bfea76 100644 (file)
@@ -1,6 +1,6 @@
 <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';
index 537dd4c50c0fd2dae485bebed0322ff2587ca166..6931d19ac0516ce99f75459b34e5d6ba7805c949 100644 (file)
@@ -3,8 +3,8 @@
        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 || '';
 
index c208250cdd8a985004732b86b7b3265e4ea43c1f..7e1b397fc89949411b3eaa623748305fd7e23ab8 100644 (file)
@@ -7,7 +7,7 @@
        } 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 {
index 09780173a5effc6d21bef357e1afea91c23e89aa..03d55cd0f5bb873e047689756d1af69dbcd55ee6 100644 (file)
@@ -4,13 +4,12 @@
        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)
                );
 
index e77923bb34cc0c1516db94e43fc3349ab335d275..1f0ac2fca47841237e2dbbaf09ba7a2842bd0005 100644 (file)
@@ -3,8 +3,7 @@
        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;
index 488249b884860473f756b62049f0378f9efa4c73..1ed68a075fe14abffb6eb1b18644cc23742fac93 100644 (file)
@@ -1,7 +1,7 @@
 <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;
index b3f61c2a9b0756f79de6bbfa9054f644f60921a7..63210ad723e4c273d7737b62a0c6c4e6462b171a 100644 (file)
@@ -1,8 +1,7 @@
 <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';
index eb533d4aeb6310bfe225a17927ce4d7618c389bd..fb57dbfac9a222b3e1002dfa5336bae4e095663e 100644 (file)
@@ -2,7 +2,7 @@
        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';
index f10566ee69024e217f7b9307a8280bbd8cded64f..fd7af18748c2191e247bee05207552aba6e9cbec 100644 (file)
@@ -2,7 +2,7 @@
        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;
index 0c35559eead17308a8913b83268d1297a5c68117..7b349043b84f256bd6d6b02f2df149f2790c32c7 100644 (file)
@@ -9,7 +9,7 @@
        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';
index 3dc1bc7d0cc1332a035e1c1a139cca432e88fca5..6072e38660f834a644fd4086b0dd95443c836662 100644 (file)
@@ -10,7 +10,7 @@
        } 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';
index 528257e01d859bf00fa68e8a3bf3fae3d82865c1..5a5b01ffe3d4eed8965dfbf38e0cb41c5b6daebd 100644 (file)
@@ -2,13 +2,7 @@
        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);
index 68cd1e0450e22212f89ee642e65e8c22fbf68992..e2edb4d02574172a8a7dd57cabb9b4704d45c6b2 100644 (file)
@@ -1,6 +1,6 @@
 <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 {
index a7ce0310e2efe0e3333c6d43a96056a3ae2307bd..322c03efa8b7b34374ea548dea284dcb8937aaa2 100644 (file)
@@ -11,7 +11,7 @@
                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';
index 0aa7e7aef64648376d3043fd8a52c640484b0e97..dbb0291adcbdf7cecf1053066b4852ae6df3b608 100644 (file)
@@ -5,12 +5,12 @@
        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';
index 9a9fb8f52e239c697908b0ac07bcdc77c2c66817..2f4b297d8556d4410f21ff16f95b34f671066389 100644 (file)
@@ -15,8 +15,7 @@
        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';
index ffbfd9fdb6270c125d43b5e8aaa3f746267cf876..28bb2cfe144d11d664b6c0221f9f2281ffdd3919 100644 (file)
@@ -5,9 +5,7 @@
        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';
@@ -72,8 +70,8 @@
                        // 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()}`
                                }
                        });
 
index fb6327ed7dc1dda044bbe5a4fc5578ce93c6a2f0..acb128d0ba99b8cbe52bc2653b70f493e42d5afe 100644 (file)
@@ -2,7 +2,7 @@
        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';
 
index 57da065df45d60b41e5be6925a1431329734b02d..7d1791e18a7a2b8c15232b537c21319f35fb7f9f 100644 (file)
@@ -7,8 +7,7 @@
        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';
index d483840eb97c2532c7071a80bb4b2f2fd4fc01fe..798cb704b217070f47039286d0889b81b52054e9 100644 (file)
@@ -1,6 +1,6 @@
 <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 {
index c3bf7bf5c50b2ce3594ff8bf5c5507ffdf41cbe7..c4682f174c4e6d67f2b82c99df7142d33e1d01bc 100644 (file)
@@ -3,12 +3,12 @@
        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>();
index aa197228a3107e8e441fa10053e2e76ba324dcbf..da1fe778bd1e199512c0339051ec7473cf12c6ce 100644 (file)
@@ -1,6 +1,6 @@
 <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 {
index 5572d118f78345b9f9f6bb966423107ebb61c2bf..7392b044605dfd41922b2503cd7961f38404a870 100644 (file)
@@ -1,6 +1,6 @@
 <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';
index 8189ad0e7242730226a1743753b07e1a5369abed..584f2ee834c5384a053b49ae5251501d820c0218 100644 (file)
@@ -1,6 +1,5 @@
 <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>
diff --git a/tools/ui/src/lib/constants/agentic.constants.ts b/tools/ui/src/lib/constants/agentic.constants.ts
new file mode 100644 (file)
index 0000000..e57104e
--- /dev/null
@@ -0,0 +1,67 @@
+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;
diff --git a/tools/ui/src/lib/constants/agentic.ts b/tools/ui/src/lib/constants/agentic.ts
deleted file mode 100644 (file)
index 582572a..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-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;
diff --git a/tools/ui/src/lib/constants/api-endpoints.constants.ts b/tools/ui/src/lib/constants/api-endpoints.constants.ts
new file mode 100644 (file)
index 0000000..74f1c73
--- /dev/null
@@ -0,0 +1,35 @@
+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';
diff --git a/tools/ui/src/lib/constants/api-endpoints.ts b/tools/ui/src/lib/constants/api-endpoints.ts
deleted file mode 100644 (file)
index 74f1c73..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/app.constants.ts b/tools/ui/src/lib/constants/app.constants.ts
new file mode 100644 (file)
index 0000000..c598f48
--- /dev/null
@@ -0,0 +1 @@
+export const APP_NAME = import.meta.env?.VITE_PUBLIC_APP_NAME || 'llama-ui';
diff --git a/tools/ui/src/lib/constants/app.ts b/tools/ui/src/lib/constants/app.ts
deleted file mode 100644 (file)
index c598f48..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export const APP_NAME = import.meta.env?.VITE_PUBLIC_APP_NAME || 'llama-ui';
diff --git a/tools/ui/src/lib/constants/attachment-labels.ts b/tools/ui/src/lib/constants/attachment-labels.ts
deleted file mode 100644 (file)
index be9999c..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/attachment-menu.constants.ts b/tools/ui/src/lib/constants/attachment-menu.constants.ts
new file mode 100644 (file)
index 0000000..62e03be
--- /dev/null
@@ -0,0 +1,93 @@
+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';
diff --git a/tools/ui/src/lib/constants/attachment-menu.ts b/tools/ui/src/lib/constants/attachment-menu.ts
deleted file mode 100644 (file)
index 1cd7f9b..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/auto-scroll.constants.ts b/tools/ui/src/lib/constants/auto-scroll.constants.ts
new file mode 100644 (file)
index 0000000..67c5f93
--- /dev/null
@@ -0,0 +1,22 @@
+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;
diff --git a/tools/ui/src/lib/constants/auto-scroll.ts b/tools/ui/src/lib/constants/auto-scroll.ts
deleted file mode 100644 (file)
index 67c5f93..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-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;
diff --git a/tools/ui/src/lib/constants/binary-detection.constants.ts b/tools/ui/src/lib/constants/binary-detection.constants.ts
new file mode 100644 (file)
index 0000000..69bd4d4
--- /dev/null
@@ -0,0 +1,7 @@
+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
+};
diff --git a/tools/ui/src/lib/constants/binary-detection.ts b/tools/ui/src/lib/constants/binary-detection.ts
deleted file mode 100644 (file)
index 69bd4d4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-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
-};
diff --git a/tools/ui/src/lib/constants/built-in-tools.constants.ts b/tools/ui/src/lib/constants/built-in-tools.constants.ts
new file mode 100644 (file)
index 0000000..679c614
--- /dev/null
@@ -0,0 +1,52 @@
+// 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;
diff --git a/tools/ui/src/lib/constants/built-in-tools.ts b/tools/ui/src/lib/constants/built-in-tools.ts
deleted file mode 100644 (file)
index 5bd24ff..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-// Registry of built-in and frontend (browser) tools whose renderer
-// shows a recognizable icon and friendly label inline in the chat UI.
-//
-// To add a new built-in tool, add an entry to BUILTIN_TOOL_UI. To give a
-// tool a custom title or body renderer, add a dedicated component under
-// ChatMessageToolCall/ and route it in ChatMessageToolCallBlock.svelte
-// (see ChatMessageToolCallBlockGetDatetime and
-// ChatMessageToolCallBlockSearchResults for prior art).
-
-import {
-       Braces,
-       Clock,
-       Eye,
-       FilePen,
-       FilePlus,
-       FileSearch,
-       FileText,
-       Info,
-       SearchCode,
-       Terminal
-} from '@lucide/svelte';
-import { BuiltInTool, ToolSource } from '$lib/enums';
-import type { 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;
-}
diff --git a/tools/ui/src/lib/constants/cache.constants.ts b/tools/ui/src/lib/constants/cache.constants.ts
new file mode 100644 (file)
index 0000000..b60792d
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/cache.ts b/tools/ui/src/lib/constants/cache.ts
deleted file mode 100644 (file)
index 07fe868..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/chat-commands.ts b/tools/ui/src/lib/constants/chat-commands.ts
deleted file mode 100644 (file)
index c34c635..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-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'
-               }
-       ];
-}
diff --git a/tools/ui/src/lib/constants/chat-form.constants.ts b/tools/ui/src/lib/constants/chat-form.constants.ts
new file mode 100644 (file)
index 0000000..6c413ed
--- /dev/null
@@ -0,0 +1,5 @@
+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__';
diff --git a/tools/ui/src/lib/constants/chat-form.ts b/tools/ui/src/lib/constants/chat-form.ts
deleted file mode 100644 (file)
index 6c413ed..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-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__';
diff --git a/tools/ui/src/lib/constants/cli-flags.constants.ts b/tools/ui/src/lib/constants/cli-flags.constants.ts
new file mode 100644 (file)
index 0000000..4fbee8a
--- /dev/null
@@ -0,0 +1,6 @@
+export const CLI_FLAGS = {
+       API_KEY: '--api-key',
+       MCP_PROXY: '--ui-mcp-proxy',
+       SLOTS: '--slots',
+       TOOLS: '--tools'
+} as const;
diff --git a/tools/ui/src/lib/constants/cli-flags.ts b/tools/ui/src/lib/constants/cli-flags.ts
deleted file mode 100644 (file)
index 4fbee8a..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-export const CLI_FLAGS = {
-       API_KEY: '--api-key',
-       MCP_PROXY: '--ui-mcp-proxy',
-       SLOTS: '--slots',
-       TOOLS: '--tools'
-} as const;
diff --git a/tools/ui/src/lib/constants/code-block.constants.ts b/tools/ui/src/lib/constants/code-block.constants.ts
new file mode 100644 (file)
index 0000000..05db575
--- /dev/null
@@ -0,0 +1,50 @@
+// 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;
diff --git a/tools/ui/src/lib/constants/code-blocks.ts b/tools/ui/src/lib/constants/code-blocks.ts
deleted file mode 100644 (file)
index 0f72651..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/code.ts b/tools/ui/src/lib/constants/code.ts
deleted file mode 100644 (file)
index 4b41142..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-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:/;
diff --git a/tools/ui/src/lib/constants/content-detection.constants.ts b/tools/ui/src/lib/constants/content-detection.constants.ts
new file mode 100644 (file)
index 0000000..c5c0581
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/context-gauge-popup.constants.ts b/tools/ui/src/lib/constants/context-gauge-popup.constants.ts
new file mode 100644 (file)
index 0000000..fe2e6a1
--- /dev/null
@@ -0,0 +1,8 @@
+// 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;
diff --git a/tools/ui/src/lib/constants/context-gauge-popup.ts b/tools/ui/src/lib/constants/context-gauge-popup.ts
deleted file mode 100644 (file)
index fe2e6a1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-// 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;
diff --git a/tools/ui/src/lib/constants/context-keys.constants.ts b/tools/ui/src/lib/constants/context-keys.constants.ts
new file mode 100644 (file)
index 0000000..0bd733b
--- /dev/null
@@ -0,0 +1,3 @@
+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';
diff --git a/tools/ui/src/lib/constants/context-keys.ts b/tools/ui/src/lib/constants/context-keys.ts
deleted file mode 100644 (file)
index 0bd733b..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/control-actions.constants.ts b/tools/ui/src/lib/constants/control-actions.constants.ts
new file mode 100644 (file)
index 0000000..c8ebf70
--- /dev/null
@@ -0,0 +1,5 @@
+// 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;
diff --git a/tools/ui/src/lib/constants/control-actions.ts b/tools/ui/src/lib/constants/control-actions.ts
deleted file mode 100644 (file)
index 935ae95..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-// 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];
diff --git a/tools/ui/src/lib/constants/conversation-import.constants.ts b/tools/ui/src/lib/constants/conversation-import.constants.ts
new file mode 100644 (file)
index 0000000..ed50044
--- /dev/null
@@ -0,0 +1,3 @@
+// 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];
diff --git a/tools/ui/src/lib/constants/conversation-import.ts b/tools/ui/src/lib/constants/conversation-import.ts
deleted file mode 100644 (file)
index ed50044..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-// 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];
diff --git a/tools/ui/src/lib/constants/css-classes.constants.ts b/tools/ui/src/lib/constants/css-classes.constants.ts
new file mode 100644 (file)
index 0000000..4e33105
--- /dev/null
@@ -0,0 +1,30 @@
+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';
diff --git a/tools/ui/src/lib/constants/css-classes.ts b/tools/ui/src/lib/constants/css-classes.ts
deleted file mode 100644 (file)
index 4e33105..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/database.constants.ts b/tools/ui/src/lib/constants/database.constants.ts
new file mode 100644 (file)
index 0000000..f2c9610
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/database.ts b/tools/ui/src/lib/constants/database.ts
deleted file mode 100644 (file)
index 95e698f..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/diagram-blocks.constants.ts b/tools/ui/src/lib/constants/diagram-blocks.constants.ts
new file mode 100644 (file)
index 0000000..caeb6b5
--- /dev/null
@@ -0,0 +1,9 @@
+// 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';
diff --git a/tools/ui/src/lib/constants/diagram-blocks.ts b/tools/ui/src/lib/constants/diagram-blocks.ts
deleted file mode 100644 (file)
index caeb6b5..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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';
diff --git a/tools/ui/src/lib/constants/error.constants.ts b/tools/ui/src/lib/constants/error.constants.ts
new file mode 100644 (file)
index 0000000..17527fc
--- /dev/null
@@ -0,0 +1,23 @@
+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
+};
diff --git a/tools/ui/src/lib/constants/error.ts b/tools/ui/src/lib/constants/error.ts
deleted file mode 100644 (file)
index 17527fc..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-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
-};
diff --git a/tools/ui/src/lib/constants/floating-ui-constraints.ts b/tools/ui/src/lib/constants/floating-ui-constraints.ts
deleted file mode 100644 (file)
index 003fc77..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export const VIEWPORT_GUTTER = 8;
-export const MENU_OFFSET = 6;
diff --git a/tools/ui/src/lib/constants/formatters.constants.ts b/tools/ui/src/lib/constants/formatters.constants.ts
new file mode 100644 (file)
index 0000000..d6d1b88
--- /dev/null
@@ -0,0 +1,8 @@
+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';
diff --git a/tools/ui/src/lib/constants/formatters.ts b/tools/ui/src/lib/constants/formatters.ts
deleted file mode 100644 (file)
index d6d1b88..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/headers.constants.ts b/tools/ui/src/lib/constants/headers.constants.ts
new file mode 100644 (file)
index 0000000..f40df05
--- /dev/null
@@ -0,0 +1,35 @@
+/** 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';
diff --git a/tools/ui/src/lib/constants/icons.constants.ts b/tools/ui/src/lib/constants/icons.constants.ts
new file mode 100644 (file)
index 0000000..5563740
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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>`;
diff --git a/tools/ui/src/lib/constants/icons.ts b/tools/ui/src/lib/constants/icons.ts
deleted file mode 100644 (file)
index 5563740..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * 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>`;
diff --git a/tools/ui/src/lib/constants/image-size.ts b/tools/ui/src/lib/constants/image-size.ts
deleted file mode 100644 (file)
index 8a7f921..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export const MEGAPIXELS_TO_PIXELS = 1_000_000;
-
-export const HEIC_JPEG_QUALITY = 0.85;
diff --git a/tools/ui/src/lib/constants/image.constants.ts b/tools/ui/src/lib/constants/image.constants.ts
new file mode 100644 (file)
index 0000000..53a90ea
--- /dev/null
@@ -0,0 +1,32 @@
+/** 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;
index 357a33a6241e98868157a72ac3ab126a05125e99..17e5f4d00ab501f0ee4e3c21efe1451c3df80579 100644 (file)
@@ -1,67 +1,61 @@
 // 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';
diff --git a/tools/ui/src/lib/constants/jpeg-exif.ts b/tools/ui/src/lib/constants/jpeg-exif.ts
deleted file mode 100644 (file)
index 5b2591b..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/key-value-pairs.constants.ts b/tools/ui/src/lib/constants/key-value-pairs.constants.ts
new file mode 100644 (file)
index 0000000..48dadbe
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/key-value-pairs.ts b/tools/ui/src/lib/constants/key-value-pairs.ts
deleted file mode 100644 (file)
index 48dadbe..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/latex-protection.constants.ts b/tools/ui/src/lib/constants/latex-protection.constants.ts
new file mode 100644 (file)
index 0000000..c42aec4
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/latex-protection.ts b/tools/ui/src/lib/constants/latex-protection.ts
deleted file mode 100644 (file)
index c42aec4..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/literal-html.constants.ts b/tools/ui/src/lib/constants/literal-html.constants.ts
new file mode 100644 (file)
index 0000000..8efa6b5
--- /dev/null
@@ -0,0 +1,10 @@
+export const PHRASE_PARENTS = new Set([
+       'paragraph',
+       'heading',
+       'emphasis',
+       'strong',
+       'delete',
+       'link',
+       'linkReference',
+       'tableCell'
+]);
diff --git a/tools/ui/src/lib/constants/literal-html.ts b/tools/ui/src/lib/constants/literal-html.ts
deleted file mode 100644 (file)
index ed1b0cf..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-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);
diff --git a/tools/ui/src/lib/constants/markdown.constants.ts b/tools/ui/src/lib/constants/markdown.constants.ts
new file mode 100644 (file)
index 0000000..c8289c3
--- /dev/null
@@ -0,0 +1,17 @@
+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;
diff --git a/tools/ui/src/lib/constants/markdown.ts b/tools/ui/src/lib/constants/markdown.ts
deleted file mode 100644 (file)
index 1cace78..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/max-bundle-size.constants.ts b/tools/ui/src/lib/constants/max-bundle-size.constants.ts
new file mode 100644 (file)
index 0000000..e04348f
--- /dev/null
@@ -0,0 +1 @@
+export const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
diff --git a/tools/ui/src/lib/constants/max-bundle-size.ts b/tools/ui/src/lib/constants/max-bundle-size.ts
deleted file mode 100644 (file)
index e04348f..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
diff --git a/tools/ui/src/lib/constants/mcp-form.constants.ts b/tools/ui/src/lib/constants/mcp-form.constants.ts
new file mode 100644 (file)
index 0000000..7a1ccff
--- /dev/null
@@ -0,0 +1,2 @@
+export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
+export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
diff --git a/tools/ui/src/lib/constants/mcp-form.ts b/tools/ui/src/lib/constants/mcp-form.ts
deleted file mode 100644 (file)
index 7a1ccff..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
-export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
diff --git a/tools/ui/src/lib/constants/mcp-resource.constants.ts b/tools/ui/src/lib/constants/mcp-resource.constants.ts
new file mode 100644 (file)
index 0000000..c2639da
--- /dev/null
@@ -0,0 +1,73 @@
+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;
diff --git a/tools/ui/src/lib/constants/mcp-resource.ts b/tools/ui/src/lib/constants/mcp-resource.ts
deleted file mode 100644 (file)
index c2639da..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-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;
diff --git a/tools/ui/src/lib/constants/mcp.constants.ts b/tools/ui/src/lib/constants/mcp.constants.ts
new file mode 100644 (file)
index 0000000..11013d2
--- /dev/null
@@ -0,0 +1,81 @@
+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
+};
diff --git a/tools/ui/src/lib/constants/mcp.ts b/tools/ui/src/lib/constants/mcp.ts
deleted file mode 100644 (file)
index 854cbf9..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-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?';
diff --git a/tools/ui/src/lib/constants/mention-badge.constants.ts b/tools/ui/src/lib/constants/mention-badge.constants.ts
new file mode 100644 (file)
index 0000000..a9ef963
--- /dev/null
@@ -0,0 +1,45 @@
+/**
+ * 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'
+];
diff --git a/tools/ui/src/lib/constants/mention-badge.ts b/tools/ui/src/lib/constants/mention-badge.ts
deleted file mode 100644 (file)
index a9ef963..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * 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'
-];
diff --git a/tools/ui/src/lib/constants/mermaid-blocks.constants.ts b/tools/ui/src/lib/constants/mermaid-blocks.constants.ts
new file mode 100644 (file)
index 0000000..cd9467f
--- /dev/null
@@ -0,0 +1,9 @@
+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';
diff --git a/tools/ui/src/lib/constants/mermaid-blocks.ts b/tools/ui/src/lib/constants/mermaid-blocks.ts
deleted file mode 100644 (file)
index cd9467f..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-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';
diff --git a/tools/ui/src/lib/constants/message-export.constants.ts b/tools/ui/src/lib/constants/message-export.constants.ts
new file mode 100644 (file)
index 0000000..f6c576d
--- /dev/null
@@ -0,0 +1,24 @@
+// 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;
diff --git a/tools/ui/src/lib/constants/message-export.ts b/tools/ui/src/lib/constants/message-export.ts
deleted file mode 100644 (file)
index fc4dbe2..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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 = '-';
diff --git a/tools/ui/src/lib/constants/model-id.constants.ts b/tools/ui/src/lib/constants/model-id.constants.ts
new file mode 100644 (file)
index 0000000..081a13e
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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
+};
diff --git a/tools/ui/src/lib/constants/model-id.ts b/tools/ui/src/lib/constants/model-id.ts
deleted file mode 100644 (file)
index 4108a21..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/** 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;
diff --git a/tools/ui/src/lib/constants/model-loading.constants.ts b/tools/ui/src/lib/constants/model-loading.constants.ts
new file mode 100644 (file)
index 0000000..0d0ca32
--- /dev/null
@@ -0,0 +1,14 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/model-loading.ts b/tools/ui/src/lib/constants/model-loading.ts
deleted file mode 100644 (file)
index 0d0ca32..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/path-display.constants.ts b/tools/ui/src/lib/constants/path-display.constants.ts
new file mode 100644 (file)
index 0000000..fd10017
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * 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]*?)\)/;
diff --git a/tools/ui/src/lib/constants/path-display.ts b/tools/ui/src/lib/constants/path-display.ts
deleted file mode 100644 (file)
index fd10017..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * 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]*?)\)/;
diff --git a/tools/ui/src/lib/constants/precision.constants.ts b/tools/ui/src/lib/constants/precision.constants.ts
new file mode 100644 (file)
index 0000000..8df5c4f
--- /dev/null
@@ -0,0 +1,2 @@
+export const PRECISION_MULTIPLIER = 1000000;
+export const PRECISION_DECIMAL_PLACES = 6;
diff --git a/tools/ui/src/lib/constants/precision.ts b/tools/ui/src/lib/constants/precision.ts
deleted file mode 100644 (file)
index 8df5c4f..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export const PRECISION_MULTIPLIER = 1000000;
-export const PRECISION_DECIMAL_PLACES = 6;
diff --git a/tools/ui/src/lib/constants/processing-info.ts b/tools/ui/src/lib/constants/processing-info.ts
deleted file mode 100644 (file)
index 2c3f7dc..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-export const PROCESSING_INFO_TIMEOUT = 2000;
-
-/**
- * Statistics units labels
- */
-export const STATS_UNITS = {
-       TOKENS_PER_SECOND: 't/s'
-} as const;
diff --git a/tools/ui/src/lib/constants/pwa.constants.ts b/tools/ui/src/lib/constants/pwa.constants.ts
new file mode 100644 (file)
index 0000000..e807f4a
--- /dev/null
@@ -0,0 +1,361 @@
+/**
+ * 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
+                       }
+               ]
+       }
+};
diff --git a/tools/ui/src/lib/constants/pwa.ts b/tools/ui/src/lib/constants/pwa.ts
deleted file mode 100644 (file)
index 4da37c0..0000000
+++ /dev/null
@@ -1,361 +0,0 @@
-/**
- * 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
-                       }
-               ]
-       }
-};
diff --git a/tools/ui/src/lib/constants/reasoning-effort-tokens.ts b/tools/ui/src/lib/constants/reasoning-effort-tokens.ts
deleted file mode 100644 (file)
index a0c244e..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-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
-};
diff --git a/tools/ui/src/lib/constants/reasoning-effort.constants.ts b/tools/ui/src/lib/constants/reasoning-effort.constants.ts
new file mode 100644 (file)
index 0000000..e8ec5f0
--- /dev/null
@@ -0,0 +1,35 @@
+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
+};
diff --git a/tools/ui/src/lib/constants/reasoning-effort.ts b/tools/ui/src/lib/constants/reasoning-effort.ts
deleted file mode 100644 (file)
index 4cbb738..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-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 }
-];
diff --git a/tools/ui/src/lib/constants/recommended-mcp-servers.constants.ts b/tools/ui/src/lib/constants/recommended-mcp-servers.constants.ts
new file mode 100644 (file)
index 0000000..6a550ee
--- /dev/null
@@ -0,0 +1,38 @@
+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'
+       }
+];
diff --git a/tools/ui/src/lib/constants/recommended-mcp-servers.ts b/tools/ui/src/lib/constants/recommended-mcp-servers.ts
deleted file mode 100644 (file)
index 6a550ee..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-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'
-       }
-];
diff --git a/tools/ui/src/lib/constants/routes.constants.ts b/tools/ui/src/lib/constants/routes.constants.ts
new file mode 100644 (file)
index 0000000..84b0f53
--- /dev/null
@@ -0,0 +1,38 @@
+/** 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;
diff --git a/tools/ui/src/lib/constants/routes.ts b/tools/ui/src/lib/constants/routes.ts
deleted file mode 100644 (file)
index 84b0f53..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/** 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;
diff --git a/tools/ui/src/lib/constants/sandbox.constants.ts b/tools/ui/src/lib/constants/sandbox.constants.ts
new file mode 100644 (file)
index 0000000..9846e47
--- /dev/null
@@ -0,0 +1,13 @@
+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]';
diff --git a/tools/ui/src/lib/constants/sandbox.ts b/tools/ui/src/lib/constants/sandbox.ts
deleted file mode 100644 (file)
index 11e5fcf..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-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);
diff --git a/tools/ui/src/lib/constants/settings-keys.constants.ts b/tools/ui/src/lib/constants/settings-keys.constants.ts
new file mode 100644 (file)
index 0000000..b53d110
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts
deleted file mode 100644 (file)
index b53d110..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/settings-registry.constants.ts b/tools/ui/src/lib/constants/settings-registry.constants.ts
new file mode 100644 (file)
index 0000000..e444599
--- /dev/null
@@ -0,0 +1,752 @@
+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;
diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts
deleted file mode 100644 (file)
index ada029a..0000000
+++ /dev/null
@@ -1,756 +0,0 @@
-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;
diff --git a/tools/ui/src/lib/constants/special-characters.constants.ts b/tools/ui/src/lib/constants/special-characters.constants.ts
new file mode 100644 (file)
index 0000000..aaeebca
--- /dev/null
@@ -0,0 +1,16 @@
+// 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/;
diff --git a/tools/ui/src/lib/constants/sse.ts b/tools/ui/src/lib/constants/sse.ts
deleted file mode 100644 (file)
index 0eb4b6e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * 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]';
diff --git a/tools/ui/src/lib/constants/storage.constants.ts b/tools/ui/src/lib/constants/storage.constants.ts
new file mode 100644 (file)
index 0000000..5d9acaa
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * 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
+};
diff --git a/tools/ui/src/lib/constants/storage.ts b/tools/ui/src/lib/constants/storage.ts
deleted file mode 100644 (file)
index 5d9acaa..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * 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
-};
diff --git a/tools/ui/src/lib/constants/stream.constants.ts b/tools/ui/src/lib/constants/stream.constants.ts
new file mode 100644 (file)
index 0000000..64f6724
--- /dev/null
@@ -0,0 +1,24 @@
+// 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]';
diff --git a/tools/ui/src/lib/constants/stream.ts b/tools/ui/src/lib/constants/stream.ts
deleted file mode 100644 (file)
index 67951ee..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-// 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;
diff --git a/tools/ui/src/lib/constants/supported-file-types.constants.ts b/tools/ui/src/lib/constants/supported-file-types.constants.ts
new file mode 100644 (file)
index 0000000..a6bcefa
--- /dev/null
@@ -0,0 +1,234 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/supported-file-types.ts b/tools/ui/src/lib/constants/supported-file-types.ts
deleted file mode 100644 (file)
index a6bcefa..0000000
+++ /dev/null
@@ -1,234 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/svg-blocks.constants.ts b/tools/ui/src/lib/constants/svg-blocks.constants.ts
new file mode 100644 (file)
index 0000000..705800c
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * 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'
+};
diff --git a/tools/ui/src/lib/constants/svg-blocks.ts b/tools/ui/src/lib/constants/svg-blocks.ts
deleted file mode 100644 (file)
index 359e069..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-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}';
diff --git a/tools/ui/src/lib/constants/table-html-restorer.constants.ts b/tools/ui/src/lib/constants/table-html-restorer.constants.ts
new file mode 100644 (file)
index 0000000..e5d5b12
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/table-html-restorer.ts b/tools/ui/src/lib/constants/table-html-restorer.ts
deleted file mode 100644 (file)
index e5d5b12..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * 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;
diff --git a/tools/ui/src/lib/constants/title-generation.constants.ts b/tools/ui/src/lib/constants/title-generation.constants.ts
new file mode 100644 (file)
index 0000000..0496daa
--- /dev/null
@@ -0,0 +1,9 @@
+/* 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;
diff --git a/tools/ui/src/lib/constants/title-generation.ts b/tools/ui/src/lib/constants/title-generation.ts
deleted file mode 100644 (file)
index 0496daa..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-/* 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;
diff --git a/tools/ui/src/lib/constants/tools.ts b/tools/ui/src/lib/constants/tools.ts
deleted file mode 100644 (file)
index a467cda..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-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;
diff --git a/tools/ui/src/lib/constants/tooltip-config.ts b/tools/ui/src/lib/constants/tooltip-config.ts
deleted file mode 100644 (file)
index ad76ab3..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export const TOOLTIP_DELAY_DURATION = 500;
diff --git a/tools/ui/src/lib/constants/ui.constants.ts b/tools/ui/src/lib/constants/ui.constants.ts
new file mode 100644 (file)
index 0000000..3374b6e
--- /dev/null
@@ -0,0 +1,62 @@
+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'
+       }
+];
diff --git a/tools/ui/src/lib/constants/ui.ts b/tools/ui/src/lib/constants/ui.ts
deleted file mode 100644 (file)
index 9489a3f..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-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'
-       }
-];
diff --git a/tools/ui/src/lib/constants/uri-template.constants.ts b/tools/ui/src/lib/constants/uri-template.constants.ts
new file mode 100644 (file)
index 0000000..d4b6801
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * 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/;
diff --git a/tools/ui/src/lib/constants/uri-template.ts b/tools/ui/src/lib/constants/uri-template.ts
deleted file mode 100644 (file)
index 3ae2555..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * 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/;
diff --git a/tools/ui/src/lib/constants/url.constants.ts b/tools/ui/src/lib/constants/url.constants.ts
new file mode 100644 (file)
index 0000000..214c8af
--- /dev/null
@@ -0,0 +1,186 @@
+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 = /\/+$/;
diff --git a/tools/ui/src/lib/constants/url.ts b/tools/ui/src/lib/constants/url.ts
deleted file mode 100644 (file)
index 214c8af..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-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 = /\/+$/;
diff --git a/tools/ui/src/lib/constants/viewport.ts b/tools/ui/src/lib/constants/viewport.ts
deleted file mode 100644 (file)
index 26e202c..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export const DEFAULT_MOBILE_BREAKPOINT = 768;
diff --git a/tools/ui/src/lib/constants/working-directory.constants.ts b/tools/ui/src/lib/constants/working-directory.constants.ts
new file mode 100644 (file)
index 0000000..cb63e92
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * 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;
diff --git a/tools/ui/src/lib/constants/working-directory.ts b/tools/ui/src/lib/constants/working-directory.ts
deleted file mode 100644 (file)
index 20d1d15..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * 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;
index 1c3258cb19da8c918e993256e9aa42c3b0028aa0..c6fc2b82ad981439facf72f3b31a75b0db4cc698 100644 (file)
@@ -47,6 +47,16 @@ export enum AttachmentAction {
        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.
  */
index ebf8b6dbe5b00920b5cb07a71eced331d2cb25ca..217ecc2fe197547a556f9ed99e7894452283426a 100644 (file)
@@ -1,4 +1,5 @@
 export {
+       AttachmentLabel,
        AttachmentType,
        AttachmentMenuItemId,
        AttachmentItemEnabledWhen,
@@ -43,13 +44,13 @@ export {
        FileExtensionAudio,
        FileExtensionPdf,
        FileExtensionText,
-       MimeTypePrefix,
-       MimeTypeIncludes,
-       UriPattern,
        MimeTypeApplication,
        MimeTypeAudio,
        MimeTypeVideo,
        MimeTypeImage,
+       MimeTypePrefix,
+       MimeTypeIncludes,
+       UriPattern,
        MimeTypeText,
        SpecialFileType
 } from './files.enums';
index 712189746b6d37b7d4a8dc0e506aa61af8061236..986cc46c7215cf08583817a56897b05a932fb43e 100644 (file)
@@ -1,6 +1,7 @@
-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,
index f99b273e8445e89729b557354fdffc7a07639587..ce394c383d7245db8e258bce6bdfab0f21509052 100644 (file)
@@ -1,5 +1,5 @@
 import { goto } from '$app/navigation';
-import { ROUTES } from '$lib/constants/routes';
+import { ROUTES } from '$lib/constants';
 import { KeyboardKey } from '$lib/enums';
 
 interface KeyboardShortcutsCallbacks {
index 1a02710677b0b093388bb760068d151610c41aa8..2bad85aacd7cece491c615a0c80cdcdca0308f91 100644 (file)
@@ -1,6 +1,5 @@
 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';
 
index 6c47b160fe0be60d8b1f38b0796175bb1ee6262f..399a08528d79ce0d1a2bf87a3612594f2fcffe99 100644 (file)
@@ -1,5 +1,4 @@
-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';
index e326d0c8abe3773d37c0c7ca4c9cd226546ed2dd..efa7bcbc1ec5c9a618956ce906fd63f087f8f1d1 100644 (file)
@@ -1,6 +1,6 @@
 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 {
index 540d742393f9562ae45357afd3f74fcad68402be..1dde2e18fefcfbd33a1de350ea35ec73b64caed2 100644 (file)
@@ -5,10 +5,8 @@ import {
        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,
@@ -19,6 +17,7 @@ import {
        STREAM_VISIBILITY_KICK_MS
 } from '$lib/constants';
 import {
+       AttachmentLabel,
        AttachmentType,
        ContentPartType,
        MessageRole,
@@ -341,7 +340,7 @@ export class ChatService {
                        // 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);
@@ -1291,7 +1290,7 @@ export class ChatService {
 
                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
                        });
                }
@@ -1304,7 +1303,11 @@ export class ChatService {
 
                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
                        });
                }
@@ -1382,7 +1385,7 @@ export class ChatService {
                                }
                        } 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
                                });
                        }
@@ -1396,7 +1399,7 @@ export class ChatService {
                for (const mcpPrompt of mcpPrompts) {
                        contentParts.push({
                                text: formatAttachmentText(
-                                       ATTACHMENT_LABEL_MCP_PROMPT,
+                                       AttachmentLabel.MCP_PROMPT,
                                        mcpPrompt.name,
                                        mcpPrompt.content,
                                        mcpPrompt.serverName
@@ -1413,7 +1416,7 @@ export class ChatService {
                for (const mcpResource of mcpResources) {
                        contentParts.push({
                                text: formatAttachmentText(
-                                       ATTACHMENT_LABEL_MCP_RESOURCE,
+                                       AttachmentLabel.MCP_RESOURCE,
                                        mcpResource.name,
                                        mcpResource.content,
                                        mcpResource.serverName
index 8704b1bd6ce52624264ce88f75701c9820faaeb7..328d3b4828923297ad550fe91c469e175a7b7e7c 100644 (file)
@@ -276,7 +276,7 @@ export { MCPService } from './mcp.service';
  * - **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';
index fbac5b7b58e67b982f9f619998d9ebc2e497ae06..43f905c33acaac1105eccbae1926e434f5de9814 100644 (file)
@@ -13,12 +13,12 @@ import type {
        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,
@@ -120,7 +120,7 @@ export class MCPService {
                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)
@@ -141,8 +141,8 @@ export class MCPService {
        ) {
                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);
@@ -361,7 +361,7 @@ export class MCPService {
                                                        {
                                                                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
@@ -751,7 +751,7 @@ export class MCPService {
                                                        headers: sanitizeHeaders(
                                                                serverConfig.headers,
                                                                Object.keys(serverConfig.headers ?? {}),
-                                                               MCP_PARTIAL_REDACT_HEADERS
+                                                               HEADERS.PARTIAL_REDACT
                                                        ),
                                                        serverName,
                                                        transportType,
index ffe4bd846799d49c5d2c9d6dab3e426f4cc97e95..3dcbbed8278194c44ec478eb5fe4bff91be85d98 100644 (file)
@@ -22,12 +22,13 @@ import {
        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';
 
index 7399a3cc6e433ec2f60b744ab850651df49a4c9e..9b37927bb66b9438a6507d2abf78ebc76c20c8b3 100644 (file)
@@ -1,16 +1,4 @@
-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';
@@ -142,13 +130,13 @@ export class ModelsService {
                };
                // 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 {
@@ -156,11 +144,11 @@ export class ModelsService {
                }
 
                // 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 {
@@ -170,16 +158,16 @@ export class ModelsService {
                // 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
@@ -188,8 +176,8 @@ export class ModelsService {
                        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 {
@@ -200,33 +188,33 @@ export class ModelsService {
                }
 
                // 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());
                        });
                }
 
index 6fa172eec23428de10c9adc5ba33772630f512a3..59de4cb6fe23a91fc80bb4cff813b1530a397a16 100644 (file)
@@ -1,4 +1,4 @@
-import { ROUTES } from '$lib/constants/routes';
+import { ROUTES } from '$lib/constants';
 
 export class RouterService {
        static chat(id: string): string {
index 9cb5ecb46dbf705b7000cce5cc84ea4fa1a47fb1..f100b74f9324e178b451a8c161474c20869b2908 100644 (file)
@@ -1,5 +1,5 @@
 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';
@@ -31,7 +31,7 @@ export class ToolsService {
        ): 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
                });
@@ -64,9 +64,9 @@ export class ToolsService {
        ): 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 }),
@@ -99,7 +99,7 @@ export class ToolsService {
        ): 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 }),
index a84fa34dc276dc68658772fc985a64346459aeb8..cba3b586fa50ad8b6393402b8a79094e2c0e4aa9 100644 (file)
@@ -27,7 +27,8 @@ import {
        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 {
@@ -1122,7 +1123,7 @@ class AgenticStore {
                        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}]`;
index edbb8e621b7cca3a15f3717dcf7d8cf3673afab2..3965c4cbb57adc7ab9d28199998f0b8e175ae704 100644 (file)
  * @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,
@@ -234,7 +234,7 @@ class ChatStore {
                        // 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) {
@@ -414,7 +414,7 @@ class ChatStore {
 
                // 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);
@@ -808,7 +808,7 @@ class ChatStore {
                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'
                        });
 
@@ -829,7 +829,7 @@ class ChatStore {
                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);
@@ -930,8 +930,8 @@ class ChatStore {
 
                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++;
index b1fcac4bedd13cfead241bdb0bed8a17df2e5425..54613a27012363bd552d0f3cdda8cebe3c101e15 100644 (file)
 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,
@@ -1002,18 +992,18 @@ class ConversationsStore {
        ): 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}`;
        }
@@ -1028,7 +1018,7 @@ class ConversationsStore {
        serializeSessionToJsonl(data: ExportedConversation): string {
                const { conv, messages } = data;
                const sessionLine = JSON.stringify({
-                       harness: SESSION_HARNESS,
+                       harness: EXPORT_CONV.HARNESS,
                        type: SessionRecordType.SESSION,
                        ...conv
                });
@@ -1209,7 +1199,7 @@ class ConversationsStore {
                        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 });
 
index 6f0f8d7c80c5a9ed7720a9fddaec468ff1c7d218..1002c476e53b2641da6be7b8afa9a39b66e911c8 100644 (file)
@@ -13,8 +13,7 @@
 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';
@@ -251,7 +250,7 @@ class MCPResourceStore {
         */
        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;
 
@@ -280,7 +279,7 @@ class MCPResourceStore {
                // 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);
 
index 4582caf5ce186d2a23e35922679eb1eaffcb7a15..0b669f88b3943a8e8eed8a35ac787681605db491 100644 (file)
@@ -26,14 +26,11 @@ import type { ListChangedHandlers } from '@modelcontextprotocol/sdk/types.js';
 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 {
@@ -917,7 +914,7 @@ class MCPStore {
                }
 
                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;
@@ -936,10 +933,10 @@ class MCPStore {
                                                        () =>
                                                                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
                                                )
                                        );
 
@@ -980,7 +977,7 @@ class MCPStore {
                                        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 {
@@ -1737,7 +1734,7 @@ class MCPStore {
                                // 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) {
index 150fb3800f64b7368e082fd04f3d282f68c89863..b2be636c51c198b0230f7e0fd3fcfe97af8e7a0e 100644 (file)
@@ -2,8 +2,7 @@ import { base } from '$app/paths';
 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
@@ -84,8 +83,8 @@ class ModelsStore {
         * 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());
 
index 3984dd2bc8d846ce3a1c60703369d23218185bb3..ff276d95bae089fc6c737f3415f8dddc37928a49 100644 (file)
@@ -1,6 +1,5 @@
 import {
        buildReadMediaToolDefinition,
-       buildSandboxToolDefinition,
        DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY,
        HOME_TILDE,
        TOOL_GROUP_LABELS,
@@ -19,6 +18,7 @@ import { mcpStore } from '$lib/stores/mcp.svelte';
 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 */
index dac241a0124791607572a3fac03dc1099037558e..fee8db4acb99fdbe11bb15785e51963c8e19ba9b 100644 (file)
@@ -1,5 +1,5 @@
 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({
index 2da043905790f6b0e3ff13e5b5c2e5b246aeb025..cb93e7795150023d3dcbd071b9b4a5e7b3ce64be 100644 (file)
@@ -1,6 +1,39 @@
 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;
@@ -182,6 +215,18 @@ export interface FileMentionEntry {
  * 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;
index dbe0fa823dfb074fb2f556102277321fe94a9e7a..a7e755040cfabd03a10b15ca4bd8e7f85dc0f5db 100644 (file)
@@ -40,6 +40,7 @@ export type {
 
 // Chat types
 export type {
+       AttachmentMenuItem,
        ChatUploadedFile,
        ChatAttachmentDisplayItem,
        ChatMessageSiblingInfo,
@@ -55,7 +56,9 @@ export type {
        AttachmentDisplayItemsOptions,
        FileProcessingResult,
        FileMentionEntry,
-       ChatFormCommand
+       ChatFormCommand,
+       ChatCommandsOptions,
+       ControlAction
 } from './chat.d';
 
 // Database types
@@ -174,8 +177,11 @@ export type {
        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';
diff --git a/tools/ui/src/lib/types/navigation.d.ts b/tools/ui/src/lib/types/navigation.d.ts
new file mode 100644 (file)
index 0000000..357e060
--- /dev/null
@@ -0,0 +1,14 @@
+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[];
+}
index 95b6256a515f6190376768a71e2e6619a2305e74..39e6fc579990ac50e3da586168c3ae39471b938d 100644 (file)
@@ -1,5 +1,15 @@
 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;
index b2dd2cd9ed10f7d82f67d5f86a70d76b46513ab2..2be4dc71476794e70bc4d31796896089cc5af6b9 100644 (file)
@@ -1,17 +1,9 @@
 import {
        ATTACHMENT_SAVED_REGEX,
-       MARKDOWN_ATX_HEADING_REGEX,
-       MARKDOWN_BLOCKQUOTE_REGEX,
-       MARKDOWN_BOLD_REGEX,
-       MARKDOWN_CODE_FENCE_REGEX,
-       MARKDOWN_LINK_REGEX,
-       MARKDOWN_LIST_BULLET_REGEX,
-       MARKDOWN_LIST_NUMBERED_REGEX,
-       MARKDOWN_TABLE_SEPARATOR_REGEX,
+       MARKDOWN,
        NEWLINE,
        REASONING_TAGS,
-       SEARCH_SUMMARY_SEPARATOR,
-       SEARCH_SUMMARY_TOTAL_REGEX,
+       SEARCH_SUMMARY,
        TOOL_RESULT_JSON_OPEN_REGEX
 } from '$lib/constants';
 import {
@@ -283,11 +275,11 @@ export function splitSearchSummaryList(
        text: string,
        captureTotal: (n: number) => void
 ): { lines: string[] } {
-       const separatorIndex = text.indexOf(SEARCH_SUMMARY_SEPARATOR);
+       const separatorIndex = text.indexOf(SEARCH_SUMMARY.SEPARATOR);
        const matchesText = separatorIndex === -1 ? text : text.slice(0, separatorIndex);
        const summaryText =
-               separatorIndex === -1 ? '' : text.slice(separatorIndex + SEARCH_SUMMARY_SEPARATOR.length);
-       const totalMatch = summaryText.match(SEARCH_SUMMARY_TOTAL_REGEX);
+               separatorIndex === -1 ? '' : text.slice(separatorIndex + SEARCH_SUMMARY.SEPARATOR.length);
+       const totalMatch = summaryText.match(SEARCH_SUMMARY.TOTAL_REGEX);
 
        if (totalMatch) {
                captureTotal(parseInt(totalMatch[1], 10));
@@ -412,31 +404,31 @@ export function classifyToolResult(content: string | undefined): ToolResultKind
  */
 function looksLikeMarkdown(content: string): boolean {
        // Code fences are unambiguous - triple backticks or tildes at line start.
-       if (MARKDOWN_CODE_FENCE_REGEX.test(content)) return true;
+       if (MARKDOWN.CODE_FENCE_REGEX.test(content)) return true;
 
        const lines = content.split(NEWLINE);
 
        for (const line of lines) {
-               if (MARKDOWN_ATX_HEADING_REGEX.test(line)) return true;
+               if (MARKDOWN.ATX_HEADING_REGEX.test(line)) return true;
 
-               if (MARKDOWN_BLOCKQUOTE_REGEX.test(line)) return true;
+               if (MARKDOWN.BLOCKQUOTE_REGEX.test(line)) return true;
 
-               if (MARKDOWN_LIST_BULLET_REGEX.test(line)) return true;
+               if (MARKDOWN.LIST_BULLET_REGEX.test(line)) return true;
 
-               if (MARKDOWN_LIST_NUMBERED_REGEX.test(line)) return true;
+               if (MARKDOWN.LIST_NUMBERED_REGEX.test(line)) return true;
        }
 
        // Inline structural markers anywhere in the body.
-       if (MARKDOWN_LINK_REGEX.test(content)) return true;
+       if (MARKDOWN.LINK_REGEX.test(content)) return true;
 
-       if (MARKDOWN_BOLD_REGEX.test(content)) return true;
+       if (MARKDOWN.BOLD_REGEX.test(content)) return true;
 
        // Tables: a pipe-bearing header line followed by a separator row.
        if (lines.length >= 2) {
                const head = lines[0];
                const sep = lines[1];
 
-               if (head.includes('|') && MARKDOWN_TABLE_SEPARATOR_REGEX.test(sep)) return true;
+               if (head.includes('|') && MARKDOWN.TABLE_SEPARATOR_REGEX.test(sep)) return true;
        }
 
        return false;
index 7c3b9a9d232530c8ed14f9aa204913566e324bca..65e1129def1263b8a9c4561a598917a08db4dd51 100644 (file)
@@ -1,6 +1,6 @@
 import { getAuthHeaders, getJsonHeaders } from './api-headers';
 import { base } from '$app/paths';
-import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants/error';
+import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants';
 import { UrlProtocol } from '$lib/enums';
 
 /**
index 16823de011e7188696b32f8a7652814e46f0b227..0038e6f3cb6f46a18fae381082aad97e57b3113e 100644 (file)
@@ -1,11 +1,5 @@
 import { redactValue } from './redact';
-import {
-       AUTHORIZATION_HEADER,
-       BEARER_PREFIX,
-       CONTENT_TYPE_HEADER,
-       CORS_PROXY_HEADER_PREFIX,
-       REDACTED_HEADERS
-} from '$lib/constants';
+import { CORS_PROXY, HEADERS } from '$lib/constants';
 import { MimeTypeApplication } from '$lib/enums';
 import { config } from '$lib/stores/settings.svelte';
 
@@ -17,7 +11,7 @@ export function getAuthHeaders(): Record<string, string> {
        const currentConfig = config();
        const apiKey = currentConfig.apiKey?.toString().trim();
 
-       return apiKey ? { [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey}` } : {};
+       return apiKey ? { [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey}` } : {};
 }
 
 /**
@@ -25,14 +19,14 @@ export function getAuthHeaders(): Record<string, string> {
  */
 export function getJsonHeaders(): Record<string, string> {
        return {
-               [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON,
+               [HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON,
                ...getAuthHeaders()
        };
 }
 
 /**
  * Sanitize HTTP headers by redacting sensitive values.
- * Known sensitive headers (from REDACTED_HEADERS) and any extra headers
+ * Known sensitive headers (from HEADERS.REDACTED) and any extra headers
  * specified by the caller are fully redacted. Headers listed in
  * `partialRedactHeaders` are partially redacted, showing only the
  * specified number of trailing characters.
@@ -59,8 +53,8 @@ export function sanitizeHeaders(
 
        for (const [key, value] of normalized.entries()) {
                const normalizedKey = key.toLowerCase();
-               const unproxiedKey = normalizedKey.startsWith(CORS_PROXY_HEADER_PREFIX)
-                       ? normalizedKey.slice(CORS_PROXY_HEADER_PREFIX.length)
+               const unproxiedKey = normalizedKey.startsWith(CORS_PROXY.HEADER_PREFIX)
+                       ? normalizedKey.slice(CORS_PROXY.HEADER_PREFIX.length)
                        : normalizedKey;
                const partialChars =
                        partialRedactHeaders?.get(normalizedKey) ?? partialRedactHeaders?.get(unproxiedKey);
@@ -68,8 +62,8 @@ export function sanitizeHeaders(
                if (partialChars !== undefined) {
                        sanitized[key] = redactValue(value, partialChars);
                } else if (
-                       REDACTED_HEADERS.has(normalizedKey) ||
-                       REDACTED_HEADERS.has(unproxiedKey) ||
+                       HEADERS.REDACTED.has(normalizedKey) ||
+                       HEADERS.REDACTED.has(unproxiedKey) ||
                        redactedHeaders.has(normalizedKey) ||
                        redactedHeaders.has(unproxiedKey)
                ) {
index 7b087ba9ad6f4de406b067d63f28321aecdeafd9..7724bb4f5286ef00e89b8237ec8aa7d6cd3621fd 100644 (file)
@@ -1,7 +1,7 @@
 import { error } from '@sveltejs/kit';
 import { browser } from '$app/environment';
 import { base } from '$app/paths';
-import { AUTHORIZATION_HEADER, BEARER_PREFIX, CONTENT_TYPE_HEADER } from '$lib/constants';
+import { HEADERS } from '$lib/constants';
 import { MimeTypeApplication } from '$lib/enums';
 import { config } from '$lib/stores/settings.svelte';
 
@@ -18,14 +18,14 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
 
        try {
                const headers: Record<string, string> = {
-                       [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON
+                       [HEADERS.CONTENT_TYPE]: MimeTypeApplication.JSON
                };
 
                // Probe /props even without a stored key: on a server started with
                // --api-key the unauthenticated request returns 401 and surfaces the
                // API key splash, which is the onboarding path for entering the key.
                if (apiKey) {
-                       headers[AUTHORIZATION_HEADER] = `${BEARER_PREFIX}${apiKey}`;
+                       headers[HEADERS.AUTHORIZATION] = `${HEADERS.BEARER}${apiKey}`;
                }
 
                const response = await fetch(`${base}/props`, { headers });
diff --git a/tools/ui/src/lib/utils/built-in-tools.ts b/tools/ui/src/lib/utils/built-in-tools.ts
new file mode 100644 (file)
index 0000000..73b5547
--- /dev/null
@@ -0,0 +1,13 @@
+import { BUILTIN_TOOL_UI } from '$lib/constants';
+import type { BuiltinToolUiEntry } from '$lib/types';
+
+/**
+ * Resolve the UI metadata (label + icon) for a built-in tool by its name.
+ * Falls back to null for unknown or non-built-in tools so callers can render
+ * a generic chrome instead.
+ */
+export function getBuiltinToolUi(toolName: string | undefined): BuiltinToolUiEntry | null {
+       if (!toolName) return null;
+
+       return (BUILTIN_TOOL_UI as Record<string, BuiltinToolUiEntry>)[toolName] ?? null;
+}
index 2f5b2494d00683ae46642ae632cc4e294aeed3ae..bb0100755b306ce01ea1da852a725e64396f4b1f 100644 (file)
@@ -1,4 +1,4 @@
-import { DEFAULT_CACHE_MAX_ENTRIES, DEFAULT_CACHE_TTL_MS } from '$lib/constants';
+import { CACHE } from '$lib/constants';
 
 /**
  * TTL Cache - Time-To-Live cache implementation for memory optimization
@@ -36,8 +36,8 @@ export class TTLCache<K extends string, V> {
        private readonly onEvict?: (key: string, value: unknown) => void;
 
        constructor(options: TTLCacheOptions = {}) {
-               this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
-               this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
+               this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
+               this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
                this.onEvict = options.onEvict;
        }
 
@@ -217,8 +217,8 @@ export class ReactiveTTLMap<K extends string, V> {
        private readonly maxEntries: number;
 
        constructor(options: TTLCacheOptions = {}) {
-               this.ttlMs = options.ttlMs ?? DEFAULT_CACHE_TTL_MS;
-               this.maxEntries = options.maxEntries ?? DEFAULT_CACHE_MAX_ENTRIES;
+               this.ttlMs = options.ttlMs ?? CACHE.DEFAULT_TTL_MS;
+               this.maxEntries = options.maxEntries ?? CACHE.DEFAULT_MAX_ENTRIES;
        }
 
        get(key: K): V | null {
index c4aa4d597ce1163f21d648159a1eff424a289a64..6878bc1ad87ae9a047633f4b2ae7b8963299354d 100644 (file)
@@ -1,6 +1,5 @@
 import { getJpegOrientationFromDataURL, isJpegMimeType } from './jpeg-orientation';
-import { MEGAPIXELS_TO_PIXELS } from '$lib/constants/image-size';
-import { BASE64_IMAGE_URI_REGEX } from '$lib/constants/uri-template';
+import { BASE64_IMAGE_URI_REGEX, IMAGE } from '$lib/constants';
 import { MimeTypeImage } from '$lib/enums';
 
 /**
@@ -51,7 +50,7 @@ export function capImageDataURLSize(
                                        const targetWidth = img.naturalWidth;
                                        const targetHeight = img.naturalHeight;
                                        const totalPixels = targetWidth * targetHeight;
-                                       const maxPixels = Math.floor(maxMegapixels * MEGAPIXELS_TO_PIXELS);
+                                       const maxPixels = Math.floor(maxMegapixels * IMAGE.MEGAPIXELS_TO_PIXELS);
 
                                        if (maxPixels > 0 && totalPixels > maxPixels) {
                                                const scaleFactor = Math.sqrt(maxPixels / totalPixels);
diff --git a/tools/ui/src/lib/utils/chat-commands.ts b/tools/ui/src/lib/utils/chat-commands.ts
new file mode 100644 (file)
index 0000000..9345f0a
--- /dev/null
@@ -0,0 +1,35 @@
+import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
+import { ChatFormCommandAction } from '$lib/enums';
+import type { ChatCommandsOptions, ChatFormCommand } from '$lib/types';
+
+/**
+ * The slash commands surfaced by the `/` command picker, in display order.
+ *
+ * Availability is supplied as predicates rather than store imports: this
+ * module is re-exported through the `$lib/utils` barrel, and importing
+ * stores at module load would create a circular dependency (the stores
+ * themselves import from `$lib/utils`).
+ */
+export function getChatCommands(options: ChatCommandsOptions): ChatFormCommand[] {
+       return [
+               {
+                       action: ChatFormCommandAction.PROMPT,
+                       description: 'Insert an MCP prompt',
+                       disabled: !options.hasPrompts(),
+                       name: 'prompt'
+               },
+               {
+                       action: ChatFormCommandAction.CWD,
+                       description: SET_WORKING_DIRECTORY_LABEL,
+                       disabled: !options.hasCwdTools(),
+                       keywords: ['current working directory'],
+                       name: 'cwd'
+               },
+               {
+                       action: ChatFormCommandAction.MODEL,
+                       description: 'Select model',
+                       disabled: !options.showModelSelector,
+                       name: 'model'
+               }
+       ];
+}
index 9ce1fc9f300b92c4c99143cc3fc0fbae575fa657..ab3e4b56f8f998f244e3a52b8309f3482f506f0b 100644 (file)
@@ -1,14 +1,4 @@
-import {
-       AMPERSAND_REGEX,
-       DEFAULT_LANGUAGE,
-       FENCE_PATTERN,
-       GT_REGEX,
-       LANG_PATTERN,
-       LT_REGEX,
-       NEWLINE,
-       TRIM_LEADING_PADDING_REGEX,
-       TRIM_TRAILING_PADDING_REGEX
-} from '$lib/constants';
+import { CODE_BLOCK, NEWLINE } from '$lib/constants';
 import hljs from 'highlight.js';
 
 export interface IncompleteCodeBlock {
@@ -81,11 +71,16 @@ export function splitGluedClosingCodeFences(markdown: string): string {
  * so internal blank lines are still rendered as such.
  */
 function trimCodePadding(code: string): string {
-       return code.replace(TRIM_LEADING_PADDING_REGEX, '').replace(TRIM_TRAILING_PADDING_REGEX, '');
+       return code
+               .replace(CODE_BLOCK.TRIM_LEADING_PADDING_REGEX, '')
+               .replace(CODE_BLOCK.TRIM_TRAILING_PADDING_REGEX, '');
 }
 
 function escapeCode(code: string): string {
-       return code.replace(AMPERSAND_REGEX, '&amp;').replace(LT_REGEX, '&lt;').replace(GT_REGEX, '&gt;');
+       return code
+               .replace(CODE_BLOCK.AMPERSAND_REGEX, '&amp;')
+               .replace(CODE_BLOCK.LT_REGEX, '&lt;')
+               .replace(CODE_BLOCK.GT_REGEX, '&gt;');
 }
 
 /** Bounded cache for highlightCode results. */
@@ -152,7 +147,7 @@ export { trimCodePadding };
 export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock | null {
        // Count all code fences in the markdown
        // A code block is incomplete if there's an odd number of ``` fences
-       const fencePattern = new RegExp(FENCE_PATTERN.source, FENCE_PATTERN.flags);
+       const fencePattern = new RegExp(CODE_BLOCK.FENCE_PATTERN.source, CODE_BLOCK.FENCE_PATTERN.flags);
        const fences: number[] = [];
 
        let fenceMatch;
@@ -174,8 +169,8 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock
        const openingIndex = fences[fences.length - 1];
        const afterOpening = markdown.slice(openingIndex + 3);
        // Extract language and code content
-       const langMatch = afterOpening.match(LANG_PATTERN);
-       const language = langMatch?.[1] || DEFAULT_LANGUAGE;
+       const langMatch = afterOpening.match(CODE_BLOCK.LANG_PATTERN);
+       const language = langMatch?.[1] || CODE_BLOCK.DEFAULT_LANGUAGE;
        const codeStartIndex = openingIndex + 3 + (langMatch?.[0]?.length ?? 0);
        const code = markdown.slice(codeStartIndex);
 
index 114fdab64af308d4931963ad3a4ea65a1784b9b8..ea6939b23a0aef4d0a1456c32f75cbcbe1aaf96f 100644 (file)
@@ -520,7 +520,7 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
  * tokens, `<span data-mention-badge="true">` elements for badges,
  * `<code data-code-token>` elements for code spans. The badge's class
  * string + inline SVG are shared with the rehype plugin via
- * `$lib/constants/mention-badge`.
+ * `$lib/constants`.
  */
 export function buildFragment(tokens: ContentToken[]): DocumentFragment {
        const fragment = document.createDocumentFragment();
index 1694b7dbe6b836446aa515b0059f07cdbe9ed483..58423b7e7df95450b8acf79653bfe75f78052e1f 100644 (file)
@@ -3,11 +3,7 @@
  */
 
 import { base } from '$app/paths';
-import {
-       CORS_PROXY_ENDPOINT,
-       CORS_PROXY_HEADER_PREFIX,
-       CORS_PROXY_URL_PARAM
-} from '$lib/constants';
+import { CORS_PROXY, CORS_PROXY_ENDPOINT } from '$lib/constants';
 
 /**
  * Build a proxied URL that routes through llama-server's CORS proxy.
@@ -18,7 +14,7 @@ export function buildProxiedUrl(targetUrl: string): URL {
        const proxyPath = `${base}${CORS_PROXY_ENDPOINT}`;
        const proxyUrl = new URL(proxyPath, window.location.origin);
 
-       proxyUrl.searchParams.set(CORS_PROXY_URL_PARAM, targetUrl);
+       proxyUrl.searchParams.set(CORS_PROXY.URL_PARAM, targetUrl);
 
        return proxyUrl;
 }
@@ -32,7 +28,7 @@ export function buildProxiedHeaders(headers: Record<string, string>): Record<str
        const proxiedHeaders: Record<string, string> = {};
 
        for (const [key, value] of Object.entries(headers)) {
-               proxiedHeaders[`${CORS_PROXY_HEADER_PREFIX}${key}`] = value;
+               proxiedHeaders[`${CORS_PROXY.HEADER_PREFIX}${key}`] = value;
        }
 
        return proxiedHeaders;
index c7d5bca79bd57b809cb8a02c8f93cb28901c0471..ded32793b6bf49fcaca2bb899be0e3c9da2e093f 100644 (file)
@@ -12,12 +12,7 @@ import {
        joinPath,
        rankEntries
 } from './working-directory';
-import {
-       GLOB_WILDCARD,
-       PATH_NAV_MAX_DEPTH,
-       PATH_SEPARATOR,
-       WINDOWS_SEPARATOR
-} from '$lib/constants';
+import { GLOB, PATH_SEPARATOR, SEARCH } from '$lib/constants';
 import { BuiltInTool, GlobSearchType } from '$lib/enums';
 import { ToolsService } from '$lib/services/tools.service';
 
@@ -113,7 +108,7 @@ export async function runGlobSearchWithChildren(
        options: GlobSearchChildOptions = {}
 ): Promise<GlobSearchChildResult> {
        const {
-               childMaxDepth = PATH_NAV_MAX_DEPTH,
+               childMaxDepth = SEARCH.PATH_NAV_MAX_DEPTH,
                descendOnTrailingSeparator = false,
                type = GlobSearchType.ALL
        } = options;
@@ -128,7 +123,7 @@ export async function runGlobSearchWithChildren(
 
        if (last) {
                const wantsDescend = descendOnTrailingSeparator
-                       ? query.endsWith(PATH_SEPARATOR) || query.endsWith(WINDOWS_SEPARATOR)
+                       ? query.endsWith(PATH_SEPARATOR) || query.endsWith(GLOB.WINDOWS_SEPARATOR)
                        : true;
                const exact = ranked.find(
                        (e) => e.type === 'dir' && lastPathSegment(e.path).toLowerCase() === last.toLowerCase()
@@ -137,7 +132,7 @@ export async function runGlobSearchWithChildren(
                if (wantsDescend && exact) {
                        const exactDir = joinPath(res.base, exact.path);
                        const childRes = await runGlobSearch(
-                               { include: GLOB_WILDCARD, maxDepth: childMaxDepth, path: exactDir, rankQuery: '' },
+                               { include: GLOB.WILDCARD, maxDepth: childMaxDepth, path: exactDir, rankQuery: '' },
                                type,
                                limit,
                                signal
index 143b713bf6193dccbf3b82f4769146e1e2f6e947..9358d6087f66243289fac819136e3b784b66e72a 100644 (file)
@@ -1,4 +1,4 @@
-import { HEIC_JPEG_QUALITY } from '$lib/constants/image-size';
+import { IMAGE } from '$lib/constants';
 import { MimeTypeImage } from '$lib/enums';
 
 // heic requires a relatively large decoder, in order to reduce primary bundle size
@@ -32,7 +32,7 @@ export async function heicFileToJpegDataURL(file: File | Blob): Promise<string>
        const { heicTo } = await getHeicTo();
        const jpegBlob = await heicTo({
                blob: file,
-               quality: HEIC_JPEG_QUALITY,
+               quality: IMAGE.HEIC_JPEG_QUALITY,
                type: MimeTypeImage.JPEG
        });
 
index c3585bd5f85dbc3e910d9c978247ee6d14c3cace..eef5be9bfeadea73303150244143730d5b0fc277 100644 (file)
@@ -317,7 +317,16 @@ export { tryParseToolResultObject } from './tool-call-meta';
 // Per-tool UI metadata (label + icon) used by the tool-call chrome.
 // Re-exported through $lib/utils so renderer components can read the
 // label without depending on $lib/constants directly.
-export { getBuiltinToolUi, type BuiltinToolUiEntry } from '$lib/constants/built-in-tools';
+export { getBuiltinToolUi } from './built-in-tools';
+export type { BuiltinToolUiEntry } from '$lib/types';
+
+// Chat command picker
+
+export { getChatCommands } from './chat-commands';
+
+// Sandbox tool definition
+// SANDBOX_TOOL_DEFINITION is deprecated; kept for backward compatibility.
+export { buildSandboxToolDefinition, SANDBOX_TOOL_DEFINITION } from './sandbox-tool';
 
 // Cryptography utilities
 
index d0dfb9271359e60722127f8382d79a77aa58a728..1389a0be27fc05b8b6838776f97c7478370a5326 100644 (file)
@@ -1,14 +1,4 @@
-import {
-       APP1_MARKER,
-       EXIF_ORIENTATION_TAG,
-       EXIF_SCAN_BYTE_LIMIT,
-       EXIF_SIGNATURE,
-       IFD_ENTRY_SIZE,
-       JPEG_SOI_MARKER,
-       SOS_MARKER,
-       TIFF_LITTLE_ENDIAN,
-       TIFF_MAGIC
-} from '$lib/constants/jpeg-exif';
+import { EXIF } from '$lib/constants';
 import { MimeTypeImage } from '$lib/enums';
 
 /**
@@ -28,7 +18,7 @@ export function getJpegOrientationFromDataURL(base64UrlJpeg: string): number {
                }
 
                // Keep the slice a multiple of 4 characters so atob accepts it
-               const charLimit = Math.ceil(EXIF_SCAN_BYTE_LIMIT / 3) * 4;
+               const charLimit = Math.ceil(EXIF.SCAN_BYTE_LIMIT / 3) * 4;
                const slice = base64UrlJpeg.slice(payloadStart, payloadStart + charLimit);
                const binary = atob(slice.slice(0, slice.length - (slice.length % 4)));
                const bytes = new Uint8Array(binary.length);
@@ -49,7 +39,7 @@ export function getJpegOrientationFromDataURL(base64UrlJpeg: string): number {
  * @returns The orientation value (1 to 8), or 1 when absent or malformed
  */
 function findExifOrientation(view: DataView): number {
-       if (view.byteLength < 4 || view.getUint16(0) !== JPEG_SOI_MARKER) {
+       if (view.byteLength < 4 || view.getUint16(0) !== EXIF.JPEG_SOI_MARKER) {
                return 1;
        }
 
@@ -63,13 +53,13 @@ function findExifOrientation(view: DataView): number {
                const marker = view.getUint8(offset + 1);
 
                // Compressed image data starts here: no EXIF past this point
-               if (marker === SOS_MARKER) {
+               if (marker === EXIF.SOS_MARKER) {
                        return 1;
                }
 
                const segmentLength = view.getUint16(offset + 2);
 
-               if (marker === APP1_MARKER) {
+               if (marker === EXIF.APP1_MARKER) {
                        return parseExifOrientation(view, offset + 4, segmentLength);
                }
 
@@ -92,7 +82,7 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
        // The payload opens with the "Exif\0\0" signature
        if (
                start + 6 > end ||
-               view.getUint32(start) !== EXIF_SIGNATURE ||
+               view.getUint32(start) !== EXIF.EXIF_SIGNATURE ||
                view.getUint16(start + 4) !== 0
        ) {
                return 1;
@@ -104,9 +94,9 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
                return 1;
        }
 
-       const littleEndian = view.getUint16(tiff) === TIFF_LITTLE_ENDIAN;
+       const littleEndian = view.getUint16(tiff) === EXIF.TIFF_LITTLE_ENDIAN;
 
-       if (view.getUint16(tiff + 2, littleEndian) !== TIFF_MAGIC) {
+       if (view.getUint16(tiff + 2, littleEndian) !== EXIF.TIFF_MAGIC) {
                return 1;
        }
 
@@ -120,13 +110,13 @@ function parseExifOrientation(view: DataView, start: number, segmentLength: numb
 
        // Scan IFD0 entries for the orientation tag
        for (let i = 0; i < entryCount; i++) {
-               const entry = tiff + ifdOffset + 2 + i * IFD_ENTRY_SIZE;
+               const entry = tiff + ifdOffset + 2 + i * EXIF.IFD_ENTRY_SIZE;
 
-               if (entry + IFD_ENTRY_SIZE > end) {
+               if (entry + EXIF.IFD_ENTRY_SIZE > end) {
                        return 1;
                }
 
-               if (view.getUint16(entry, littleEndian) === EXIF_ORIENTATION_TAG) {
+               if (view.getUint16(entry, littleEndian) === EXIF.ORIENTATION_TAG) {
                        const orientation = view.getUint16(entry + 8, littleEndian);
 
                        return orientation >= 1 && orientation <= 8 ? orientation : 1;
index c4493d870362ebb1031b426b5d1ae42195cde21b..61d5f8a9a508b972e300166d9afcc18d921a8eb9 100644 (file)
@@ -15,23 +15,16 @@ import {
        FILE_EXTENSION_REGEX,
        IMAGE_FILE_EXTENSION_REGEX,
        MCP_SERVER_ID_PREFIX,
-       MCP_SSE_ENDPOINT,
-       MCP_SSE_ENDPOINT_QUERY,
-       MCP_SSE_ENDPOINT_SLASH,
+       MCP_SSE,
+       MIME_TYPE_PREFIXES,
+       MIME_TYPE_SUBSTRINGS,
        PATH_SEPARATOR,
        PROTOCOL_PREFIX_REGEX,
        RESOURCE_TEXT_CONTENT_SEPARATOR,
-       TEXT_FILE_EXTENSION_REGEX
+       TEXT_FILE_EXTENSION_REGEX,
+       URI_PATTERNS
 } from '$lib/constants';
-import {
-       MCPLogLevel,
-       MCPTransportType,
-       MimeTypeIncludes,
-       MimeTypePrefix,
-       MimeTypeText,
-       UriPattern,
-       UrlProtocol
-} from '$lib/enums';
+import { MCPLogLevel, MCPTransportType, MimeTypeText, UrlProtocol } from '$lib/enums';
 import type { MCPResourceContent, MCPResourceInfo, MCPServerSettingsEntry } from '$lib/types';
 import type { MimeTypeUnion } from '$lib/types/common';
 import type { Component } from 'svelte';
@@ -51,9 +44,9 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType {
        }
 
        if (
-               normalized.endsWith(MCP_SSE_ENDPOINT) ||
-               normalized.endsWith(MCP_SSE_ENDPOINT_SLASH) ||
-               normalized.includes(MCP_SSE_ENDPOINT_QUERY)
+               normalized.endsWith(MCP_SSE.ENDPOINT) ||
+               normalized.endsWith(MCP_SSE.ENDPOINT_SLASH) ||
+               normalized.includes(MCP_SSE.ENDPOINT_QUERY)
        ) {
                return MCPTransportType.SSE;
        }
@@ -150,7 +143,7 @@ export function getMcpLogLevelClass(level: MCPLogLevel): string {
  * @returns True if the MIME type starts with 'image/'
  */
 export function isImageMimeType(mimeType?: MimeTypeUnion): boolean {
-       return mimeType?.startsWith(MimeTypePrefix.IMAGE) ?? false;
+       return mimeType?.startsWith(MIME_TYPE_PREFIXES.IMAGE) ?? false;
 }
 
 /**
@@ -213,9 +206,9 @@ export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean
        const u = uri?.toLowerCase() || '';
 
        return (
-               mime.includes(MimeTypeIncludes.JSON) ||
-               mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
-               mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.JSON) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT) ||
                CODE_FILE_EXTENSION_REGEX.test(u)
        );
 }
@@ -231,7 +224,7 @@ export function isImageResource(mimeType?: MimeTypeUnion, uri?: string): boolean
        const mime = mimeType?.toLowerCase() || '';
        const u = uri?.toLowerCase() || '';
 
-       return mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u);
+       return mime.startsWith(MIME_TYPE_PREFIXES.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u);
 }
 
 /**
@@ -245,24 +238,24 @@ export function getResourceIcon(mimeType?: MimeTypeUnion, uri?: string): Compone
        const mime = mimeType?.toLowerCase() || '';
        const u = uri?.toLowerCase() || '';
 
-       if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
+       if (mime.startsWith(MIME_TYPE_PREFIXES.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
                return Image;
        }
 
        if (
-               mime.includes(MimeTypeIncludes.JSON) ||
-               mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
-               mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.JSON) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT) ||
+               mime.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT) ||
                CODE_FILE_EXTENSION_REGEX.test(u)
        ) {
                return Code;
        }
 
-       if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
+       if (mime.includes(MIME_TYPE_PREFIXES.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
                return FileText;
        }
 
-       if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) {
+       if (u.includes(URI_PATTERNS.DATABASE_KEYWORD) || u.includes(URI_PATTERNS.DATABASE_SCHEME)) {
                return Database;
        }
 
index 5b1b7d1fe5aa08b05632c0158f35e868f2be5ba0..0a0cc60c68daad00d5103e4affbf436a006f9679 100644 (file)
@@ -1,10 +1,11 @@
 import { abbreviateHome, lastPathSegment } from './path-display';
-import { DIRECTORY_PATH_SUFFIX, FILE_URI_PREFIX } from '$lib/constants';
 import {
+       DIRECTORY_PATH_SUFFIX,
+       FILE_URI_PREFIX,
        MENTION_BADGE_FILE_ICON_PATHS,
        MENTION_BADGE_FOLDER_ICON_PATHS,
        MENTION_LINK_SCAN_FLAGS
-} from '$lib/constants/mention-badge';
+} from '$lib/constants';
 import { FileMentionEntryType } from '$lib/enums';
 import type { FileMentionEntry } from '$lib/types';
 
@@ -14,7 +15,7 @@ export {
        MENTION_BADGE_SVG_ATTRIBUTES,
        MENTION_BADGE_FILE_ICON_PATHS,
        MENTION_BADGE_FOLDER_ICON_PATHS
-} from '$lib/constants/mention-badge';
+} from '$lib/constants';
 
 // `)` is allowed in a path only when not followed by whitespace or `[`,
 // so macOS paths parse while adjacent badges still terminate the match.
index 8f8c081e0495e2365d47d055cd733c86ec69dfc3..19ba335708d0cacc04c055a366de3221e12696a7 100644 (file)
@@ -4,10 +4,10 @@ import {
        CWD_LINK_REGEX,
        FILE_URI_PREFIX,
        HOME_TILDE,
-       HOME_TILDE_PREFIX
+       HOME_TILDE_PREFIX,
+       PATH_SEPARATOR,
+       TRAILING_SLASHES_REGEX
 } from '$lib/constants';
-import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
-import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
 
 export function lastPathSegment(p: string): string {
        const trimmed = p.replace(TRAILING_SLASHES_REGEX, '');
diff --git a/tools/ui/src/lib/utils/sandbox-tool.ts b/tools/ui/src/lib/utils/sandbox-tool.ts
new file mode 100644 (file)
index 0000000..bc057e4
--- /dev/null
@@ -0,0 +1,52 @@
+import {
+       SANDBOX_TIMEOUT_MS_DEFAULT,
+       SANDBOX_TIMEOUT_MS_MAX,
+       SANDBOX_TOOL_NAME
+} from '$lib/constants';
+import { JsonSchemaType, ToolCallType } from '$lib/enums';
+import type { OpenAIToolDefinition } from '$lib/types';
+
+const NERDAMER_DESCRIPTION = `
+Symbolic/numeric math via \`nerdamer\`
+nerdamer(expr,subs?,opts?)/nerdamer.func(...)→Expression Format via .text(fmt?) (fmt: 'decimals'|'fractions'|'scientific') eval via .evaluate(subs?)
+nerdamer(expr,{x:2}) substitutes numeric via opts 'numer' or .evaluate()
+simplify/expand/factor(expr) div/gcd/lcm(...) coeffs/partfrac(expr,var)
+diff/integrate(expr,var) defint(expr,lo,hi,var?) sum/product(expr,var,lo,hi) limit(expr,var,pt)
+solve(expr,var) solveEquations([eq1,eq2],[var1,var2])
+polarform/rectform/arg/realpart/imagpart(z)
+set/get Var/Constant(name,val?) setFunction(name,[params],body)
+IMPORTANT:Identifier 'nerdamer' has already been declared, use it directly`;
+
+/**
+ * Build the sandbox tool definition. When `includeSymbolicMath` is true,
+ * the description includes nerdamer API documentation; otherwise it
+ * describes a plain JavaScript sandbox.
+ */
+export function buildSandboxToolDefinition(includeSymbolicMath: boolean): OpenAIToolDefinition {
+       return {
+               function: {
+                       description: includeSymbolicMath
+                               ? `Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.${NERDAMER_DESCRIPTION}`
+                               : 'Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.',
+                       name: SANDBOX_TOOL_NAME,
+                       parameters: {
+                               properties: {
+                                       code: {
+                                               description: 'JavaScript source to execute',
+                                               type: JsonSchemaType.STRING
+                                       },
+                                       timeout_ms: {
+                                               description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}`,
+                                               type: JsonSchemaType.NUMBER
+                                       }
+                               },
+                               required: ['code'],
+                               type: JsonSchemaType.OBJECT
+                       }
+               },
+               type: ToolCallType.FUNCTION
+       };
+}
+
+/** @deprecated Use {@link buildSandboxToolDefinition} instead. Kept for backward compatibility. */
+export const SANDBOX_TOOL_DEFINITION = buildSandboxToolDefinition(true);
index 452ea91aca26f69edc7250bbc8d0982ed83d7724..586669adf2b77da535f6590251da59b4f10a4e98 100644 (file)
@@ -1,4 +1,4 @@
-import { SVG_MAX_BYTES, SVG_SANITIZE_CONFIG, SVG_TAG_PREFIX } from '$lib/constants';
+import { SVG } from '$lib/constants';
 import DOMPurify from 'dompurify';
 
 /**
@@ -10,13 +10,13 @@ import DOMPurify from 'dompurify';
 export function sanitizeSvg(source: string): string {
        const trimmed = source.trim();
 
-       if (!trimmed || trimmed.length > SVG_MAX_BYTES) return '';
+       if (!trimmed || trimmed.length > SVG.MAX_BYTES) return '';
 
-       if (!trimmed.startsWith(SVG_TAG_PREFIX)) return '';
+       if (!trimmed.startsWith(SVG.TAG_PREFIX)) return '';
 
-       const clean = DOMPurify.sanitize(trimmed, SVG_SANITIZE_CONFIG) as unknown as string;
+       const clean = DOMPurify.sanitize(trimmed, SVG.SANITIZE_CONFIG) as unknown as string;
 
-       if (!clean || !clean.includes(SVG_TAG_PREFIX)) return '';
+       if (!clean || !clean.includes(SVG.TAG_PREFIX)) return '';
 
        return clean;
 }
index 253786a7e3019ae9cf4328094f45aa0f807335b0..bf0946fc09b27dc31eacce0e5791e718eee912c5 100644 (file)
@@ -1,3 +1,5 @@
+import { CONVERSATION_ID_SEPARATOR } from '$lib/constants';
+
 /**
  * Build the conversation identity used by the server side replay buffer.
  *
@@ -11,5 +13,5 @@ export function streamIdentity(conversationId: string, model?: string | null): s
 
        if (!model) return conversationId;
 
-       return `${conversationId}::${model}`;
+       return `${conversationId}${CONVERSATION_ID_SEPARATOR}${model}`;
 }
index 8dc7b97eb91956dbb19ec5d0ac693a47ceb7ab94..4ba82719b2f0a256c7d221a925ab066854432568 100644 (file)
@@ -2,8 +2,7 @@ import {
        LEADING_SLASHES_REGEX,
        TEMPLATE_EXPRESSION_REGEX,
        URI_SCHEME_SEPARATOR,
-       URI_TEMPLATE_OPERATORS,
-       URI_TEMPLATE_SEPARATORS,
+       URI_TEMPLATE_SYMBOLS,
        VARIABLE_EXPLODE_MODIFIER_REGEX,
        VARIABLE_PREFIX_MODIFIER_REGEX
 } from '../constants';
@@ -126,60 +125,59 @@ export function expandTemplate(template: string, values: Record<string, string>)
                        if (expandedParts.length === 0) return '';
 
                        switch (operator) {
-                               case URI_TEMPLATE_OPERATORS.RESERVED:
+                               case URI_TEMPLATE_SYMBOLS.RESERVED:
                                        // Reserved expansion: no encoding
-                                       return expandedParts.join(URI_TEMPLATE_SEPARATORS.COMMA);
-                               case URI_TEMPLATE_OPERATORS.FRAGMENT:
+                                       return expandedParts.join(URI_TEMPLATE_SYMBOLS.COMMA);
+                               case URI_TEMPLATE_SYMBOLS.FRAGMENT:
                                        // Fragment expansion
-                                       return (
-                                               URI_TEMPLATE_OPERATORS.FRAGMENT + expandedParts.join(URI_TEMPLATE_SEPARATORS.COMMA)
-                                       );
-                               case URI_TEMPLATE_OPERATORS.PATH_SEGMENT:
+                                       return URI_TEMPLATE_SYMBOLS.FRAGMENT + expandedParts.join(URI_TEMPLATE_SYMBOLS.COMMA);
+                               case URI_TEMPLATE_SYMBOLS.PATH_SEGMENT:
                                        // Path segments
-                                       return URI_TEMPLATE_SEPARATORS.SLASH + expandedParts.join(URI_TEMPLATE_SEPARATORS.SLASH);
-                               case URI_TEMPLATE_OPERATORS.LABEL:
-                                       // Label expansion
                                        return (
-                                               URI_TEMPLATE_SEPARATORS.PERIOD + expandedParts.join(URI_TEMPLATE_SEPARATORS.PERIOD)
+                                               URI_TEMPLATE_SYMBOLS.PATH_SEGMENT +
+                                               expandedParts.join(URI_TEMPLATE_SYMBOLS.PATH_SEGMENT)
                                        );
-                               case URI_TEMPLATE_OPERATORS.PATH_PARAM:
+                               case URI_TEMPLATE_SYMBOLS.LABEL:
+                                       // Label expansion
+                                       return URI_TEMPLATE_SYMBOLS.LABEL + expandedParts.join(URI_TEMPLATE_SYMBOLS.LABEL);
+                               case URI_TEMPLATE_SYMBOLS.PATH_PARAM:
                                        // Path-style parameters
                                        return varNames
                                                .filter((_: string, i: number) => expandedParts[i])
                                                .map(
                                                        (name: string, i: number) =>
-                                                               `${URI_TEMPLATE_SEPARATORS.SEMICOLON}${name}=${expandedParts[i]}`
+                                                               `${URI_TEMPLATE_SYMBOLS.PATH_PARAM}${name}=${expandedParts[i]}`
                                                )
                                                .join('');
-                               case URI_TEMPLATE_OPERATORS.FORM_QUERY:
+                               case URI_TEMPLATE_SYMBOLS.FORM_QUERY:
                                        // Form-style query
                                        return (
-                                               URI_TEMPLATE_SEPARATORS.QUERY_PREFIX +
+                                               URI_TEMPLATE_SYMBOLS.FORM_QUERY +
                                                varNames
                                                        .filter((_: string, i: number) => expandedParts[i])
                                                        .map(
                                                                (name: string, i: number) =>
                                                                        `${encodeURIComponent(name)}=${encodeURIComponent(expandedParts[i])}`
                                                        )
-                                                       .join(URI_TEMPLATE_SEPARATORS.QUERY_CONTINUATION)
+                                                       .join(URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION)
                                        );
-                               case URI_TEMPLATE_OPERATORS.FORM_CONTINUATION:
+                               case URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION:
                                        // Form-style query continuation
                                        return (
-                                               URI_TEMPLATE_SEPARATORS.QUERY_CONTINUATION +
+                                               URI_TEMPLATE_SYMBOLS.FORM_CONTINUATION +
                                                varNames
                                                        .filter((_: string, i: number) => expandedParts[i])
                                                        .map(
                                                                (name: string, i: number) =>
                                                                        `${encodeURIComponent(name)}=${encodeURIComponent(expandedParts[i])}`
                                                        )
-                                                       .join(URI_TEMPLATE_SEPARATORS.COMMA)
+                                                       .join(URI_TEMPLATE_SYMBOLS.COMMA)
                                        );
                                default:
                                        // Simple string expansion (default operator)
                                        return expandedParts
                                                .map((v: string) => encodeURIComponent(v))
-                                               .join(URI_TEMPLATE_SEPARATORS.COMMA);
+                                               .join(URI_TEMPLATE_SYMBOLS.COMMA);
                        }
                }
        );
index ae545301921525abf90b45389a80f58548860ef3..09ee87ecc440a02e201763f7f8fad3478aa4c586 100644 (file)
@@ -7,20 +7,13 @@
 
 import { lastPathSegment } from './path-display';
 import {
-       DRIVE_PREFIX_REGEX,
-       DRIVE_ROOT_REGEX,
-       GLOB_RANGE_CLOSE,
-       GLOB_RANGE_OPEN,
-       GLOB_SPECIAL_CHARS,
-       GLOB_WILDCARD,
+       GLOB,
        HOME_TILDE,
        LEADING_SLASHES_REGEX,
-       PATH_NAV_MAX_DEPTH,
-       UNC_ROOT_REGEX,
-       WINDOWS_SEPARATOR
+       PATH_SEPARATOR,
+       SEARCH,
+       TRAILING_SLASHES_REGEX
 } from '$lib/constants';
-import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
-import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
 
 export interface GlobEntry {
        path: string;
@@ -37,17 +30,18 @@ export interface PathQuery {
  * backslash is left alone: it is a legal filename character on POSIX.
  */
 function toPosixSeparators(query: string): string {
-       if (!DRIVE_PREFIX_REGEX.test(query) && !query.startsWith(WINDOWS_SEPARATOR)) return query;
+       if (!GLOB.DRIVE_PREFIX_REGEX.test(query) && !query.startsWith(GLOB.WINDOWS_SEPARATOR))
+               return query;
 
-       return query.split(WINDOWS_SEPARATOR).join(PATH_SEPARATOR);
+       return query.split(GLOB.WINDOWS_SEPARATOR).join(PATH_SEPARATOR);
 }
 
 export function rootPrefixLength(path: string): number {
-       const unc = path.match(UNC_ROOT_REGEX);
+       const unc = path.match(GLOB.UNC_ROOT_REGEX);
 
        if (unc) return unc[0].length;
 
-       const drive = path.match(DRIVE_ROOT_REGEX);
+       const drive = path.match(GLOB.DRIVE_ROOT_REGEX);
 
        if (drive) return drive[0].length;
 
@@ -83,20 +77,20 @@ export function splitPathQuery(query: string): PathQuery | null {
 }
 
 export function buildCaseInsensitiveGlob(query: string): string {
-       let out = GLOB_WILDCARD;
+       let out = GLOB.WILDCARD;
 
        for (const c of query) {
                const lo = c.toLowerCase();
                const up = c.toUpperCase();
 
-               if (lo !== up) out += GLOB_RANGE_OPEN + lo + up + GLOB_RANGE_CLOSE;
+               if (lo !== up) out += GLOB.RANGE_OPEN + lo + up + GLOB.RANGE_CLOSE;
                // glob metacharacters are escaped into a literal character class so a
                // query like "a*b" matches a literal '*' instead of becoming "ab"
-               else if (GLOB_SPECIAL_CHARS.includes(c)) out += GLOB_RANGE_OPEN + c + GLOB_RANGE_CLOSE;
+               else if (GLOB.SPECIAL_CHARS.includes(c)) out += GLOB.RANGE_OPEN + c + GLOB.RANGE_CLOSE;
                else out += c;
        }
 
-       return out + GLOB_WILDCARD;
+       return out + GLOB.WILDCARD;
 }
 
 export interface GlobSearchArgs {
@@ -120,9 +114,9 @@ export function buildGlobSearchArgs(
        const include = pathQuery
                ? pathQuery.last
                        ? buildCaseInsensitiveGlob(pathQuery.last)
-                       : GLOB_WILDCARD
+                       : GLOB.WILDCARD
                : buildCaseInsensitiveGlob(query);
-       const maxDepth = pathQuery ? PATH_NAV_MAX_DEPTH : searchDepth;
+       const maxDepth = pathQuery ? SEARCH.PATH_NAV_MAX_DEPTH : searchDepth;
 
        return { include, last: pathQuery?.last, maxDepth, path, rankQuery: pathQuery?.last ?? query };
 }
index c867f6de9201b747550d9a46328f6ebc007ca2aa..f167c8e77ce59f7b9cd370e7cc3346047605749a 100644 (file)
@@ -2,8 +2,7 @@
        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);
index 089958bdba439bf6550002438f6eba2696c69b88..560d36c4d72216b82dfa5d5c854b7c188504618a 100644 (file)
@@ -7,10 +7,14 @@
        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 })
index feb36058a32fe2908e256d7e37d875e6e541c276..424b74f67cf91bdba64519ad7a2690ce533b274d 100644 (file)
@@ -3,7 +3,7 @@
        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';
index 061a38a61f65e36c65c88cb87d78ae94ffda6bb3..bad7f6ccb07661171594db02990a4a8b1900128a 100644 (file)
@@ -1,4 +1,4 @@
-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';
index 9e684a7635b533ac6bda9d0e866346657427a686..a4fd375b3cd005eefd211444208f1b4e10fa3713 100644 (file)
@@ -6,7 +6,7 @@
 // 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';
index 6e169702cfe240e8c902f649617a0044ca29d15c..ddf1393e70e8389e34330295c0cc48008ab7fcb6 100644 (file)
@@ -1,5 +1,4 @@
-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';
index 380a2e74d0629276cc6b3294be00f5cc47456ad0..078cc7cbece388e687c1a5c845546b1cef874257 100644 (file)
@@ -4,7 +4,7 @@
 // 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';
index bef2400df1685234a36e4275027d1d224719ab4b..110011ef0dcb661fbcfb397bd93e61a35288f225 100644 (file)
@@ -1,4 +1,4 @@
-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';
index f4c2e8a4761b53af71bb864bc9168fa8937e6c5f..ec4fc213c3a3f85178a08d68b4293a4038894779 100644 (file)
@@ -1,4 +1,4 @@
-import { LEGACY_AGENTIC_REGEX } from '$lib/constants/agentic';
+import { LEGACY_AGENTIC_REGEX } from '$lib/constants';
 import { describe, expect, it } from 'vitest';
 
 /**
index e7400eeb121f2d35a2efd47881f79c9e1650e197..47d6ac25363ec80cff26c759f93a8deaa38e7961 100644 (file)
@@ -1,5 +1,4 @@
-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';
 
index 52bc0dad222c63e003fffed20ba5c7fe3df93ab0..a47a83cf311a3b9f04cd21f2d107b98e4c311b3f 100644 (file)
@@ -1,4 +1,4 @@
-import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
+import { SETTINGS_KEYS } from '$lib/constants';
 import { describe, expect, it } from 'vitest';
 
 /**
@@ -12,7 +12,7 @@ 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);
index d79a3f025e88ab1053587c2ea8ce35bd9ad45c3d..358881b47044d3f3f8d8b5d719a8c22d971b80b5 100644 (file)
@@ -1,5 +1,5 @@
 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';
@@ -96,9 +96,9 @@ describe('MCPService', () => {
 
        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
index 85b3420710bd7b6603ce5c33edb8c2246f0620a6..a0808a767a4121623c5719230892dbac9cc7ac3c 100644 (file)
@@ -1,4 +1,4 @@
-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';
 
index 099de8bdf13ab71dbe6085f76989760e63468585..b5ee8648574475db5b645f1bccb982d15642e61e 100644 (file)
@@ -1,4 +1,4 @@
-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';
 
@@ -61,9 +61,9 @@ describe('sanitizeHeaders', () => {
        });
 
        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',
index 4b52c1a9c33c34f799fe072be32e8b141853d53b..5ffab7fb074bfa53b1dcee409315435bacd3cb36 100644 (file)
@@ -1,4 +1,4 @@
-import { URI_TEMPLATE_OPERATORS } from '../../src/lib/constants/uri-template';
+import { URI_TEMPLATE_SYMBOLS } from '../../src/lib/constants/uri-template.constants';
 import {
        expandTemplate,
        extractTemplateVariables,
@@ -26,7 +26,7 @@ describe('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', () => {
@@ -48,13 +48,13 @@ describe('extractTemplateVariables', () => {
        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', () => {
index 35b15c5c5f554d5253b186e964cce21750988634..6e9b067cf3d1fa71d6afd9baf24b9f4cbef89a54 100644 (file)
@@ -1,4 +1,4 @@
-import { GLOB_WILDCARD, PATH_NAV_MAX_DEPTH } from '$lib/constants';
+import { GLOB, SEARCH } from '$lib/constants';
 import {
        buildCaseInsensitiveGlob,
        buildGlobSearchArgs,
@@ -148,7 +148,7 @@ describe('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');
        });
@@ -157,8 +157,8 @@ describe('buildGlobSearchArgs', () => {
                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', () => {
@@ -166,7 +166,7 @@ describe('buildGlobSearchArgs', () => {
 
                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');
        });
index 8f80d8ec08f0f7b2c54a2912af2f766ada5e6f32..f650b4cfb96a98c8e0ce3ca4cd4e6e8c373db338 100644 (file)
@@ -2,7 +2,7 @@ import { buildInfoPlugin } from './scripts/vite-plugin-build-info';
 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';