]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: Linting & Formatting scripts (#26819)
authorAleksander Grygier <redacted>
Mon, 10 Aug 2026 06:38:37 +0000 (08:38 +0200)
committerGitHub <redacted>
Mon, 10 Aug 2026 06:38:37 +0000 (08:38 +0200)
538 files changed:
tools/ui/eslint.config.js
tools/ui/package-lock.json
tools/ui/package.json
tools/ui/playwright.config.ts
tools/ui/pwa-assets-dark.config.ts
tools/ui/pwa-assets.config.ts
tools/ui/scripts/favicon-colorize.ts
tools/ui/scripts/make-icons-circular.js
tools/ui/scripts/vite-plugin-build-info.ts
tools/ui/scripts/vite-plugin-nerdamer.ts
tools/ui/scripts/vite-plugin-relativize-base.ts
tools/ui/scripts/vite-plugin-splash-screen.ts
tools/ui/src/app.d.ts
tools/ui/src/lib/components/app/actions/ActionIcon.svelte
tools/ui/src/lib/components/app/actions/ActionIconCopyToClipboard.svelte
tools/ui/src/lib/components/app/badges/BadgesModality.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItem.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpPrompt.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpResource.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailFile.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailImage.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItem.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItemAudio.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItemPdf.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewNavButtons.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.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/ChatFormActionAdd/ChatFormActionsAdd.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.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/ChatFormContextGauge/ChatFormContextGauge.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDetailRow.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDetails.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeDial.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugeLoadModel.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/ContextGaugePopup.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge.ts
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormMcpResourcesList.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerItemHeader.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItemSkeleton.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerPopover.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPromptPickerArgumentForm.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPromptPickerArgumentInput.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryChip.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectoryResultsList.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistantModel.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistantProcessingInfo.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistantRawOutput.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistantStatistics.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageCwdChange.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageMcpPrompt/ChatMessageMcpPrompt.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageMcpPrompt/ChatMessageMcpPromptContent.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSynthetic.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockFileGlobSearch.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetDatetime.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetInfo.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockRunJavascript.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search.ts
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/run-javascript.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUser.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserBubble.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionCard/ChatMessageActionCard.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionCard/ChatMessageActionCardContinueRequest.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionCard/ChatMessageActionCardPermissionRequest.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionIcons/ChatMessageActionIcons.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionIcons/ChatMessageActionIconsBranchingControls.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics/ChatMessageStatistics.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics/ChatMessageStatisticsBadge.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenActionScrollDown.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenDialogsAndAlerts.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenServerError.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenStreamResumeStatus.svelte
tools/ui/src/lib/components/app/content/CollapsibleContentBlock.svelte
tools/ui/src/lib/components/app/content/CollapsibleTerminalBlock.svelte
tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
tools/ui/src/lib/components/app/content/MarkdownContent/markdown-handlers.ts
tools/ui/src/lib/components/app/content/MarkdownContent/markdown-utils.ts
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-links.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/enhance-mermaid-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/pre-transform.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/rehype-rtl-support.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/resolve-attachment-images.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/svg-pre.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/table-html-restorer.ts
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/remark/literal-html.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/DialogChatAttachmentsPreview.svelte
tools/ui/src/lib/components/app/dialogs/DialogChatError.svelte
tools/ui/src/lib/components/app/dialogs/DialogCodePreview.svelte
tools/ui/src/lib/components/app/dialogs/DialogConfirmation.svelte
tools/ui/src/lib/components/app/dialogs/DialogConversationRename.svelte
tools/ui/src/lib/components/app/dialogs/DialogConversationSelection.svelte
tools/ui/src/lib/components/app/dialogs/DialogEmptyFileAlert.svelte
tools/ui/src/lib/components/app/dialogs/DialogExportSettings.svelte
tools/ui/src/lib/components/app/dialogs/DialogFileUploadError.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/DialogMermaidPreview.svelte
tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte
tools/ui/src/lib/components/app/dialogs/DialogModelNotAvailable.svelte
tools/ui/src/lib/components/app/forms/HighlightedMatch.svelte
tools/ui/src/lib/components/app/forms/InputWithSuggestions.svelte
tools/ui/src/lib/components/app/forms/KeyValuePairs.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/McpCapabilitiesBadges.svelte
tools/ui/src/lib/components/app/mcp/McpConnectionLogs.svelte
tools/ui/src/lib/components/app/mcp/McpResourcePreview.svelte
tools/ui/src/lib/components/app/mcp/McpResourceTemplateForm.svelte
tools/ui/src/lib/components/app/mcp/McpResourcesBrowser/McpResourcesBrowser.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/McpResourcesBrowser/mcp-resources-browser.ts
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardActions.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardCompact.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardDeleteDialog.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardEditForm.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardHeader.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCardToolsList.svelte
tools/ui/src/lib/components/app/mcp/McpServerForm.svelte
tools/ui/src/lib/components/app/mcp/McpServerIdentity.svelte
tools/ui/src/lib/components/app/mcp/McpServerInfo.svelte
tools/ui/src/lib/components/app/misc/CodeBlockActions.svelte
tools/ui/src/lib/components/app/misc/ConversationSelection.svelte
tools/ui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte
tools/ui/src/lib/components/app/misc/KeyboardShortcutInfo.svelte
tools/ui/src/lib/components/app/misc/TruncatedText.svelte
tools/ui/src/lib/components/app/models/ModelBadge.svelte
tools/ui/src/lib/components/app/models/ModelId.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorDropdown.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorList.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorOption.svelte
tools/ui/src/lib/components/app/models/ModelsSelectorSheet.svelte
tools/ui/src/lib/components/app/models/utils.ts
tools/ui/src/lib/components/app/navigation/DropdownMenuActions.svelte
tools/ui/src/lib/components/app/navigation/DropdownMenuSearchable.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigation.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/navigation/SidebarNavigation/SidebarNavigationConversationList.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationSearch.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationSearchResults.svelte
tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationSelectionBar.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/SettingsChat.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/SettingsChatImportExportTab.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/app/settings/SettingsFooter.svelte
tools/ui/src/lib/components/app/settings/SettingsGroup.svelte
tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte
tools/ui/src/lib/components/pwa/PwaMetaTags.svelte
tools/ui/src/lib/components/pwa/PwaRefreshAlert.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte
tools/ui/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte
tools/ui/src/lib/components/ui/alert-dialog/index.ts
tools/ui/src/lib/components/ui/alert/alert-description.svelte
tools/ui/src/lib/components/ui/alert/alert-title.svelte
tools/ui/src/lib/components/ui/alert/alert.svelte
tools/ui/src/lib/components/ui/badge/badge.svelte
tools/ui/src/lib/components/ui/button-group/button-group-root.svelte
tools/ui/src/lib/components/ui/button/button.svelte
tools/ui/src/lib/components/ui/card/card-action.svelte
tools/ui/src/lib/components/ui/card/card-content.svelte
tools/ui/src/lib/components/ui/card/card-description.svelte
tools/ui/src/lib/components/ui/card/card-footer.svelte
tools/ui/src/lib/components/ui/card/card-header.svelte
tools/ui/src/lib/components/ui/card/card-title.svelte
tools/ui/src/lib/components/ui/card/card.svelte
tools/ui/src/lib/components/ui/card/index.ts
tools/ui/src/lib/components/ui/checkbox/checkbox.svelte
tools/ui/src/lib/components/ui/collapsible/collapsible.svelte
tools/ui/src/lib/components/ui/collapsible/index.ts
tools/ui/src/lib/components/ui/dialog/dialog-content.svelte
tools/ui/src/lib/components/ui/dialog/dialog-description.svelte
tools/ui/src/lib/components/ui/dialog/dialog-footer.svelte
tools/ui/src/lib/components/ui/dialog/dialog-header.svelte
tools/ui/src/lib/components/ui/dialog/dialog-overlay.svelte
tools/ui/src/lib/components/ui/dialog/dialog-title.svelte
tools/ui/src/lib/components/ui/dialog/index.ts
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte
tools/ui/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte
tools/ui/src/lib/components/ui/dropdown-menu/index.ts
tools/ui/src/lib/components/ui/empty/empty-content.svelte
tools/ui/src/lib/components/ui/empty/empty-description.svelte
tools/ui/src/lib/components/ui/empty/empty-header.svelte
tools/ui/src/lib/components/ui/empty/empty-media.svelte
tools/ui/src/lib/components/ui/empty/empty-title.svelte
tools/ui/src/lib/components/ui/empty/empty.svelte
tools/ui/src/lib/components/ui/empty/index.ts
tools/ui/src/lib/components/ui/hover-card/hover-card-content.svelte
tools/ui/src/lib/components/ui/hover-card/index.ts
tools/ui/src/lib/components/ui/input/input.svelte
tools/ui/src/lib/components/ui/label/label.svelte
tools/ui/src/lib/components/ui/popover/index.ts
tools/ui/src/lib/components/ui/popover/popover-content.svelte
tools/ui/src/lib/components/ui/popover/popover-trigger.svelte
tools/ui/src/lib/components/ui/radio-group/radio-group-item.svelte
tools/ui/src/lib/components/ui/radio-group/radio-group.svelte
tools/ui/src/lib/components/ui/scroll-area/index.ts
tools/ui/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte
tools/ui/src/lib/components/ui/scroll-area/scroll-area.svelte
tools/ui/src/lib/components/ui/select/index.ts
tools/ui/src/lib/components/ui/select/select-content.svelte
tools/ui/src/lib/components/ui/select/select-group-heading.svelte
tools/ui/src/lib/components/ui/select/select-item.svelte
tools/ui/src/lib/components/ui/select/select-label.svelte
tools/ui/src/lib/components/ui/select/select-scroll-down-button.svelte
tools/ui/src/lib/components/ui/select/select-scroll-up-button.svelte
tools/ui/src/lib/components/ui/select/select-separator.svelte
tools/ui/src/lib/components/ui/select/select-trigger.svelte
tools/ui/src/lib/components/ui/separator/separator.svelte
tools/ui/src/lib/components/ui/sheet/index.ts
tools/ui/src/lib/components/ui/sheet/sheet-content.svelte
tools/ui/src/lib/components/ui/sheet/sheet-description.svelte
tools/ui/src/lib/components/ui/sheet/sheet-footer.svelte
tools/ui/src/lib/components/ui/sheet/sheet-header.svelte
tools/ui/src/lib/components/ui/sheet/sheet-overlay.svelte
tools/ui/src/lib/components/ui/sheet/sheet-title.svelte
tools/ui/src/lib/components/ui/skeleton/skeleton.svelte
tools/ui/src/lib/components/ui/switch/switch.svelte
tools/ui/src/lib/components/ui/table/table-body.svelte
tools/ui/src/lib/components/ui/table/table-caption.svelte
tools/ui/src/lib/components/ui/table/table-cell.svelte
tools/ui/src/lib/components/ui/table/table-footer.svelte
tools/ui/src/lib/components/ui/table/table-head.svelte
tools/ui/src/lib/components/ui/table/table-header.svelte
tools/ui/src/lib/components/ui/table/table-row.svelte
tools/ui/src/lib/components/ui/table/table.svelte
tools/ui/src/lib/components/ui/textarea/textarea.svelte
tools/ui/src/lib/components/ui/tooltip/index.ts
tools/ui/src/lib/components/ui/tooltip/tooltip-content.svelte
tools/ui/src/lib/components/ui/utils.ts
tools/ui/src/lib/constants/agentic.ts
tools/ui/src/lib/constants/api-endpoints.ts
tools/ui/src/lib/constants/attachment-menu.ts
tools/ui/src/lib/constants/binary-detection.ts
tools/ui/src/lib/constants/built-in-tools.ts
tools/ui/src/lib/constants/chat-commands.ts
tools/ui/src/lib/constants/error.ts
tools/ui/src/lib/constants/icons.ts
tools/ui/src/lib/constants/mcp-resource.ts
tools/ui/src/lib/constants/mcp.ts
tools/ui/src/lib/constants/mention-badge.ts
tools/ui/src/lib/constants/model-loading.ts
tools/ui/src/lib/constants/pwa.ts
tools/ui/src/lib/constants/reasoning-effort-tokens.ts
tools/ui/src/lib/constants/reasoning-effort.ts
tools/ui/src/lib/constants/recommended-mcp-servers.ts
tools/ui/src/lib/constants/routes.ts
tools/ui/src/lib/constants/sandbox.ts
tools/ui/src/lib/constants/settings-keys.ts
tools/ui/src/lib/constants/settings-registry.ts
tools/ui/src/lib/constants/supported-file-types.ts
tools/ui/src/lib/constants/svg-blocks.ts
tools/ui/src/lib/constants/title-generation.ts
tools/ui/src/lib/constants/ui.ts
tools/ui/src/lib/constants/uri-template.ts
tools/ui/src/lib/constants/url.ts
tools/ui/src/lib/contexts/chat-actions.context.ts
tools/ui/src/lib/contexts/chat-settings-config.context.ts
tools/ui/src/lib/contexts/message-edit.context.ts
tools/ui/src/lib/hooks/use-attachment-menu.svelte.ts
tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts
tools/ui/src/lib/hooks/use-chat-form-pickers.svelte.ts
tools/ui/src/lib/hooks/use-chat-screen-active-model.svelte.ts
tools/ui/src/lib/hooks/use-chat-screen-drag-and-drop.svelte.ts
tools/ui/src/lib/hooks/use-chat-screen-file-upload.svelte.ts
tools/ui/src/lib/hooks/use-chat-screen-scroll.svelte.ts
tools/ui/src/lib/hooks/use-context-gauge.svelte.ts
tools/ui/src/lib/hooks/use-debounced-search.svelte.ts
tools/ui/src/lib/hooks/use-draft-messages.svelte.ts
tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts
tools/ui/src/lib/hooks/use-marquee-selection.svelte.ts
tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts
tools/ui/src/lib/hooks/use-models-selector.svelte.ts
tools/ui/src/lib/hooks/use-picker-navigation.svelte.ts
tools/ui/src/lib/hooks/use-processing-state.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-scroll-active-row.svelte.ts
tools/ui/src/lib/hooks/use-scroll-carousel.svelte.ts
tools/ui/src/lib/hooks/use-settings-navigation.svelte.ts
tools/ui/src/lib/hooks/use-tools-panel.svelte.ts
tools/ui/src/lib/services/chat.service.ts
tools/ui/src/lib/services/database.service.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/parameter-sync.service.spec.ts
tools/ui/src/lib/services/parameter-sync.service.ts
tools/ui/src/lib/services/props.service.ts
tools/ui/src/lib/services/sandbox-harness.ts
tools/ui/src/lib/services/sandbox.service.ts
tools/ui/src/lib/services/tools.service.ts
tools/ui/src/lib/stores/agentic.svelte.ts
tools/ui/src/lib/stores/build-info.svelte.ts
tools/ui/src/lib/stores/chat.svelte.ts
tools/ui/src/lib/stores/context-gauge-popup.svelte.ts
tools/ui/src/lib/stores/conversations.svelte.ts
tools/ui/src/lib/stores/device.svelte.ts
tools/ui/src/lib/stores/draft-messages.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/permissions.svelte.ts
tools/ui/src/lib/stores/persisted.svelte.ts
tools/ui/src/lib/stores/server.svelte.ts
tools/ui/src/lib/stores/settings.svelte.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/stores/version.svelte.ts
tools/ui/src/lib/types/agentic.d.ts
tools/ui/src/lib/types/api.d.ts
tools/ui/src/lib/types/chat.d.ts
tools/ui/src/lib/types/database.d.ts
tools/ui/src/lib/types/mcp.d.ts
tools/ui/src/lib/types/settings.d.ts
tools/ui/src/lib/types/tools.d.ts
tools/ui/src/lib/utils/abort.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/attachment-display.ts
tools/ui/src/lib/utils/audio-recording.ts
tools/ui/src/lib/utils/branching.ts
tools/ui/src/lib/utils/cache-ttl.ts
tools/ui/src/lib/utils/cap-img-size.ts
tools/ui/src/lib/utils/chat-template-thinking-detector.ts
tools/ui/src/lib/utils/clipboard.ts
tools/ui/src/lib/utils/code.ts
tools/ui/src/lib/utils/command-token.ts
tools/ui/src/lib/utils/compute-line-diff.ts
tools/ui/src/lib/utils/config-helpers.ts
tools/ui/src/lib/utils/contenteditable-tokenizer.ts
tools/ui/src/lib/utils/convert-files-to-extra.ts
tools/ui/src/lib/utils/file-preview.ts
tools/ui/src/lib/utils/file-type.ts
tools/ui/src/lib/utils/formatters.ts
tools/ui/src/lib/utils/glob-search.ts
tools/ui/src/lib/utils/headers.ts
tools/ui/src/lib/utils/heic-to-jpeg.ts
tools/ui/src/lib/utils/jpeg-orientation.ts
tools/ui/src/lib/utils/latex-protection.ts
tools/ui/src/lib/utils/mcp.ts
tools/ui/src/lib/utils/mention-badge.ts
tools/ui/src/lib/utils/mention-token.ts
tools/ui/src/lib/utils/modality-file-validation.ts
tools/ui/src/lib/utils/parse-exec-shell-error.ts
tools/ui/src/lib/utils/parse-exec-shell-status.ts
tools/ui/src/lib/utils/parse-partial-json-args.ts
tools/ui/src/lib/utils/path-display.ts
tools/ui/src/lib/utils/pdf-processing.ts
tools/ui/src/lib/utils/portal-to-body.ts
tools/ui/src/lib/utils/process-uploaded-files.ts
tools/ui/src/lib/utils/progress.ts
tools/ui/src/lib/utils/sanitize-svg.ts
tools/ui/src/lib/utils/sanitize.ts
tools/ui/src/lib/utils/search-results.ts
tools/ui/src/lib/utils/source-history.ts
tools/ui/src/lib/utils/sse.ts
tools/ui/src/lib/utils/stream-identity.ts
tools/ui/src/lib/utils/svg-shadow.ts
tools/ui/src/lib/utils/svg-to-png.ts
tools/ui/src/lib/utils/text-files.ts
tools/ui/src/lib/utils/text.ts
tools/ui/src/lib/utils/tool-call-meta.ts
tools/ui/src/lib/utils/uri-template.ts
tools/ui/src/lib/utils/url.ts
tools/ui/src/lib/utils/webp-to-png.ts
tools/ui/src/lib/utils/working-directory.ts
tools/ui/src/routes/(chat)/+page.svelte
tools/ui/src/routes/(chat)/chat/[id]/+page.svelte
tools/ui/src/routes/+error.svelte
tools/ui/src/routes/+layout.svelte
tools/ui/src/routes/search/+page.svelte
tools/ui/src/routes/settings/+layout.svelte
tools/ui/src/routes/settings/[[section]]/+page.svelte
tools/ui/svelte.config.js
tools/ui/tests/client/agentic-stream.perf.svelte.test.ts
tools/ui/tests/client/apikey-splash.svelte.test.ts
tools/ui/tests/client/cap-img-size.svelte.test.ts
tools/ui/tests/client/chat-form-contenteditable-blocks.svelte.test.ts
tools/ui/tests/client/chat-form-contenteditable-undo.svelte.test.ts
tools/ui/tests/client/chat-form-contenteditable.svelte.test.ts
tools/ui/tests/client/chat-form-enter-code-block.svelte.test.ts
tools/ui/tests/client/chat-form-mention-picker-gate.svelte.test.ts
tools/ui/tests/client/chat-form-slash-commands.svelte.test.ts
tools/ui/tests/client/collapsible-lazy-body.svelte.test.ts
tools/ui/tests/client/components/AgenticPerfWrapper.svelte
tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte
tools/ui/tests/client/components/ChatFormPickersHarness.svelte
tools/ui/tests/client/components/ChatFormTestWrapper.svelte
tools/ui/tests/client/components/ChatMessagesPerfWrapper.svelte
tools/ui/tests/client/components/CollapsibleLazyBodyHarness.svelte
tools/ui/tests/client/components/McpServerFormWrapper.svelte
tools/ui/tests/client/components/TestWrapper.svelte
tools/ui/tests/client/components/agentic-perf-state.svelte.ts
tools/ui/tests/client/conversation-import-db.svelte.test.ts
tools/ui/tests/client/mcp-display-name.svelte.test.ts
tools/ui/tests/client/mcp-server-form.svelte.test.ts
tools/ui/tests/client/page.svelte.test.ts
tools/ui/tests/client/picker-list-scroll.svelte.test.ts
tools/ui/tests/client/sandbox.service.svelte.test.ts
tools/ui/tests/client/settings-raw-text-migration.svelte.test.ts
tools/ui/tests/client/settings-registry-invariants.svelte.test.ts
tools/ui/tests/client/ui-settings-sync.svelte.test.ts
tools/ui/tests/client/update-message-in-place.svelte.test.ts
tools/ui/tests/e2e/pwa.e2e.ts
tools/ui/tests/stories/ChatMessage.stories.svelte
tools/ui/tests/stories/ChatScreenForm.stories.svelte
tools/ui/tests/stories/MarkdownContent.stories.svelte
tools/ui/tests/stories/ModelsSelector.stories.svelte
tools/ui/tests/stories/PwaRefreshAlert.stories.svelte
tools/ui/tests/stories/SidebarNavigation.stories.svelte
tools/ui/tests/stories/a11y/ActionIcon.a11y.stories.svelte
tools/ui/tests/stories/a11y/ChatMessageStatistics.a11y.stories.svelte
tools/ui/tests/stories/a11y/ChatScreenForm.a11y.stories.svelte
tools/ui/tests/stories/a11y/HorizontalScrollCarousel.a11y.stories.svelte
tools/ui/tests/stories/a11y/SidebarNavigationConversationItem.a11y.stories.svelte
tools/ui/tests/stories/fixtures/storybook-mocks.ts
tools/ui/tests/unit/abort.test.ts
tools/ui/tests/unit/agentic-hotpath.bench.ts
tools/ui/tests/unit/agentic-sections.test.ts
tools/ui/tests/unit/agentic-strip.test.ts
tools/ui/tests/unit/assistant-raw-output.test.ts
tools/ui/tests/unit/classify-tool-result.test.ts
tools/ui/tests/unit/clipboard.test.ts
tools/ui/tests/unit/code.test.ts
tools/ui/tests/unit/command-token.test.ts
tools/ui/tests/unit/compute-line-diff.test.ts
tools/ui/tests/unit/contenteditable-tokenizer.test.ts
tools/ui/tests/unit/contenteditable-word-jump.test.ts
tools/ui/tests/unit/continue-intent.test.ts
tools/ui/tests/unit/conversation-import.test.ts
tools/ui/tests/unit/favicon-colorize.test.ts
tools/ui/tests/unit/glob-search-children.test.ts
tools/ui/tests/unit/headers.test.ts
tools/ui/tests/unit/jpeg-orientation.test.ts
tools/ui/tests/unit/latex-protection.test.ts
tools/ui/tests/unit/mcp-default-overrides-merge.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/mention-badge.test.ts
tools/ui/tests/unit/mention-token.test.ts
tools/ui/tests/unit/model-id-parser.test.ts
tools/ui/tests/unit/model-names.test.ts
tools/ui/tests/unit/parse-exec-shell-status.test.ts
tools/ui/tests/unit/parse-mcp-server-settings.test.ts
tools/ui/tests/unit/parse-toolcalls-memo.test.ts
tools/ui/tests/unit/partial-tool-call-cleanup.test.ts
tools/ui/tests/unit/pwa.spec.ts
tools/ui/tests/unit/reasoning-context.test.ts
tools/ui/tests/unit/redact.test.ts
tools/ui/tests/unit/request-helpers.test.ts
tools/ui/tests/unit/sanitize-headers.test.ts
tools/ui/tests/unit/search-results-fixture.test.ts
tools/ui/tests/unit/search-results.test.ts
tools/ui/tests/unit/source-history.test.ts
tools/ui/tests/unit/sse.test.ts
tools/ui/tests/unit/stream-discovery.test.ts
tools/ui/tests/unit/stream-resume.test.ts
tools/ui/tests/unit/tool-call-meta.test.ts
tools/ui/tests/unit/tool-calls.test.ts
tools/ui/tests/unit/uri-template.test.ts
tools/ui/tests/unit/working-directory.test.ts
tools/ui/vite.config.ts
tools/ui/vitest-setup-client.ts

index fcbf7ee954899045b50efd0a8851f1ff17dea99a..c65484048e806ceae7fa5d7e5fab90c2c057ad2b 100644 (file)
@@ -1,14 +1,15 @@
 // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
-import storybook from 'eslint-plugin-storybook';
-
-import prettier from 'eslint-config-prettier';
+import svelteConfig from './svelte.config.js';
 import { includeIgnoreFile } from '@eslint/compat';
 import js from '@eslint/js';
+import prettier from 'eslint-config-prettier';
+import perfectionist from 'eslint-plugin-perfectionist';
+import simpleImportSort from 'eslint-plugin-simple-import-sort';
+import storybook from 'eslint-plugin-storybook';
 import svelte from 'eslint-plugin-svelte';
 import globals from 'globals';
 import { fileURLToPath } from 'node:url';
 import ts from 'typescript-eslint';
-import svelteConfig from './svelte.config.js';
 
 const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
 
@@ -21,32 +22,67 @@ export default ts.config(
        ...svelte.configs.prettier,
        {
                languageOptions: { globals: { ...globals.browser, ...globals.node } },
+               plugins: { perfectionist, 'simple-import-sort': simpleImportSort },
                rules: {
-                       // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
-                       // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
-                       'no-undef': 'off',
-                       'svelte/no-at-html-tags': 'off',
-                       // This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
-                       'svelte/no-navigation-without-resolve': 'off',
-
                        // Snippet bodies often ignore one or more of the parent's params
                        // (e.g. `{#snippet children(_meta, ctx)}` when only ctx is read).
                        '@typescript-eslint/no-unused-vars': [
                                'error',
                                { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
                        ],
-
                        // Enforce empty line at end of file
-                       'eol-last': 'error'
+                       'eol-last': 'error',
+                       // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
+                       // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
+                       'no-undef': 'off',
+
+                       'padding-line-between-statements': [
+                               'error',
+                               // Blank line between function/class declarations.
+                               { blankLine: 'always', next: ['function', 'class'], prev: ['function', 'class'] },
+                               // Blank line around if blocks (if/else and else if stay one statement).
+                               { blankLine: 'always', next: '*', prev: 'if' },
+                               { blankLine: 'always', next: 'if', prev: '*' },
+                               // Blank line after the last declaration in a group. Because the 'never'
+                               // rules below are scoped per declaration kind, a const group and a let
+                               // group get separated by a blank line, while same-kind declarations stay
+                               // together.
+                               { blankLine: 'always', next: '*', prev: ['const', 'let', 'var'] },
+                               // No blank line between consecutive declarations of the same kind (kept
+                               // last so each takes precedence over the always rule above for matching
+                               // declaration pairs).
+                               { blankLine: 'never', next: 'const', prev: 'const' },
+                               { blankLine: 'never', next: 'let', prev: 'let' },
+                               { blankLine: 'never', next: 'var', prev: 'var' },
+                               // Blank line before a statement that follows another statement in the block
+                               // (works for return/throw/break/continue). A blank line for a terminal
+                               // statement that opens a block body can't be enforced here: Prettier removes
+                               // the leading blank line of a block, so the two formatters would fight.
+                               { blankLine: 'always', next: ['return', 'throw', 'break', 'continue'], prev: '*' }
+                       ],
+
+                       'perfectionist/sort-objects': ['error', { type: 'natural' }],
+
+                       // Alphabetical order for variable declarations and object keys
+                       'perfectionist/sort-variable-declarations': ['error', { type: 'natural' }],
+
+                       // Sort imports alphabetically by module path, and sort named members within
+                       // each statement. A single catch-all group keeps the list flat (no blank-line
+                       // grouping); Prettier normalizes comma spacing afterwards.
+                       'simple-import-sort/imports': ['error', { groups: [['.*']] }],
+                       'svelte/no-at-html-tags': 'off',
+
+                       // This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
+                       'svelte/no-navigation-without-resolve': 'off'
                }
        },
        {
                files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
                languageOptions: {
                        parserOptions: {
-                               projectService: true,
                                extraFileExtensions: ['.svelte'],
                                parser: ts.parser,
+                               projectService: true,
                                svelteConfig
                        }
                }
index 976ed050f8378d118a14f17a489aad10e286edc1..f9b793b29f98c05619d8b4b76e2839f8c0577dd1 100644 (file)
@@ -39,6 +39,8 @@
                                "dompurify": "3.4.13",
                                "eslint": "9.39.4",
                                "eslint-config-prettier": "10.1.8",
+                               "eslint-plugin-perfectionist": "^5.10.1",
+                               "eslint-plugin-simple-import-sort": "^14.0.0",
                                "eslint-plugin-storybook": "10.5.6",
                                "eslint-plugin-svelte": "3.19.0",
                                "fflate": "0.8.3",
                                "eslint": ">=7.0.0"
                        }
                },
+               "node_modules/eslint-plugin-perfectionist": {
+                       "version": "5.10.1",
+                       "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.10.1.tgz",
+                       "integrity": "sha512-Kprsp9Us0GqAesYaAIzUViw57xYp5WBqzXrcE0Mtww++E5fexWXYBipMuuD7yvyH4vvpBH0+oJ+OMAmZ0oYXkw==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@typescript-eslint/utils": "^8.65.0",
+                               "natural-orderby": "^5.0.0"
+                       },
+                       "engines": {
+                               "node": "^20.0.0 || >=22.0.0"
+                       },
+                       "peerDependencies": {
+                               "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/project-service": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz",
+                       "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@typescript-eslint/tsconfig-utils": "^8.66.0",
+                               "@typescript-eslint/types": "^8.66.0",
+                               "debug": "^4.4.3"
+                       },
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       },
+                       "peerDependencies": {
+                               "typescript": ">=4.8.4 <6.1.0"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/scope-manager": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz",
+                       "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@typescript-eslint/types": "8.66.0",
+                               "@typescript-eslint/visitor-keys": "8.66.0"
+                       },
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/tsconfig-utils": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz",
+                       "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==",
+                       "dev": true,
+                       "license": "MIT",
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       },
+                       "peerDependencies": {
+                               "typescript": ">=4.8.4 <6.1.0"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/types": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz",
+                       "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==",
+                       "dev": true,
+                       "license": "MIT",
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/typescript-estree": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz",
+                       "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@typescript-eslint/project-service": "8.66.0",
+                               "@typescript-eslint/tsconfig-utils": "8.66.0",
+                               "@typescript-eslint/types": "8.66.0",
+                               "@typescript-eslint/visitor-keys": "8.66.0",
+                               "debug": "^4.4.3",
+                               "minimatch": "^10.2.2",
+                               "semver": "^7.7.3",
+                               "tinyglobby": "^0.2.15",
+                               "ts-api-utils": "^2.5.0"
+                       },
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       },
+                       "peerDependencies": {
+                               "typescript": ">=4.8.4 <6.1.0"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/utils": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz",
+                       "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@eslint-community/eslint-utils": "^4.9.1",
+                               "@typescript-eslint/scope-manager": "8.66.0",
+                               "@typescript-eslint/types": "8.66.0",
+                               "@typescript-eslint/typescript-estree": "8.66.0"
+                       },
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       },
+                       "peerDependencies": {
+                               "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+                               "typescript": ">=4.8.4 <6.1.0"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/@typescript-eslint/visitor-keys": {
+                       "version": "8.66.0",
+                       "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz",
+                       "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "@typescript-eslint/types": "8.66.0",
+                               "eslint-visitor-keys": "^5.0.0"
+                       },
+                       "engines": {
+                               "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+                       },
+                       "funding": {
+                               "type": "opencollective",
+                               "url": "https://opencollective.com/typescript-eslint"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/balanced-match": {
+                       "version": "4.0.4",
+                       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+                       "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+                       "dev": true,
+                       "license": "MIT",
+                       "engines": {
+                               "node": "18 || 20 || >=22"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/brace-expansion": {
+                       "version": "5.0.9",
+                       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+                       "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+                       "dev": true,
+                       "license": "MIT",
+                       "dependencies": {
+                               "balanced-match": "^4.0.2"
+                       },
+                       "engines": {
+                               "node": "20 || >=22"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/eslint-visitor-keys": {
+                       "version": "5.0.1",
+                       "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+                       "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+                       "dev": true,
+                       "license": "Apache-2.0",
+                       "engines": {
+                               "node": "^20.19.0 || ^22.13.0 || >=24"
+                       },
+                       "funding": {
+                               "url": "https://opencollective.com/eslint"
+                       }
+               },
+               "node_modules/eslint-plugin-perfectionist/node_modules/minimatch": {
+                       "version": "10.2.6",
+                       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+                       "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+                       "dev": true,
+                       "license": "BlueOak-1.0.0",
+                       "dependencies": {
+                               "brace-expansion": "^5.0.8"
+                       },
+                       "engines": {
+                               "node": "18 || 20 || >=22"
+                       },
+                       "funding": {
+                               "url": "https://github.com/sponsors/isaacs"
+                       }
+               },
+               "node_modules/eslint-plugin-simple-import-sort": {
+                       "version": "14.0.0",
+                       "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-14.0.0.tgz",
+                       "integrity": "sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==",
+                       "dev": true,
+                       "license": "MIT",
+                       "peerDependencies": {
+                               "eslint": ">=5.0.0"
+                       }
+               },
                "node_modules/eslint-plugin-storybook": {
                        "version": "10.5.6",
                        "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.6.tgz",
                        "dev": true,
                        "license": "MIT"
                },
+               "node_modules/natural-orderby": {
+                       "version": "5.0.0",
+                       "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz",
+                       "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==",
+                       "dev": true,
+                       "license": "MIT",
+                       "engines": {
+                               "node": ">=18"
+                       }
+               },
                "node_modules/negotiator": {
                        "version": "1.0.0",
                        "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
index 7f042f415ae822055c240b6e872bd278c874c495..f6d6880d7ae4a983d591802f243795a6121caa4a 100644 (file)
@@ -12,7 +12,7 @@
                "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
                "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
                "reset": "rm -rf .svelte-kit node_modules",
-               "format": "prettier --write .",
+               "format": "eslint --fix . && prettier --write .",
                "lint": "prettier --check . && eslint .",
                "test": "npm run test:ui -- --run && npm run test:client -- --run && npm run test:unit -- --run && npm run test:e2e",
                "test:e2e": "playwright test",
@@ -36,6 +36,7 @@
                "@playwright/test": "1.56.1",
                "@storybook/addon-a11y": "10.5.6",
                "@storybook/addon-docs": "10.5.6",
+               "@storybook/addon-mcp": "0.7.0",
                "@storybook/addon-svelte-csf": "5.1.2",
                "@storybook/addon-vitest": "10.5.6",
                "@storybook/sveltekit": "10.5.6",
@@ -57,6 +58,8 @@
                "dompurify": "3.4.13",
                "eslint": "9.39.4",
                "eslint-config-prettier": "10.1.8",
+               "eslint-plugin-perfectionist": "^5.10.1",
+               "eslint-plugin-simple-import-sort": "^14.0.0",
                "eslint-plugin-storybook": "10.5.6",
                "eslint-plugin-svelte": "3.19.0",
                "fflate": "0.8.3",
                "vite-plugin-devtools-json": "0.2.1",
                "vitest": "4.1.10",
                "vitest-browser-svelte": "2.1.1",
-               "workbox-window": "7.4.1",
-               "@storybook/addon-mcp": "0.7.0"
+               "workbox-window": "7.4.1"
        },
        "overrides": {
                "cookie": "1.1.1",
index 55bf3851404725cf694eb4076d6426f978e79cdf..057ed416df59adbf86040cb876d5bc30373f623b 100644 (file)
@@ -1,31 +1,31 @@
 import { defineConfig, devices } from '@playwright/test';
 
 export default defineConfig({
-       testDir: 'tests/e2e',
-       testMatch: ['**/*.e2e.ts'],
-       timeout: 30000,
        expect: {
                timeout: 5000
        },
-       fullyParallel: true,
        forbidOnly: !!process.env.CI,
-       retries: process.env.CI ? 2 : 0,
-       workers: process.env.CI ? 1 : undefined,
-       reporter: 'line',
-       use: {
-               baseURL: 'http://localhost:8181',
-               trace: 'on-first-retry'
-       },
+       fullyParallel: true,
        projects: [
                {
                        name: 'chromium',
                        use: { ...devices['Desktop Chrome'] }
                }
        ],
+       reporter: 'line',
+       retries: process.env.CI ? 2 : 0,
+       testDir: 'tests/e2e',
+       testMatch: ['**/*.e2e.ts'],
+       timeout: 30000,
+       use: {
+               baseURL: 'http://localhost:8181',
+               trace: 'on-first-retry'
+       },
        webServer: {
                command: 'npm run build && npx http-server ./dist -p 8181',
                port: 8181,
-               timeout: 120000,
-               reuseExistingServer: !process.env.CI
-       }
+               reuseExistingServer: !process.env.CI,
+               timeout: 120000
+       },
+       workers: process.env.CI ? 1 : undefined
 });
index 358c0ebc074ef4fbb32837f7cc3e767d6099d815..1446b47a82a4b67953b1d72cc334e15b855e3ed3 100644 (file)
@@ -1,6 +1,6 @@
-import { defineConfig } from '@vite-pwa/assets-generator/config';
-import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
 import { writeThemeFavicons } from './scripts/favicon-colorize';
+import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
+import { defineConfig } from '@vite-pwa/assets-generator/config';
 
 writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
        padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
@@ -10,18 +10,18 @@ export default defineConfig({
        headLinkOptions: {
                preset: '2023'
        },
+       images: ['static/favicon-dark.svg'],
        preset: {
-               transparent: {
-                       sizes: [],
-                       favicons: [[48, 'favicon-dark.ico']],
-                       padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
+               apple: {
+                       sizes: []
                },
                maskable: {
                        sizes: []
                },
-               apple: {
+               transparent: {
+                       favicons: [[48, 'favicon-dark.ico']],
+                       padding: PWA_ASSET_GENERATOR.FAVICON_PADDING,
                        sizes: []
                }
-       },
-       images: ['static/favicon-dark.svg']
+       }
 });
index b69884d94a9edcb70b4bc8dede79db71bf61eb38..5fed0a595743851d66916d3c8f48a8c5d7312f86 100644 (file)
@@ -1,3 +1,11 @@
+import { writeThemeFavicons } from './scripts/favicon-colorize';
+import {
+       FAVICON_COLORS,
+       PWA_ASSET_GENERATOR,
+       PWA_GENERATOR_DEVICES,
+       THEME_COLORS
+} from './src/lib/constants/pwa';
+import { SplashOrientation } from './src/lib/enums/splash.enums';
 import {
        combinePresetAndAppleSplashScreens,
        defineConfig,
@@ -5,14 +13,6 @@ import {
 } from '@vite-pwa/assets-generator/config';
 import { readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
-import {
-       THEME_COLORS,
-       PWA_GENERATOR_DEVICES,
-       PWA_ASSET_GENERATOR,
-       FAVICON_COLORS
-} from './src/lib/constants/pwa';
-import { SplashOrientation } from './src/lib/enums/splash.enums';
-import { writeThemeFavicons } from './scripts/favicon-colorize';
 
 writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
        padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
@@ -22,6 +22,7 @@ export default defineConfig({
        headLinkOptions: {
                preset: PWA_ASSET_GENERATOR.LINK_PRESET
        },
+       images: ['static/favicon.svg'],
        preset: combinePresetAndAppleSplashScreens(
                {
                        ...minimal2023Preset,
@@ -32,37 +33,37 @@ export default defineConfig({
                        }
                },
                {
-                       padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
-                       resizeOptions: {
-                               background: THEME_COLORS.BACKGROUND_LIGHT,
-                               fit: PWA_ASSET_GENERATOR.FIT_MODE
-                       },
-                       darkResizeOptions: {
-                               background: THEME_COLORS.BACKGROUND_DARK,
-                               fit: PWA_ASSET_GENERATOR.FIT_MODE
-                       },
                        darkImageResolver: async (imageName: string) => {
                                if (imageName.endsWith('favicon.svg')) {
                                        return readFileSync(resolve('static/favicon-dark.svg'));
                                }
                        },
+                       darkResizeOptions: {
+                               background: THEME_COLORS.BACKGROUND_DARK,
+                               fit: PWA_ASSET_GENERATOR.FIT_MODE
+                       },
                        linkMediaOptions: {
-                               log: true,
                                addMediaScreen: PWA_ASSET_GENERATOR.ADD_MEDIA_SCREEN,
                                basePath: PWA_ASSET_GENERATOR.BASE_PATH,
+                               log: true,
                                xhtml: PWA_ASSET_GENERATOR.XHTML
                        },
-                       png: {
-                               compressionLevel: PWA_ASSET_GENERATOR.PNG_COMPRESSION_LEVEL,
-                               quality: PWA_ASSET_GENERATOR.PNG_QUALITY
-                       },
                        name: (landscape, size, dark) => {
                                const orientation = landscape ? SplashOrientation.LANDSCAPE : SplashOrientation.PORTRAIT;
                                const darkPrefix = dark ? PWA_ASSET_GENERATOR.DARK_PREFIX : '';
+
                                return `apple-splash-${orientation}-${darkPrefix}${size.width}x${size.height}.png`;
+                       },
+                       padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
+                       png: {
+                               compressionLevel: PWA_ASSET_GENERATOR.PNG_COMPRESSION_LEVEL,
+                               quality: PWA_ASSET_GENERATOR.PNG_QUALITY
+                       },
+                       resizeOptions: {
+                               background: THEME_COLORS.BACKGROUND_LIGHT,
+                               fit: PWA_ASSET_GENERATOR.FIT_MODE
                        }
                },
                PWA_GENERATOR_DEVICES
-       ),
-       images: ['static/favicon.svg']
+       )
 });
index e1872b7774f7fe5289744529581e1862a247f2d3..54a951296a1ff6abf8e8aa9ed9231cee2b494ea4 100644 (file)
@@ -4,12 +4,10 @@ import { fileURLToPath } from 'node:url';
 
 const HERE = dirname(fileURLToPath(import.meta.url));
 const PROJECT_ROOT = resolve(HERE, '..');
-
 const DEFAULT_LOGO = resolve(PROJECT_ROOT, 'src/lib/assets/logo.svg');
 const DEFAULT_OUT_DIR = resolve(PROJECT_ROOT, 'static');
 const DEFAULT_OUT_LIGHT = resolve(DEFAULT_OUT_DIR, 'favicon.svg');
 const DEFAULT_OUT_DARK = resolve(DEFAULT_OUT_DIR, 'favicon-dark.svg');
-
 const CURRENT_COLOR = 'currentColor';
 
 export interface ColorizedFavicon {
@@ -39,8 +37,8 @@ export function colorizeFaviconSvg(
        darkColor: string
 ): ColorizedFavicon {
        return {
-               light: svg.replaceAll(CURRENT_COLOR, lightColor),
-               dark: svg.replaceAll(CURRENT_COLOR, darkColor)
+               dark: svg.replaceAll(CURRENT_COLOR, darkColor),
+               light: svg.replaceAll(CURRENT_COLOR, lightColor)
        };
 }
 
@@ -54,33 +52,40 @@ export function padFaviconSvg(svg: string, padding: number): string {
        if (!(padding > 0) || padding >= 1) return svg;
 
        const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
+
        if (!viewBoxMatch) return svg;
 
        const parts = viewBoxMatch[1]
                .trim()
                .split(/[\s,]+/)
                .map(Number);
+
        if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return svg;
 
        const [, , width, height] = parts;
+
        if (width <= 0 || height <= 0) return svg;
 
        const scale = 1 - padding;
        const translateX = (padding * width) / 2;
        const translateY = (padding * height) / 2;
-
        const openTagStart = svg.search(/<svg\b/i);
+
        if (openTagStart === -1) return svg;
+
        const openTagEnd = svg.indexOf('>', openTagStart);
+
        if (openTagEnd === -1) return svg;
+
        const closeStart = svg.lastIndexOf('</svg');
+
        if (closeStart === -1 || closeStart <= openTagEnd) return svg;
 
        const openTag = svg.slice(0, openTagEnd + 1);
        const inner = svg.slice(openTagEnd + 1, closeStart);
        const closeTag = svg.slice(closeStart);
-
        const group = `<g transform="translate(${translateX} ${translateY}) scale(${scale})">`;
+
        return `${openTag}${group}${inner}</g>${closeTag}`;
 }
 
@@ -93,14 +98,15 @@ export function writeThemeFavicons(
        lightColor: string,
        darkColor: string,
        {
-               sourcePath = DEFAULT_LOGO,
-               lightOutPath = DEFAULT_OUT_LIGHT,
                darkOutPath = DEFAULT_OUT_DARK,
-               padding = 0
+               lightOutPath = DEFAULT_OUT_LIGHT,
+               padding = 0,
+               sourcePath = DEFAULT_LOGO
        }: WriteThemeFaviconsOptions = {}
 ): void {
        const source = readFileSync(sourcePath, 'utf-8');
-       const { light, dark } = colorizeFaviconSvg(source, lightColor, darkColor);
+       const { dark, light } = colorizeFaviconSvg(source, lightColor, darkColor);
+
        mkdirSync(dirname(lightOutPath), { recursive: true });
        writeFileSync(lightOutPath, padFaviconSvg(light, padding));
        writeFileSync(darkOutPath, padFaviconSvg(dark, padding));
index 7dfd6521e5761702ccba22da85b65ee16bda84c3..b4763c6256b63736bc8a325ed96a9a22861f9ae3 100644 (file)
  * maskable-icon and apple-touch-icon are left untouched.
  */
 
-import sharp from 'sharp';
 import fs from 'fs';
 import path from 'path';
+import sharp from 'sharp';
 import { fileURLToPath } from 'url';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
-
 const STATIC_DIR = path.resolve(__dirname, '..', 'static');
-
 const paddingPct = process.argv.reduce((acc, arg, i, args) => {
        if (arg === '--padding-pct' && args[i + 1]) return parseFloat(args[i + 1]);
+
        return acc;
 }, 0);
-
 // Scale down the source image before cropping to circle
 const scalePct = process.argv.reduce((acc, arg, i, args) => {
        if (arg === '--scale-pct' && args[i + 1]) return parseFloat(args[i + 1]);
+
        return acc;
 }, 85); // default 85% - icon fills 85% of the circular area
-
 // Source for circular icons: the maskable icon (white bg, full logo)
 const sourceIcon = 'maskable-icon-512x512.png';
 const targetIcons = ['pwa-64x64.png', 'pwa-192x192.png', 'pwa-512x512.png'];
-
 // maskable-icon and apple-touch-icon stay square
 const untouchedIcons = ['maskable-icon-512x512.png', 'apple-touch-icon-180x180.png'];
 
@@ -47,10 +44,13 @@ async function makeCircle(targetFilename) {
 
        if (!fs.existsSync(sourcePath)) {
                console.log(`⏭️  ${sourceIcon} not found, skipping`);
+
                return;
        }
+
        if (!fs.existsSync(targetPath)) {
                console.log(`⏭️  ${targetFilename} not found, skipping`);
+
                return;
        }
 
@@ -58,16 +58,18 @@ async function makeCircle(targetFilename) {
        const size = Math.max(metadata.width, metadata.height);
        const radius = Math.floor((size * (1 - paddingPct / 100)) / 2);
        const center = Math.floor(size / 2);
-
        // Build circular mask as RGBA buffer: white opaque circle on transparent bg
        const maskBuf = Buffer.alloc(size * size * 4, 0);
+
        for (let y = 0; y < size; y++) {
                for (let x = 0; x < size; x++) {
                        const dx = x - center;
                        const dy = y - center;
                        const dist = Math.sqrt(dx * dx + dy * dy);
+
                        if (dist < radius) {
                                const i = (y * size + x) * 4;
+
                                maskBuf[i] = 255;
                                maskBuf[i + 1] = 255;
                                maskBuf[i + 2] = 255;
@@ -77,8 +79,9 @@ async function makeCircle(targetFilename) {
        }
 
        const tmpMask = path.join(STATIC_DIR, '.mask-tmp.png');
+
        await sharp(maskBuf, {
-               raw: { width: size, height: size, channels: 4 }
+               raw: { channels: 4, height: size, width: size }
        })
                .png()
                .toFile(tmpMask);
@@ -87,28 +90,26 @@ async function makeCircle(targetFilename) {
        const circleDiameter = Math.floor(size * (1 - paddingPct / 100));
        const scaledSize = Math.floor((circleDiameter * scalePct) / 100);
        const offset = Math.floor((size - scaledSize) / 2);
-
        const scaledBuf = await sharp(sourcePath)
                .resize(scaledSize, scaledSize, {
-                       fit: 'cover',
-                       background: { r: 255, g: 255, b: 255, alpha: 1 }
+                       background: { alpha: 1, b: 255, g: 255, r: 255 },
+                       fit: 'cover'
                })
                .ensureAlpha()
                .png()
                .toBuffer();
-
        // Step 2: Composite scaled image onto white background, then apply circular mask
        const output = await sharp({
                create: {
-                       width: size,
-                       height: size,
+                       background: { alpha: 1, b: 255, g: 255, r: 255 },
                        channels: 4,
-                       background: { r: 255, g: 255, b: 255, alpha: 1 }
+                       height: size,
+                       width: size
                }
        })
                .composite([
-                       { input: scaledBuf, top: offset, left: offset },
-                       { input: tmpMask, top: 0, left: 0, blend: 'dest-in' }
+                       { input: scaledBuf, left: offset, top: offset },
+                       { blend: 'dest-in', input: tmpMask, left: 0, top: 0 }
                ])
                .png()
                .toBuffer();
@@ -130,6 +131,7 @@ async function main() {
        console.log('\nUnchanged:');
        for (const icon of untouchedIcons) {
                const fp = path.join(STATIC_DIR, icon);
+
                console.log(`  ${icon} (${fs.existsSync(fp) ? fs.statSync(fp).size + ' bytes' : 'missing'})`);
        }
 }
index 972ba3b664c85586a1617191ee9bc9cdfd954a13..80023863057b24e20d1dac09d6e650045c04a6a3 100644 (file)
@@ -1,7 +1,7 @@
-import { writeFileSync, existsSync } from 'node:fs';
+import { BUILD_CONFIG } from '../src/lib/constants/pwa';
+import { existsSync, writeFileSync } from 'node:fs';
 import { resolve } from 'path';
 import type { Plugin } from 'vite';
-import { BUILD_CONFIG } from '../src/lib/constants/pwa';
 
 let processed = false;
 
@@ -15,27 +15,29 @@ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
  */
 export function buildInfoPlugin(): Plugin {
        return {
-               name: 'llamacpp:build-info',
                apply: 'build',
                closeBundle() {
                        setTimeout(() => {
                                try {
                                        if (processed) return;
+
                                        processed = true;
 
                                        const buildNumber = process.env.LLAMA_BUILD_NUMBER || 'b0000';
-
                                        const outDir = resolve(OUTPUT_DIR);
                                        const indexPath = resolve(outDir, 'index.html');
+
                                        if (!existsSync(indexPath)) return;
 
                                        const buildJsonPath = resolve(outDir, 'build.json');
+
                                        writeFileSync(buildJsonPath, JSON.stringify({ version: buildNumber }), 'utf-8');
                                        console.log(`Created build.json (version: ${buildNumber})`);
                                } catch (error) {
                                        console.error('Failed to write build.json:', error);
                                }
                        }, 100);
-               }
+               },
+               name: 'llamacpp:build-info'
        };
 }
index 218c2fa233d043fd449980351339aeb57178dca7..84e463c6d764b8cda2bd66eaf2442b97c6ef74b6 100644 (file)
@@ -4,7 +4,6 @@ import { fileURLToPath } from 'url';
 import type { Plugin } from 'vite';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
-
 const VENDORS_DIR = resolve(__dirname, '../src/lib/vendors');
 const VIRTUAL_ID = 'virtual:nerdamer';
 const RESOLVED_ID = '\0' + VIRTUAL_ID;
@@ -21,29 +20,32 @@ export function nerdamerPlugin(): Plugin {
        let bundled: string | null = null;
 
        return {
-               name: 'llamacpp:nerdamer',
-               resolveId(id) {
-                       return id === VIRTUAL_ID ? RESOLVED_ID : undefined;
-               },
                async load(id) {
                        if (id !== RESOLVED_ID) return undefined;
+
                        if (bundled === null) {
                                const result = await build({
-                                       entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')],
-                                       bundle: true,
-                                       minify: true,
-                                       format: 'iife',
-                                       globalName: 'nerdamer',
                                        alias: {
                                                'big-integer': resolve(VENDORS_DIR, 'big-integer/BigInteger.js'),
                                                'decimal.js': resolve(VENDORS_DIR, 'decimal.js/decimal.js')
                                        },
-                                       write: false,
-                                       logLevel: 'silent'
+                                       bundle: true,
+                                       entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')],
+                                       format: 'iife',
+                                       globalName: 'nerdamer',
+                                       logLevel: 'silent',
+                                       minify: true,
+                                       write: false
                                });
+
                                bundled = result.outputFiles[0].text;
                        }
+
                        return `export default ${JSON.stringify(bundled)};`;
+               },
+               name: 'llamacpp:nerdamer',
+               resolveId(id) {
+                       return id === VIRTUAL_ID ? RESOLVED_ID : undefined;
                }
        };
 }
index ce2f1b6e9fabe1fbb3a806b906307d8fd71e563c..f8eac1d66555e8665659ae1288342a5a3e433e9b 100644 (file)
@@ -1,7 +1,7 @@
-import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { BUILD_CONFIG } from '../src/lib/constants/pwa';
+import { existsSync, readFileSync, writeFileSync } from 'node:fs';
 import { resolve } from 'path';
 import type { Plugin } from 'vite';
-import { BUILD_CONFIG } from '../src/lib/constants/pwa';
 
 let processed = false;
 
@@ -11,11 +11,15 @@ function rewrite(path: string, pairs: [string, string][]): void {
        if (!existsSync(path)) {
                return;
        }
+
        const text = readFileSync(path, 'utf-8');
+
        let out = text;
+
        for (const [from, to] of pairs) {
                out = out.split(from).join(to);
        }
+
        if (out !== text) {
                writeFileSync(path, out, 'utf-8');
        }
@@ -32,12 +36,12 @@ function rewrite(path: string, pairs: [string, string][]): void {
  */
 export function relativizeBasePlugin(): Plugin {
        return {
-               name: 'llamacpp:relativize-base',
                apply: 'build',
                closeBundle() {
                        setTimeout(() => {
                                try {
                                        if (processed) return;
+
                                        processed = true;
 
                                        const outDir = resolve(OUTPUT_DIR);
@@ -56,6 +60,7 @@ export function relativizeBasePlugin(): Plugin {
                                        console.error('Failed to relativize base refs:', error);
                                }
                        }, 100);
-               }
+               },
+               name: 'llamacpp:relativize-base'
        };
 }
index 059ce4920bc5f46232561881a0d7eea5dd484885..45d931bab36c5710b8692fa9044cb013c032adae 100644 (file)
@@ -1,10 +1,10 @@
-import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
-import { resolve } from 'path';
-import type { Plugin } from 'vite';
-import { TAB, NEWLINE } from '../src/lib/constants/code';
+import { NEWLINE, TAB } from '../src/lib/constants/code';
 import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
-import type { SplashDimensions } from '../src/lib/types';
 import { SplashOrientation } from '../src/lib/enums/splash.enums';
+import type { SplashDimensions } from '../src/lib/types';
+import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { resolve } from 'path';
+import type { Plugin } from 'vite';
 
 let processed = false;
 
@@ -16,23 +16,26 @@ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
  */
 export function generateSplashScreenLinks(outDir: string): string[] {
        const files = readdirSync(outDir).filter((f) => f.match(REGEX_PATTERNS.SPLASH_FILE));
+
        if (files.length === 0) return [];
 
        const dimMap = new Map<string, SplashDimensions>();
+
        for (const [dims, spec] of Object.entries(APPLE_DEVICES)) {
                const [w, h] = dims.split('x').map(Number);
+
                // logical-point dimensions
-               dimMap.set(`${w}x${h}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
-               dimMap.set(`${h}x${w}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
+               dimMap.set(`${w}x${h}`, { deviceH: spec.height, deviceW: spec.width, dpr: spec.dpr });
+               dimMap.set(`${h}x${w}`, { deviceH: spec.height, deviceW: spec.width, dpr: spec.dpr });
                // pixel dimensions (used by actual generated splash files)
                dimMap.set(`${w * spec.dpr}x${h * spec.dpr}`, {
-                       deviceW: spec.width,
                        deviceH: spec.height,
+                       deviceW: spec.width,
                        dpr: spec.dpr
                });
                dimMap.set(`${h * spec.dpr}x${w * spec.dpr}`, {
-                       deviceW: spec.width,
                        deviceH: spec.height,
+                       deviceW: spec.width,
                        dpr: spec.dpr
                });
        }
@@ -42,20 +45,23 @@ export function generateSplashScreenLinks(outDir: string): string[] {
 
        for (const file of files) {
                const match = file.match(REGEX_PATTERNS.SPLASH_FILE);
+
                if (!match) continue;
+
                const orientation = match[1] as SplashOrientation;
                const isDark = !!match[2];
                const pixelW = parseInt(match[3]);
                const pixelH = parseInt(match[4]);
-
                const key = `${pixelW}x${pixelH}`;
                const spec = dimMap.get(key);
+
                if (!spec) {
                        console.warn(`Unknown splash screen dimensions: ${key} (${file})`);
+
                        continue;
                }
 
-               const { deviceW, deviceH, dpr } = spec;
+               const { deviceH, deviceW, dpr } = spec;
                const media = `screen and (device-width: ${deviceW}px) and (device-height: ${deviceH}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: ${orientation})`;
                const href = `./${file}`;
 
@@ -73,16 +79,17 @@ export function generateSplashScreenLinks(outDir: string): string[] {
 
 export function splashScreenPlugin(): Plugin {
        return {
-               name: 'llamacpp:splash-screen',
                apply: 'build',
                closeBundle() {
                        setTimeout(() => {
                                try {
                                        if (processed) return;
+
                                        processed = true;
 
                                        const outDir = resolve(OUTPUT_DIR);
                                        const indexPath = resolve(outDir, 'index.html');
+
                                        if (!existsSync(indexPath)) return;
 
                                        let content = readFileSync(indexPath, 'utf-8');
@@ -91,9 +98,11 @@ export function splashScreenPlugin(): Plugin {
                                        // The @vite-pwa/assets-generator generates apple-splash-*.png files;
                                        // this scans them and creates the <link> tags SvelteKit needs.
                                        const splashLinks = generateSplashScreenLinks(outDir);
+
                                        if (splashLinks.length > 0) {
                                                console.log(`Generated ${splashLinks.length} apple-splash link tags`);
                                                const splashHtml = splashLinks.map((l) => TAB + TAB + l).join(NEWLINE);
+
                                                content = content.replace(
                                                        REGEX_PATTERNS.HEAD_CLOSE,
                                                        splashHtml + NEWLINE + TAB + TAB + '</head>'
@@ -110,6 +119,7 @@ export function splashScreenPlugin(): Plugin {
                                        console.error('Failed to process build output:', error);
                                }
                        }, 100);
-               }
+               },
+               name: 'llamacpp:splash-screen'
        };
 }
index 814fcf5231d4c69882755b38891e368b7bf1fef7..6f26214e1c6ee8046b5282831ff1769b93192d16 100644 (file)
@@ -3,9 +3,8 @@
 
 import 'vite-plugin-pwa/pwa-assets';
 import 'vite-plugin-pwa/svelte';
-
+import { ModelModality, ServerModelStatus, ServerRole } from '$lib/enums';
 // Import chat types from dedicated module
-
 import type {
        // API types
        ApiChatCompletionRequest,
@@ -13,59 +12,57 @@ import type {
        ApiChatCompletionStreamChunk,
        ApiChatCompletionToolCall,
        ApiChatCompletionToolCallDelta,
-       ApiChatMessageData,
        ApiChatMessageContentPart,
+       ApiChatMessageData,
        ApiContextSizeError,
        ApiErrorResponse,
        ApiLlamaCppServerProps,
        ApiModelDataEntry,
+       ApiModelListResponse,
        ApiModelLoadStage,
-       ApiModelsSseProgress,
        ApiModelsSseData,
        ApiModelsSseEvent,
-       ApiModelListResponse,
+       ApiModelsSseProgress,
        ApiProcessingState,
        ApiRouterModelMeta,
+       ApiRouterModelsListResponse,
        ApiRouterModelsLoadRequest,
        ApiRouterModelsLoadResponse,
        ApiRouterModelsStatusRequest,
        ApiRouterModelsStatusResponse,
-       ApiRouterModelsListResponse,
        ApiRouterModelsUnloadRequest,
        ApiRouterModelsUnloadResponse,
        // Chat types
        ChatAttachmentDisplayItem,
+       ChatMessagePromptProgress,
+       ChatMessageSiblingInfo,
+       ChatMessageTimings,
        ChatMessageType,
        ChatRole,
        ChatUploadedFile,
-       ChatMessageSiblingInfo,
-       ChatMessagePromptProgress,
-       ChatMessageTimings,
        // Database types
        DatabaseConversation,
        DatabaseMessage,
        DatabaseMessageExtra,
        DatabaseMessageExtraAudioFile,
-       DatabaseMessageExtraVideoFile,
        DatabaseMessageExtraImageFile,
-       DatabaseMessageExtraTextFile,
-       DatabaseMessageExtraPdfFile,
        DatabaseMessageExtraLegacyContext,
+       DatabaseMessageExtraPdfFile,
+       DatabaseMessageExtraTextFile,
+       DatabaseMessageExtraVideoFile,
        ExportedConversation,
        ExportedConversations,
+       ModelLoadProgress,
        // Model types
        ModelModalities,
        ModelOption,
-       ModelLoadProgress,
        // Settings types
        SettingsChatServiceOptions,
+       SettingsConfigType,
        SettingsConfigValue,
-       SettingsFieldConfig,
-       SettingsConfigType
+       SettingsFieldConfig
 } from '$lib/types';
 
-import { ServerRole, ServerModelStatus, ModelModality } from '$lib/enums';
-
 declare global {
        // namespace App {
        // interface Error {}
index 608ff6fab4b24b14c408faabd84082bdb96fcf70..e29b5ad67dcadbb1be961c5f45fb74235684ff63 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { Button, type ButtonVariant, type ButtonSize } from '$lib/components/ui/button';
+       import { Button, type ButtonSize, type ButtonVariant } from '$lib/components/ui/button';
        import * as Tooltip from '$lib/components/ui/tooltip';
-       import type { Component } from 'svelte';
        import { TooltipSide } from '$lib/enums';
+       import type { Component } from 'svelte';
 
        interface Props {
                ariaLabel?: string;
        }
 
        let {
-               icon,
-               tooltip,
-               variant = 'ghost',
-               href = '',
-               size = 'sm',
+               ariaLabel,
                class: className = '',
                disabled = false,
+               href = '',
+               icon,
                iconSize = 'h-3 w-3',
-               tooltipSide = TooltipSide.TOP,
-               stopPropagationOnClick = false,
                onclick,
-               ariaLabel
+               size = 'sm',
+               stopPropagationOnClick = false,
+               tooltip,
+               tooltipSide = TooltipSide.TOP,
+               variant = 'ghost'
        }: Props = $props();
 
        let innerWidth = $state(0);
index 9b7b370ad0859eab35d9d4289f7194ecafeca4ca..7655f18e804e6ab4caaf2cd1d0b9f88d32c6ba6d 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       import ActionIcon from './ActionIcon.svelte';
        import { Copy } from '@lucide/svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { copyToClipboard } from '$lib/utils';
-       import ActionIcon from './ActionIcon.svelte';
 
        export let ariaLabel: string = 'Copy to clipboard';
        export let canCopy: boolean = true;
index d87184ea9bc8cef5ef9d4b359408604e720af5a9..4eb3e7838d18aa7c0b5dc58c72b3b0fa1d4e517b 100644 (file)
@@ -7,7 +7,7 @@
                class?: string;
        }
 
-       let { modalities, class: className = '' }: Props = $props();
+       let { class: className = '', modalities }: Props = $props();
 </script>
 
 {#each modalities as modality (modality)}
index e74bd8456a5484b83af6ad7e95389c71a6605bc6..36895c8e7991c9ffbde1e7135ebfeaec3d1cd035 100644 (file)
        }
 
        let {
-               class: className = '',
-               style = '',
+               activeModelId,
                attachments = [],
-               readonly = false,
-               onFileRemove,
-               uploadedFiles = $bindable([]),
+               class: className = '',
                // Default to small size for form previews
                imageClass = '',
                imageHeight = 'h-24',
                imageWidth = 'w-auto',
                limitToSingleRow = false,
-               activeModelId
+               onFileRemove,
+               readonly = false,
+               style = '',
+               uploadedFiles = $bindable([])
        }: Props = $props();
 
        let carouselRef: HorizontalScrollCarousel | undefined = $state();
@@ -48,7 +48,7 @@
        let previewFocusIndex = $state(0);
        let viewAllDialogOpen = $state(false);
 
-       let displayItems = $derived(getAttachmentDisplayItems({ uploadedFiles, attachments }));
+       let displayItems = $derived(getAttachmentDisplayItems({ attachments, uploadedFiles }));
 
        function openPreview(item: ChatAttachmentDisplayItem, event?: MouseEvent) {
                event?.stopPropagation();
index 143621cd9daaa5f9526cc73970bded52e5959431..ba06e18159fabe22d430996c078384c55d3977c5 100644 (file)
@@ -2,8 +2,8 @@
        import {
                ChatAttachmentsListItemMcpPrompt,
                ChatAttachmentsListItemMcpResource,
-               ChatAttachmentsListItemThumbnailImage,
-               ChatAttachmentsListItemThumbnailFile
+               ChatAttachmentsListItemThumbnailFile,
+               ChatAttachmentsListItemThumbnailImage
        } from '$lib/components/app';
        import { AttachmentType } from '$lib/enums';
        import type {
                return {
                        id,
                        resource: {
-                               uri: extra.uri,
                                name: extra.name,
+                               serverName: extra.serverName,
                                title: extra.name,
-                               serverName: extra.serverName
+                               uri: extra.uri
                        }
                };
        }
                        ? (item.attachment as DatabaseMessageExtraMcpPrompt)
                        : item.uploadedFile?.mcpPrompt
                                ? {
-                                               type: AttachmentType.MCP_PROMPT as const,
+                                               arguments: item.uploadedFile.mcpPrompt.arguments,
+                                               content: item.textContent ?? '',
                                                name: item.name,
-                                               serverName: item.uploadedFile.mcpPrompt.serverName,
                                                promptName: item.uploadedFile.mcpPrompt.promptName,
-                                               content: item.textContent ?? '',
-                                               arguments: item.uploadedFile.mcpPrompt.arguments
+                                               serverName: item.uploadedFile.mcpPrompt.serverName,
+                                               type: AttachmentType.MCP_PROMPT as const
                                        }
                                : null}
        {#if mcpPrompt}
index 636e93f221111a1fab2b2f2e5c490248c1cb50d4..f5452aade2576fc16f1fc2c0380f889ecc9e5b9e 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ChatMessageMcpPromptContent, ActionIcon } from '$lib/components/app';
        import { X } from '@lucide/svelte';
-       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
+       import { ActionIcon, ChatMessageMcpPromptContent } from '$lib/components/app';
        import { McpPromptVariant } from '$lib/enums';
+       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
 
        interface Props {
                class?: string;
index 6e1f639fa2d3decc9c2cf308b310daee89ae8e68..486b15b3b124d8db3e075a0af562f59202bd7515 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { Loader2, AlertCircle } from '@lucide/svelte';
+       import { AlertCircle, Loader2 } from '@lucide/svelte';
+       import { X } from '@lucide/svelte';
+       import { ActionIcon } from '$lib/components/app';
+       import * as Tooltip from '$lib/components/ui/tooltip';
        import { mcpStore } from '$lib/stores/mcp.svelte';
        import type { MCPResourceAttachment } from '$lib/types';
-       import * as Tooltip from '$lib/components/ui/tooltip';
-       import { ActionIcon } from '$lib/components/app';
-       import { X } from '@lucide/svelte';
-       import { getResourceIcon, getResourceDisplayName } from '$lib/utils';
+       import { getResourceDisplayName, getResourceIcon } from '$lib/utils';
 
        interface Props {
                attachment: MCPResourceAttachment;
@@ -24,6 +24,7 @@
 
        function getStatusClass(attachment: MCPResourceAttachment): string {
                if (attachment.error) return 'border-red-500/50 bg-red-500/10';
+
                if (attachment.loading) return 'border-border/50 bg-muted/30';
 
                return 'border-border/50 bg-muted/30';
index 63d0a715a1f3d754cc5828022c40a07e1faf3de9..da8dd3b8689ffe9617953923414662b07f515a94 100644 (file)
@@ -1,17 +1,17 @@
 <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 { X, Music, Video } from '@lucide/svelte';
+       import { AttachmentType } from '$lib/enums';
        import {
                formatFileSize,
                getFileTypeLabel,
                getPreviewText,
-               isPdfFile,
                isAudioFile,
-               isVideoFile,
-               isTextFile
+               isPdfFile,
+               isTextFile,
+               isVideoFile
        } from '$lib/utils';
-       import { ActionIcon } from '$lib/components/app';
-       import { AttachmentType } from '$lib/enums';
 
        interface Props {
                attachment?: DatabaseMessageExtra;
@@ -31,9 +31,9 @@
                attachment,
                class: className = '',
                id,
+               name,
                onclick,
                onRemove,
-               name,
                readonly = false,
                size,
                textContent,
index de080f5b779120242f11b0b21bad4b7908305881..a71e23a836be529656deb1f93ab0a6ad275cb221 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { ActionIcon } from '$lib/components/app';
        import { X } from '@lucide/svelte';
+       import { ActionIcon } from '$lib/components/app';
 
        interface Props {
                class?: string;
@@ -20,9 +20,9 @@
                height = 'h-16',
                id,
                imageClass = '',
+               name,
                onclick,
                onRemove,
-               name,
                preview,
                readonly = false,
                width = 'w-auto'
index cba323f2c370b1e593d570682479e4d8e12b822d..c161e5524796cdd42180846b8d900b763949de3e 100644 (file)
                getAttachmentDisplayItems,
                getLanguageFromFilename,
                isAudioFile,
-               isVideoFile,
                isImageFile,
                isMcpPrompt,
                isMcpResource,
                isPdfFile,
-               isTextFile
+               isTextFile,
+               isVideoFile
        } from '$lib/utils';
 
        interface PreviewItem {
        }
 
        let {
-               uploadedFiles = [],
-               attachments = [],
                activeModelId,
+               attachments = [],
                class: className = '',
-               previewFocusIndex = 0
+               previewFocusIndex = 0,
+               uploadedFiles = []
        }: Props = $props();
 
        let allItems = $derived(
-               getAttachmentDisplayItems({ uploadedFiles, attachments })
+               getAttachmentDisplayItems({ attachments, uploadedFiles })
                        .filter((item) => !isMcpPrompt(item) && !isMcpResource(item))
                        .map(
                                (item): PreviewItem => ({
                                        ...item,
-                                       isImage: isImageFile(item.attachment, item.uploadedFile),
                                        isAudio: isAudioFile(item.attachment, item.uploadedFile),
+                                       isImage: isImageFile(item.attachment, item.uploadedFile),
                                        isVideo: isVideoFile(item.attachment, item.uploadedFile)
                                })
                        )
 
        $effect(() => {
                const index = currentIndex;
+
                setTimeout(() => {
                        const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
 
-                       thumbnail?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
+                       thumbnail?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
                }, 0);
        });
 
index 30e84812aaa2b97c583c0aec890fdce003a5ffd0..c0d7cbd30de16a100bec72c0208f552cbb0da0c0 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import type { ChatAttachmentDisplayItem } from '$lib/types';
-       import { Image, Music, Video, FileText, FileIcon } from '@lucide/svelte';
-       import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
-       import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
        import ChatAttachmentsPreviewCurrentItemAudio from './ChatAttachmentsPreviewCurrentItemAudio.svelte';
-       import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
+       import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
+       import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
        import ChatAttachmentsPreviewCurrentItemText from './ChatAttachmentsPreviewCurrentItemText.svelte';
        import ChatAttachmentsPreviewCurrentItemUnavailable from './ChatAttachmentsPreviewCurrentItemUnavailable.svelte';
+       import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
+       import { FileIcon, FileText, Image, Music, Video } from '@lucide/svelte';
+       import type { ChatAttachmentDisplayItem } from '$lib/types';
 
        interface Props {
                currentItem: ChatAttachmentDisplayItem | null;
        }
 
        let {
+               activeModelId,
+               audioSrc,
                currentItem,
-               isImage,
+               displayPreview,
+               displayTextContent,
+               hasVisionModality,
                isAudio,
-               isVideo,
+               isImage,
                isPdf,
                isText,
-               displayPreview,
-               displayTextContent,
-               audioSrc,
-               videoSrc,
+               isVideo,
                language,
-               hasVisionModality,
-               activeModelId
+               videoSrc
        }: Props = $props();
 
        let IconComponent = $derived(
index 06e1f5928c5826276ad6a91570c6a21ee7d513b7..ace69b8181142aac213d8aa11da253091e186899 100644 (file)
@@ -6,7 +6,7 @@
                audioSrc: string | null;
        }
 
-       let { currentItem, audioSrc }: Props = $props();
+       let { audioSrc, currentItem }: Props = $props();
 </script>
 
 <div class="flex flex-1 items-center justify-center p-8">
index 7c7cf5120e1adf472b8e268a51ef31f12740d951..69b1724fb22c05a74a22b9bdb648e29195bdd4fe 100644 (file)
@@ -1,13 +1,13 @@
 <script lang="ts">
+       import { Eye, FileText, Info } from '@lucide/svelte';
+       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 { PdfViewMode } from '$lib/enums';
        import type { ChatAttachmentDisplayItem } from '$lib/types';
-       import { FileText, Eye, Info } from '@lucide/svelte';
-       import { Button } from '$lib/components/ui/button';
-       import * as Alert from '$lib/components/ui/alert';
-       import { SyntaxHighlightedCode } from '$lib/components/app';
        import { getLanguageFromFilename } from '$lib/utils';
        import { convertPDFToImage } from '$lib/utils/browser-only';
-       import { PdfViewMode } from '$lib/enums';
 
        interface Props {
                currentItem: ChatAttachmentDisplayItem | null;
@@ -17,7 +17,7 @@
                activeModelId?: string;
        }
 
-       let { currentItem, displayName, displayTextContent, hasVisionModality, activeModelId }: Props =
+       let { activeModelId, currentItem, displayName, displayTextContent, hasVisionModality }: Props =
                $props();
 
        let pdfViewMode = $state<PdfViewMode>(PdfViewMode.PAGES);
@@ -47,6 +47,7 @@
                                        currentItem.attachment.images.length > 0
                                ) {
                                        pdfImages = currentItem.attachment.images;
+
                                        return;
                                }
 
                                        const base64Data = currentItem.attachment.base64Data;
                                        const byteCharacters = atob(base64Data);
                                        const byteNumbers = new Array(byteCharacters.length);
+
                                        for (let i = 0; i < byteCharacters.length; i++) {
                                                byteNumbers[i] = byteCharacters.charCodeAt(i);
                                        }
                                        const byteArray = new Uint8Array(byteNumbers);
+
                                        file = new File([byteArray], displayName, { type: 'application/pdf' });
                                }
                        }
index a57e3145a9d67fbbcae82caa43bc886124232798..375a671687bc960eb02733109cb05463f813ab61 100644 (file)
@@ -8,7 +8,7 @@
                show: boolean;
        }
 
-       let { onPrev, onNext, show }: Props = $props();
+       let { onNext, onPrev, show }: Props = $props();
 </script>
 
 {#if show}
index 8a85df7d0c0e511df7a1f601a0a48e271749b7c0..994a35f201b297f78629b993d6e816f8c7a554bb 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Music, Video, FileText } from '@lucide/svelte';
+       import { FileText, Music, Video } from '@lucide/svelte';
        import { HorizontalScrollCarousel } from '$lib/components/app/misc';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
 
        interface PreviewItem {
                id: string;
                onNavigate: (index: number) => void;
        }
 
-       let { items, currentIndex, onNavigate }: Props = $props();
+       let { currentIndex, items, onNavigate }: Props = $props();
 
        function getFileExtension(name: string): string {
                const parts = name.split('.');
+
                if (parts.length > 1) {
                        return parts.pop()?.toUpperCase() ?? '';
                }
+
                return '';
        }
 </script>
index 2d70c302cb64ad608833695dcc602bbe0ca55885..bc1befd63320a8fa58491fbd46a15d3256db01cb 100644 (file)
@@ -1,4 +1,5 @@
 <script lang="ts">
+       import ContextGaugePopup from './ChatFormContextGauge/ContextGaugePopup.svelte';
        import {
                ChatAttachmentsList,
                ChatFormActions,
        } from '$lib/components/app';
        import {
                CLIPBOARD_CONTENT_QUOTE_PREFIX,
-               INPUT_CLASSES,
-               SETTING_CONFIG_DEFAULT,
                INITIAL_FILE_SIZE,
-               PROMPT_CONTENT_SEPARATOR
+               INPUT_CLASSES,
+               PROMPT_CONTENT_SEPARATOR,
+               SETTING_CONFIG_DEFAULT
        } from '$lib/constants';
        import {
                ContentPartType,
                MimeTypeText,
                SpecialFileType
        } from '$lib/enums';
-       import { config } from '$lib/stores/settings.svelte';
-       import ContextGaugePopup from './ChatFormContextGauge/ContextGaugePopup.svelte';
-       import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
-       import { isRouterMode } from '$lib/stores/server.svelte';
+       import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
        import { chatStore } from '$lib/stores/chat.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
        import {
-               conversationsStore,
-               activeMessages,
                activeConversation,
+               activeMessages,
+               conversationsStore,
                pendingCwd
        } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
+       import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
+       import { isRouterMode } from '$lib/stores/server.svelte';
+       import { config } from '$lib/stores/settings.svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
        import type {
                FileMentionEntry,
                GetPromptResult,
@@ -56,7 +57,6 @@
                parseClipboardContent,
                uuid
        } from '$lib/utils';
-       import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
        import {
                AudioRecorder,
                convertToWav,
                class: className = '',
                disabled = false,
                isLoading = false,
-               placeholder = 'Type a message...',
-               showMcpPromptButton = false,
-               showAddButton = true,
-               showModelSelector = true,
-               uploadedFiles = $bindable([]),
-               value = $bindable(''),
                onAttachmentRemove,
                onFilesAdd,
                onStop,
                onSystemPromptClick,
                onUploadedFileRemove,
                onUploadedFilesChange,
-               onValueChange
+               onValueChange,
+               placeholder = 'Type a message...',
+               showAddButton = true,
+               showMcpPromptButton = false,
+               showModelSelector = true,
+               uploadedFiles = $bindable([]),
+               value = $bindable('')
        }: Props = $props();
 
        // Component References
        let cwd = $derived(activeConversation()?.cwd ?? pendingCwd());
 
        const pickers = useChatFormPickers({
-               getValue: () => value,
-               setValue: (v) => {
-                       value = v;
-                       onValueChange?.(v);
-               },
-               getCaretOffset: () => inputRef?.getCaretOffset(),
-               setCaretOffset: (offset) => inputRef?.setCaretOffset(offset),
                focusInput: refocusInput,
-               getShowModelSelector: () => showModelSelector,
-               hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
-               hasCwdTools: () => toolsStore.hasEnabledCwdTools,
+               getCaretOffset: () => inputRef?.getCaretOffset(),
                getCwd: () => cwd,
+               getPickersRef: () => pickersRef,
                getServerHome: () => toolsStore.serverHome ?? null,
+               getShowModelSelector: () => showModelSelector,
+               getValue: () => value,
+               hasCwdTools: () => toolsStore.hasEnabledCwdTools,
+               hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
                openModelSelector: () => chatFormActionsRef?.openModelSelector(),
-               getPickersRef: () => pickersRef
+               setCaretOffset: (offset) => inputRef?.setCaretOffset(offset),
+               setValue: (v) => {
+                       value = v;
+                       onValueChange?.(v);
+               }
        });
 
        async function handleWorkingDirectoryChange(newDir: string | null) {
                // Committing a directory consumes the `/cwd` token; the chip's
                // clear-X path has no token to consume.
                const token = findCommandToken(value);
+
                if (token && token.name === 'cwd') {
                        value = '';
                        onValueChange?.('');
                }
+
                await conversationsStore.setCwd(newDir);
+
                if (conversationsStore.activeConversation) {
                        await chatStore.recordCwdChange(newDir?.trim() || null);
                }
 
        let pasteLongTextToFileLength = $derived.by(() => {
                const n = Number(currentConfig.pasteLongTextToFileLen);
+
                return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
        });
 
                }
 
                const selectedId = selectedModelId();
+
                if (selectedId) {
                        const model = options.find((m) => m.id === selectedId);
+
                        if (model) return model.model;
                }
 
                if (conversationModel) {
                        const model = options.find((m) => m.model === conversationModel);
+
                        if (model) return model.model;
                }
 
        $effect(() => {
                const wantContenteditable =
                        containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
+
                if (useContenteditable === wantContenteditable) return;
 
                if (!caretOffsetPinned) {
        export function checkModelSelected(): boolean {
                if (!hasModelSelected) {
                        chatFormActionsRef?.openModelSelector();
+
                        return false;
                }
+
                return true;
        }
 
        function handleFileRemove(fileId: string) {
                if (fileId.startsWith('attachment-')) {
                        const index = parseInt(fileId.replace('attachment-', ''), 10);
+
                        if (!isNaN(index) && index >= 0 && index < attachments.length) {
                                onAttachmentRemove?.(index);
                        }
                if (files.length > 0) {
                        event.preventDefault();
                        onFilesAdd?.(files);
+
                        return;
                }
 
                                                                type: MimeTypeText.PLAIN
                                                        })
                                        );
+
                                        onFilesAdd?.(attachmentFiles);
                                }
 
                                // Handle MCP prompt attachments as ChatUploadedFile with mcpPrompt data
                                if (parsed.mcpPromptAttachments.length > 0) {
                                        const mcpPromptFiles: ChatUploadedFile[] = parsed.mcpPromptAttachments.map((att) => ({
-                                               id: uuid(),
-                                               name: att.name,
-                                               size: att.content.length,
-                                               type: SpecialFileType.MCP_PROMPT,
                                                file: new File([att.content], `${att.name}${FileExtensionText.TXT}`, {
                                                        type: MimeTypeText.PLAIN
                                                }),
+                                               id: uuid(),
                                                isLoading: false,
-                                               textContent: att.content,
                                                mcpPrompt: {
-                                                       serverName: att.serverName,
+                                                       arguments: att.arguments,
                                                        promptName: att.promptName,
-                                                       arguments: att.arguments
-                                               }
+                                                       serverName: att.serverName
+                                               },
+                                               name: att.name,
+                                               size: att.content.length,
+                                               textContent: att.content,
+                                               type: SpecialFileType.MCP_PROMPT
                                        }));
 
                                        uploadedFiles = [...uploadedFiles, ...mcpPromptFiles];
 
                const promptName = promptInfo.title || promptInfo.name;
                const placeholder: ChatUploadedFile = {
-                       id: placeholderId,
-                       name: promptName,
-                       size: INITIAL_FILE_SIZE,
-                       type: SpecialFileType.MCP_PROMPT,
                        file: new File([], 'loading'),
+                       id: placeholderId,
                        isLoading: true,
                        mcpPrompt: {
-                               serverName: promptInfo.serverName,
+                               arguments: args ? { ...args } : undefined,
                                promptName: promptInfo.name,
-                               arguments: args ? { ...args } : undefined
-                       }
+                               serverName: promptInfo.serverName
+                       },
+                       name: promptName,
+                       size: INITIAL_FILE_SIZE,
+                       type: SpecialFileType.MCP_PROMPT
                };
 
                uploadedFiles = [...uploadedFiles, placeholder];
                        f.id === placeholderId
                                ? {
                                                ...f,
-                                               isLoading: false,
-                                               textContent: promptText,
-                                               size: promptText.length,
                                                file: new File([promptText], `${f.name}${FileExtensionText.TXT}`, {
                                                        type: MimeTypeText.PLAIN
-                                               })
+                                               }),
+                                               isLoading: false,
+                                               size: promptText.length,
+                                               textContent: promptText
                                        }
                                : f
                );
        function handleMentionSelect(entry: FileMentionEntry) {
                const cursor = inputRef?.getCaretOffset() ?? value.length;
                const token = findMentionToken(value, cursor);
+
                if (!token) return;
 
                const built = buildMentionInsertion(entry, value, token);
+
                if (!built) return;
 
                // Pin the post-insertion caret BEFORE the swap effect runs;
        async function handleMicClick() {
                if (!audioRecorder || !recordingSupported) {
                        console.warn('Audio recording not supported');
+
                        return;
                }
 
                                onFileUpload={handleFileUpload}
                                onMicClick={handleMicClick}
                                {onStop}
-                               onSystemPromptClick={() => onSystemPromptClick?.({ message: value, files: uploadedFiles })}
+                               onSystemPromptClick={() => onSystemPromptClick?.({ files: uploadedFiles, message: value })}
                                onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined}
                                onMcpResourcesClick={() => (isResourceDialogOpen = true)}
                        />
index b281ba7e5483bd846b5bdb6cec5ccfda6b637a99..39a314c7dd385891492942d4e06aaf275f6224c7 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        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';
 
        interface Props {
                disabled?: boolean;
index f81dcf09c06a7eec73aac26854dddb11157a44e3..c17c796a45859d74ef62f1a339beac71dd42357a 100644 (file)
@@ -1,20 +1,20 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Plus, File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
+       import { File, FolderOpen, MessageSquare, Plus, Zap } from '@lucide/svelte';
+       import {
+               ChatFormActionAddMcpServersSubmenu,
+               ChatFormActionAddReasoningSubmenu,
+               ChatFormActionAddToolsSubmenu
+       } from '$lib/components/app';
+       import { buttonVariants } from '$lib/components/ui/button';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
        import * as Tooltip from '$lib/components/ui/tooltip';
-       import { buttonVariants } from '$lib/components/ui/button';
        import { cn } from '$lib/components/ui/utils';
        import {
                ATTACHMENT_FILE_ITEMS,
                ATTACHMENT_TOOLTIP_TEXT,
                TOOLTIP_DELAY_DURATION
        } from '$lib/constants';
-       import {
-               ChatFormActionAddToolsSubmenu,
-               ChatFormActionAddMcpServersSubmenu,
-               ChatFormActionAddReasoningSubmenu
-       } from '$lib/components/app';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
 
        interface Props {
                class: className = '',
                disabled = false,
                hasAudioModality = false,
-               hasVideoModality = false,
-               hasVisionModality = false,
                hasMcpPromptsSupport = false,
                hasMcpResourcesSupport = false,
+               hasVideoModality = false,
+               hasVisionModality = false,
                onFileUpload,
-               onSystemPromptClick,
                onMcpPromptClick,
+               onMcpResourcesClick,
                onMcpSettingsClick,
-               onMcpResourcesClick
+               onSystemPromptClick
        }: Props = $props();
 
        let dropdownOpen = $state(false);
 
        const attachmentMenu = useAttachmentMenu(
                () => ({
-                       hasVisionModality,
                        hasAudioModality,
-                       hasVideoModality,
                        hasMcpPromptsSupport,
-                       hasMcpResourcesSupport
+                       hasMcpResourcesSupport,
+                       hasVideoModality,
+                       hasVisionModality
                }),
-               () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
+               () => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
                () => {
                        dropdownOpen = false;
                }
index de1ced1723198b9ec60b2d60db8cc5a027d11be1..852bf3fb66b38f2545c75e9dbd9632b37838ae39 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Settings, Plus } from '@lucide/svelte';
-       import { Switch } from '$lib/components/ui/switch';
+       import { Plus, Settings } from '@lucide/svelte';
+       import { goto } from '$app/navigation';
+       import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
-       import { McpLogo, DropdownMenuSearchable, McpServerIdentity } from '$lib/components/app';
+       import { Switch } from '$lib/components/ui/switch';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       import { ROUTES } from '$lib/constants/routes';
+       import { HealthCheckStatus } from '$lib/enums';
        import { conversationsStore } from '$lib/stores/conversations.svelte';
        import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { HealthCheckStatus } from '$lib/enums';
        import type { MCPServerSettingsEntry } from '$lib/types';
-       import { goto } from '$app/navigation';
-       import { ROUTES } from '$lib/constants/routes';
 
        interface Props {
                onMcpSettingsClick?: () => void;
        let hasMcpServers = $derived(mcpServers.length > 0);
        let filteredMcpServers = $derived.by(() => {
                const query = mcpSearchQuery.toLowerCase().trim();
+
                if (!query) return mcpServers;
+
                return mcpServers.filter((s) => {
                        const name = getServerLabel(s).toLowerCase();
                        const url = s.url.toLowerCase();
+
                        return name.includes(query) || url.includes(query);
                });
        });
index 4bf6d16726214e0f7e00a6dd48a34fad2a650ba4..03b9f9f0a9da476d1c42b8cd6876abefc7870792 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Lightbulb, LightbulbOff, Check, Info } from '@lucide/svelte';
+       import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
        import * as Tooltip from '$lib/components/ui/tooltip';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
 
        const reasoning = useReasoningMenu();
index 1c6bb0c1c6eed59fcf246968e7236020c968d745..73f616f69730d4ff178773e2f5ae6c0e841a74ee 100644 (file)
@@ -1,30 +1,30 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import type { Snippet } from 'svelte';
-       import * as Tooltip from '$lib/components/ui/tooltip';
-       import * as Sheet from '$lib/components/ui/sheet';
-       import * as Collapsible from '$lib/components/ui/collapsible';
-       import { File, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
-       import { Switch } from '$lib/components/ui/switch';
-       import { Checkbox } from '$lib/components/ui/checkbox';
-       import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
-       import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
-       import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
-       import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
-       import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { McpLogo } from '$lib/components/app';
+       import { File, FolderOpen, MessageSquare, Zap } from '@lucide/svelte';
        import {
-               PencilRuler,
+               Check,
                ChevronDown,
                ChevronRight,
                Lightbulb,
                LightbulbOff,
-               Check
+               PencilRuler
        } from '@lucide/svelte';
+       import { McpLogo } from '$lib/components/app';
+       import { Checkbox } from '$lib/components/ui/checkbox';
+       import * as Collapsible from '$lib/components/ui/collapsible';
+       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 { HealthCheckStatus } from '$lib/enums';
        import { AttachmentAction } from '$lib/enums/attachment.enums';
+       import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
+       import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
+       import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
+       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { Snippet } from 'svelte';
 
        interface Props {
                class?: string;
                class: className = '',
                disabled = false,
                hasAudioModality = false,
-               hasVisionModality = false,
-               hasVideoModality = false,
                hasMcpPromptsSupport = false,
                hasMcpResourcesSupport = false,
+               hasVideoModality = false,
+               hasVisionModality = false,
                onFileUpload,
-               onSystemPromptClick,
                onMcpPromptClick,
                onMcpResourcesClick,
+               onSystemPromptClick,
                trigger
        }: Props = $props();
 
 
        const attachmentMenu = useAttachmentMenu(
                () => ({
-                       hasVisionModality,
                        hasAudioModality,
-                       hasVideoModality,
                        hasMcpPromptsSupport,
-                       hasMcpResourcesSupport
+                       hasMcpResourcesSupport,
+                       hasVideoModality,
+                       hasVisionModality
                }),
-               () => ({ onFileUpload, onSystemPromptClick, onMcpPromptClick, onMcpResourcesClick }),
+               () => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
                () => {
                        sheetOpen = false;
                }
index 4473c29a3d25f8fb26ef09cedce72d759e84376e..844376ea65f927a3bc75a42e175066b7d9a7e348 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { PencilRuler, ChevronDown, ChevronRight, Loader2, Info, Check } from '@lucide/svelte';
+       import { Check, ChevronDown, ChevronRight, Info, Loader2, PencilRuler } from '@lucide/svelte';
        import { Checkbox } from '$lib/components/ui/checkbox';
        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 { toolsStore } from '$lib/stores/tools.svelte';
        import { CLI_FLAGS } from '$lib/constants';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
 
        const toolsPanel = useToolsPanel();
        const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0);
index 08d691c1438f1caf15fd4e4d1edd29e18ef93219..c54471a5aea87f5a2e121b3a0ea84cd9d9207ec3 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
        import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
        import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
-       import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
+       import { isMobile } from '$lib/stores/viewport.svelte';
 
        interface Props {
                disabled?: boolean;
@@ -21,9 +21,9 @@
        let {
                disabled = false,
                hasAudioModality = false,
-               hasVideoModality = false,
                hasMcpPromptsSupport = false,
                hasMcpResourcesSupport = false,
+               hasVideoModality = false,
                hasVisionModality = false,
                onFileUpload,
                onMcpPromptClick,
index 712326cba60868aa49e54207f14c246795af328a..62befb5ddabc5edf9d6f31e2295b0c3fdc29eada 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
+       import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
        import { chatStore } from '$lib/stores/chat.svelte';
+       import { activeMessages } from '$lib/stores/conversations.svelte';
        import {
-               modelsStore,
                modelOptions,
+               modelsStore,
                selectedModelId,
                selectedModelName
        } from '$lib/stores/models.svelte';
        import { isRouterMode, serverError } from '$lib/stores/server.svelte';
-       import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
        import { isMobile } from '$lib/stores/viewport.svelte';
-       import { activeMessages } from '$lib/stores/conversations.svelte';
 
        interface Props {
                disabled?: boolean;
@@ -27,9 +27,9 @@
                disabled = false,
                forceForegroundText = false,
                hasAudioModality = $bindable(false),
+               hasModelSelected = $bindable(false),
                hasVideoModality = $bindable(false),
                hasVisionModality = $bindable(false),
-               hasModelSelected = $bindable(false),
                isSelectedModelInCache = $bindable(true),
                submitTooltip = $bindable(''),
                useGlobalSelection = false
@@ -46,6 +46,7 @@
 
        let selectorModel = $derived.by(() => {
                const storeModel = selectedModelName();
+
                if (storeModel && storeModel !== conversationModel) {
                        return storeModel;
                }
@@ -66,6 +67,7 @@
                                modelsStore.selectedModelName = null;
                                modelsStore.clearSelection();
                        }
+
                        lastSyncedConversationModel = conversationModel;
                } else if (
                        isRouter &&
@@ -76,6 +78,7 @@
                ) {
                        lastSyncedConversationModel = null;
                        const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
+
                        if (first) modelsStore.selectModelById(first.id);
                }
        });
index 59a71409727e9848cc03ae6c9da6e63c46548a0e..dbf73e8dd115ac4e7ce5bec18a7f88c1f687db90 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        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';
 
        interface Props {
                class?: string;
index d19d2c7125355429572346f92749355f96f1349f..7207165a1fc6868d21f516a0bf503ce4b5839c19 100644 (file)
@@ -1,28 +1,28 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Square, SkipForward } from '@lucide/svelte';
-       import { Button } from '$lib/components/ui/button';
-       import { ChatService } from '$lib/services';
+       import { SkipForward, Square } from '@lucide/svelte';
+       import { goto } from '$app/navigation';
+       import { page } from '$app/state';
        import {
-               ChatFormActionsAdd,
                ChatFormActionModels,
                ChatFormActionRecord,
+               ChatFormActionsAdd,
                ChatFormActionSubmit,
                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 { FileTypeCategory, MessageRole } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { config } from '$lib/stores/settings.svelte';
-       import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
+       import { ChatService } from '$lib/services';
        import {
                activeProcessingState,
                isChatStreaming,
                isLoading as chatIsLoading
        } from '$lib/stores/chat.svelte';
+       import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { config } from '$lib/stores/settings.svelte';
        import { getFileTypeCategory } from '$lib/utils';
-       import { goto } from '$app/navigation';
-       import { page } from '$app/state';
-       import { ROUTES } from '$lib/constants/routes';
 
        interface Props {
                canSend?: boolean;
                isLoading = false,
                isReasoning = false,
                isRecording = false,
-               showAddButton = true,
-               showModelSelector = true,
-               uploadedFiles = [],
                onFileUpload,
+               onMcpPromptClick,
+               onMcpResourcesClick,
                onMicClick,
                onStop,
                onSystemPromptClick,
-               onMcpPromptClick,
-               onMcpResourcesClick
+               showAddButton = true,
+               showModelSelector = true,
+               uploadedFiles = []
        }: Props = $props();
 
        let currentConfig = $derived(config());
                if (!page.params.id) return false;
 
                const messages = activeMessages() as DatabaseMessage[];
+
                let totalHistoricalTokens = 0;
+
                for (const m of messages) {
                        if (m.role !== MessageRole.ASSISTANT) continue;
+
                        const timings = m.timings;
+
                        if (!timings) continue;
+
                        const agenticLlm = timings.agentic?.llm;
+
                        if (agenticLlm?.prompt_n != null || agenticLlm?.predicted_n != null) {
                                totalHistoricalTokens += (agenticLlm?.prompt_n ?? 0) + (agenticLlm?.predicted_n ?? 0);
                        } else {
                                totalHistoricalTokens += (timings.prompt_n ?? 0) + (timings.predicted_n ?? 0);
                        }
                }
+
                if (totalHistoricalTokens > 0) return true;
 
                if (!chatIsLoading() && !isChatStreaming()) return false;
 
                const processingState = activeProcessingState();
+
                if (!processingState) return false;
+
                const livePromptTokens = Math.max(
                        processingState.promptTokens ?? 0,
                        processingState.promptProgress?.processed ?? 0
                );
                const liveOutputTokens = processingState.outputTokensUsed ?? 0;
+
                return livePromptTokens > 0 || liveOutputTokens > 0;
        });
 </script>
index 18fd24302e3279515777b5e280d274e8db7c683a..98b082c89109dc5cbd6666e9c740215bbe9dce01 100644 (file)
@@ -1,11 +1,8 @@
 <script lang="ts">
-       import { onDestroy, onMount, untrack } from 'svelte';
-       import { mode } from 'mode-watcher';
-       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
-       import githubLightCss from 'highlight.js/styles/github.css?inline';
-       import { isMobile } from '$lib/stores/viewport.svelte';
-       import { ColorMode } from '$lib/enums';
        import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
+       import { ColorMode } from '$lib/enums';
+       import { isMobile } from '$lib/stores/viewport.svelte';
+       import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
        import {
                badgeAwareWordJump,
                buildFragment,
                SourceHistory,
                stripBlockBoundaryLineBreaks,
                syncCodeBlockHatches,
-               tokenizeContent,
-               textOffsetToRange
+               textOffsetToRange,
+               tokenizeContent
        } from '$lib/utils';
-       import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
+       import githubLightCss from 'highlight.js/styles/github.css?inline';
+       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
+       import { mode } from 'mode-watcher';
+       import { onDestroy, onMount, untrack } from 'svelte';
 
        interface Props {
                class?: string;
@@ -57,7 +57,9 @@
        // serialized source, not the DOM shape.
        function syncEmptyState(serialized?: string) {
                if (!rootElement) return;
+
                const source = serialized ?? serializeContent(rootElement);
+
                rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
        }
 
         */
        function highlightCodeBlockElement(el: HTMLElement): boolean {
                const segment = el.textContent ?? '';
+
                if (highlightedSegments.get(el) === segment) return false;
 
                const open = CODE_BLOCK_OPEN_RE.exec(segment);
+
                if (!open) return false;
 
                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 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
                const html = core ? highlightCode(core, language || 'text', false) : '';
                const tpl = document.createElement('template');
+
                tpl.innerHTML = html;
 
                el.replaceChildren(
                        document.createTextNode(trailing + '```')
                );
                highlightedSegments.set(el, segment);
+
                return true;
        }
 
                if (!rootElement) return;
 
                const range = safeRange();
+
                if (!range) return;
 
                let node: Node | null = range.startContainer;
+
                if (node === rootElement) {
                        node = rootElement.childNodes[range.startOffset - 1] ?? null;
                }
                while (node && node !== rootElement) {
                        if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
                                const caret = rangeToTextOffset(rootElement, range);
+
                                if (highlightCodeBlockElement(node)) {
                                        restoreCaret(caret);
                                }
+
                                return;
                        }
+
                        node = node.parentNode;
                }
        }
                document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove());
 
                const style = document.createElement('style');
+
                style.setAttribute('data-highlight-theme-preview', 'true');
                style.textContent = isDark ? githubDarkCss : githubLightCss;
 
                if (!rootElement) return null;
 
                const selection = window.getSelection();
+
                if (!selection || selection.rangeCount === 0) return null;
 
                const range = selection.getRangeAt(0);
 
                const target = textOffsetToRange(rootElement, offset);
                const selection = window.getSelection();
+
                if (!selection) return;
 
                if (extend && selection.anchorNode) {
                                target.startContainer,
                                target.startOffset
                        );
+
                        return;
                }
 
 
        function resizeHeight() {
                if (!rootElement) return;
+
                rootElement.style.height = 'auto';
                rootElement.style.height = `${rootElement.scrollHeight}px`;
        }
 
        function recordHistory(newGroup: boolean) {
                if (!rootElement) return;
+
                history.push(
-                       { value: lastEmittedValue, caret: rangeToTextOffset(rootElement, safeRange()) },
+                       { caret: rangeToTextOffset(rootElement, safeRange()), value: lastEmittedValue },
                        Date.now(),
                        newGroup
                );
                // lands on the line directly below the block.
                if (inputType === 'insertLineBreak' || inputType === 'insertParagraph') {
                        const caret = rangeToTextOffset(rootElement, safeRange());
+
                        if (stripBlockBoundaryLineBreaks(rootElement)) {
                                restoreCaret(caret);
                        } else {
                                const source = serializeContent(rootElement);
+
                                let end = caret;
 
                                // the caret must end up after the inserted \n; some browsers
                                // \n doubles as a block's separator line (source ends with
                                // \n\n) or sits inside a block element.
                                let last = rootElement.lastChild;
+
                                while (last && last.nodeName === 'BR') last = last.previousSibling;
+
                                if (
                                        end === source.length &&
                                        source.endsWith('\n') &&
                syncCodeBlockHatches(rootElement);
 
                const serialized = serializeContent(rootElement);
+
                syncEmptyState(serialized);
+
                if (serialized === lastEmittedValue) return;
 
                // Plain typing/deletes coalesce per time window; structural edits
                // completed or broken) - the browser-owned text nodes cannot
                // restyle themselves across element boundaries.
                const tokens = tokenizeContent(serialized);
+
                if (!domMatchesTokens(rootElement, tokens)) {
                        renderTokens(tokens);
 
                        // block element and the rebuild splits it back out, which
                        // synthesizes the separator newline) - keep value in sync.
                        const reserialized = serializeContent(rootElement);
+
                        if (reserialized !== serialized) {
                                lastEmittedValue = reserialized;
                                value = reserialized;
                if (!rootElement) return;
 
                const range = safeRange();
+
                if (!range) return;
 
                if (!range.collapsed) {
                // a break at the very end of a code block exits the block (the
                // new line belongs below it, not inside)
                let exitBlock: HTMLElement | null = null;
+
                if (container.nodeType === Node.TEXT_NODE) {
                        let node: Node | null = container.parentNode;
+
                        while (node && node !== rootElement) {
                                if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
                                        const tail = document.createRange();
+
                                        tail.setStart(container, offset);
                                        tail.setEnd(node, node.childNodes.length);
+
                                        if (tail.toString().length === 0) exitBlock = node;
+
                                        break;
                                }
+
                                node = node.parentNode;
                        }
                }
                        exitBlock.after(nl);
                } else if (container.nodeType === Node.TEXT_NODE) {
                        const text = container as Text;
+
                        if (offset === 0) {
                                text.before(nl);
                        } else if (offset === text.length) {
 
                const selection = window.getSelection();
                const after = document.createRange();
+
                after.setStartAfter(nl);
                after.collapse(true);
                selection?.removeAllRanges();
                if (rootElement.firstChild?.nodeName === 'BR') return false;
 
                const first = rootElement.firstChild;
+
                if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false;
 
                const range = safeRange();
+
                if (!range || !range.collapsed) return false;
 
                // the caret must sit inside the block: on its very first
                if (!first.contains(range.startContainer)) return false;
 
                const caret = rangeToTextOffset(rootElement, range);
+
                if (key === 'ArrowLeft') {
                        if (caret !== 0) return false;
                } else {
                        const firstLineEnd = (first.textContent ?? '').indexOf('\n');
+
                        if (firstLineEnd !== -1 && caret > firstLineEnd) return false;
                }
 
                // eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
                rootElement.prepend(document.createElement('br'));
                restoreCaret(0, extend);
+
                return true;
        }
 
                if (!rootElement) return;
 
                const first = rootElement.firstChild;
+
                if (first?.nodeName !== 'BR') return;
 
                const second = first.nextSibling;
+
                if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return;
 
                const range = safeRange();
                const onHatch =
                        range !== null && range.startContainer === rootElement && range.startOffset === 0;
+
                if (!onHatch) {
                        first.remove();
                }
         */
        function handleKeydown(event: KeyboardEvent) {
                const mod = event.ctrlKey || event.metaKey;
+
                if (mod && !event.altKey && !isComposing && rootElement) {
                        const key = event.key.toLowerCase();
                        const isUndo = key === 'z' && !event.shiftKey;
                        if (isUndo || isRedo) {
                                event.preventDefault();
                                const current = {
-                                       value: lastEmittedValue,
-                                       caret: rangeToTextOffset(rootElement, safeRange())
+                                       caret: rangeToTextOffset(rootElement, safeRange()),
+                                       value: lastEmittedValue
                                };
                                const entry = isUndo ? history.undo(current) : history.redo(current);
+
                                if (entry) applyHistoryEntry(entry);
+
                                return;
                        }
                }
                        // stuck on the old line (see insertLineBreak).
                        event.preventDefault();
                        insertLineBreak();
+
                        return;
                }
 
                        // re-tokenize/re-highlight follows.
                        event.preventDefault();
                        document.execCommand('insertLineBreak');
+
                        return;
                }
 
                ) {
                        if (moveCaretBeforeLeadingCodeBlock(event.key, event.shiftKey)) {
                                event.preventDefault();
+
                                return;
                        }
                }
                                if (target !== null) {
                                        event.preventDefault();
                                        restoreCaret(target, event.shiftKey);
+
                                        return;
                                }
                        }
        // change as our own and does not re-render.
        function applyHistoryEntry(entry: SourceHistoryEntry) {
                if (!rootElement) return;
+
                renderTokens(tokenizeContent(entry.value));
                lastEmittedValue = entry.value;
                value = entry.value;
         */
        function handlePasteEvent(event: ClipboardEvent) {
                const pasted = event.clipboardData?.getData('text/plain');
+
                if (pasted && pasted.length > 0) {
                        event.preventDefault();
 
                        // element-boundary carets (e.g. right before a badge) Chromium's
                        // insertText can drop the preceding text node's trailing whitespace.
                        const range = safeRange();
+
                        if (rootElement && range && range.collapsed) {
                                restoreCaret(rangeToTextOffset(rootElement, range));
                        }
        // consumes the event (files, quoted prompts, long text).
        function handlePaste(event: ClipboardEvent) {
                onPaste?.(event);
+
                if (!event.defaultPrevented) {
                        handlePasteEvent(event);
                }
                if (!rootElement) return null;
 
                const range = safeRange();
+
                if (!range || range.collapsed) return null;
 
                const startRange = range.cloneRange();
+
                startRange.collapse(true);
 
                const source = serializeContent(rootElement);
                const start = rangeToTextOffset(rootElement, startRange);
                const end = rangeToTextOffset(rootElement, range);
 
-               return { text: source.slice(start, end), range };
+               return { range, text: source.slice(start, end) };
        }
 
        function handleCopy(event: ClipboardEvent) {
                const slice = selectionSourceSlice();
+
                if (!slice) return;
 
                event.clipboardData?.setData('text/plain', slice.text);
 
        function handleCut(event: ClipboardEvent) {
                const slice = selectionSourceSlice();
+
                if (!slice) return;
 
                event.clipboardData?.setData('text/plain', slice.text);
                resizeHeight();
                syncEmptyState();
                document.addEventListener('selectionchange', handleSelectionChange);
+
                if (!isMobile.current) {
                        rootElement?.focus({ preventScroll: true });
                }
        // browser already owns the right shape.
        $effect(() => {
                const incoming = value ?? '';
+
                if (incoming === lastEmittedValue) return;
 
                recordHistory(true); // external edit (mention insert, clear, ...): own undo step
 
        export function getCaretOffset(): number {
                if (!rootElement) return 0;
+
                return rangeToTextOffset(rootElement, safeRange());
        }
 
                if (rootElement && rootElement !== document.activeElement) {
                        rootElement.focus({ preventScroll: true });
                }
+
                restoreCaret(offset);
        }
 
        export function focus() {
                if (isMobile.current) return;
+
                rootElement?.focus({ preventScroll: true });
        }
 
index ff6d39fdd48cf5f98ec389bb3854c2f72a2a7385..0e7fadc4a06905b720ac790fd42a18906fa282d2 100644 (file)
@@ -1,9 +1,7 @@
 <script lang="ts">
-       import { untrack } from 'svelte';
-       import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
-       import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
-       import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
        import ContextGaugeDial from './ContextGaugeDial.svelte';
+       import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
+       import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
        import {
                gaugeTriggerClick,
                gaugeTriggerEnter,
                gaugeTriggerLeave,
                gaugeTriggerPointerDown
        } from '$lib/stores/context-gauge-popup.svelte';
+       import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
+       import { untrack } from 'svelte';
 
        const gauge = useContextGauge();
 
        $effect(() => {
                const conv = activeConversation();
+
                untrack(() => chatStore.setActiveProcessingConversation(conv?.id ?? null));
        });
 
        $effect(() => {
                const conv = activeConversation();
                const messages = activeMessages() as DatabaseMessage[];
+
                if (!conv) return;
+
                if (isLoading() || isChatStreaming()) return;
 
                if (messages.length === 0) {
                        untrack(() => chatStore.clearProcessingState(conv.id));
+
                        return;
                }
 
index 71c6a33cb1e2542528cb19f090099e05b22c31b1..271997b39355132edc1e19f58e8468c11cc044b5 100644 (file)
@@ -5,7 +5,7 @@
                subtitle?: string;
        }
 
-       let { label, value, subtitle }: Props = $props();
+       let { label, subtitle, value }: Props = $props();
 </script>
 
 <div class="grid gap-1.5">
index fdec5aca5a9b981762e4c073fbd9f96e76f2fed5..ab36e4a43f74da4c182e39d36550a5b1accc74ce 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
+       import ContextGaugeDetailRow from './ContextGaugeDetailRow.svelte';
        import { ChevronDown } from '@lucide/svelte';
        import * as Collapsible from '$lib/components/ui/collapsible';
        import { STATS_UNITS } from '$lib/constants';
-       import ContextGaugeDetailRow from './ContextGaugeDetailRow.svelte';
 
        interface Props {
                currentRead: number;
        }
 
        let {
-               currentRead,
-               currentFresh,
+               averageTokensPerSecond,
+               cumulativeCacheTotal,
+               cumulativeOutput,
+               cumulativeRead,
                currentCache,
+               currentFresh,
                currentOutput,
+               currentRead,
                kvTotal,
-               cumulativeRead,
-               cumulativeOutput,
-               cumulativeCacheTotal,
-               averageTokensPerSecond,
                transientDetails
        }: Props = $props();
 
index 6e2616d363da96a78eba8e951e10afa3a231950c..8f3ec435212a98c2c191f3b4c92a65f0df183f17 100644 (file)
@@ -8,7 +8,7 @@
                size?: 'sm' | 'md';
        }
 
-       let { percent, level, size = 'sm' }: Props = $props();
+       let { level, percent, size = 'sm' }: Props = $props();
 
        const RADIUS = 11;
        const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
index 24a67cfdfd6be774c7b802c7dd1ce7c6acadcfb8..022e626ae9d0a28d8d5f167977e8ef7d37750a9d 100644 (file)
@@ -8,7 +8,7 @@
                onLoad: () => void;
        }
 
-       let { modelId, isLoading, onLoad }: Props = $props();
+       let { isLoading, modelId, onLoad }: Props = $props();
 </script>
 
 {#if modelId !== null && !isLoading}
index af9ad010e3a3c6db60f969450240cc66ca144cb5..db4479b65b08c218ebfc236b561839db260118e7 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { formatParameters } from '$lib/utils/formatters';
-       import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
+       import { colorLevelBgClass, colorLevelTextClass } from './context-gauge';
        import ContextGaugeDetails from './ContextGaugeDetails.svelte';
        import ContextGaugeLoadModel from './ContextGaugeLoadModel.svelte';
-       import { colorLevelBgClass, colorLevelTextClass } from './context-gauge';
+       import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
        import {
-               gaugePopup,
                gaugeCardEnter,
                gaugeCardLeave,
+               gaugePopup,
                gaugePopupClose
        } from '$lib/stores/context-gauge-popup.svelte';
+       import { formatParameters } from '$lib/utils/formatters';
 
        const gauge = useContextGauge();
 
 
                const onPointerDown = (event: PointerEvent) => {
                        const target = event.target;
+
                        if (!(target instanceof Node)) return;
+
                        if (cardEl?.contains(target)) return;
+
                        if (target instanceof Element && target.closest('[data-context-gauge-trigger]')) return;
+
                        gaugePopupClose();
                };
 
                document.addEventListener('pointerdown', onPointerDown, true);
+
                return () => document.removeEventListener('pointerdown', onPointerDown, true);
        });
 
index 5b001001564c1bf0cc264a15ce08ecc68ab79e24..42aab668e693e181e96c1fcf02b8173f31986391 100644 (file)
@@ -5,8 +5,11 @@ const CRITICAL_THRESHOLD = 95;
 
 export function colorLevelFromPercent(percent: number | null): ColorLevel {
        if (percent === null) return 'neutral';
+
        if (percent >= CRITICAL_THRESHOLD) return 'critical';
+
        if (percent >= WARNING_THRESHOLD) return 'warning';
+
        return 'ok';
 }
 
index 36c82224a67c5731264e63d4b1f935a466d70afc..874dca5a5f6cd81013b56aea1a2dc3eefcd6500a 100644 (file)
@@ -1,13 +1,13 @@
 <script lang="ts">
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import {
-               mcpResourceAttachments,
-               mcpHasResourceAttachments
-       } from '$lib/stores/mcp-resources.svelte';
        import {
                ChatAttachmentsListItemMcpResource,
                HorizontalScrollCarousel
        } from '$lib/components/app';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import {
+               mcpHasResourceAttachments,
+               mcpResourceAttachments
+       } from '$lib/stores/mcp-resources.svelte';
 
        interface Props {
                class?: string;
index 5feaaf35aa14fc1fe6bb7202cf8e3a5d312e2d31..df654b25bb452545698071adc1a5ec06049829d6 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
        import { FolderOpen, Sparkles } from '@lucide/svelte';
-       import { MODEL_SELECTOR_ICON } from '$lib/constants';
-       import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
-       import { ChatFormCommandAction } from '$lib/enums';
-       import type { ChatFormCommand } from '$lib/types';
        import {
                ChatFormPickerList,
                ChatFormPickerListItem,
                ChatFormPickerPopover
        } from '$lib/components/app/chat';
+       import { MODEL_SELECTOR_ICON } from '$lib/constants';
+       import { ChatFormCommandAction } from '$lib/enums';
+       import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
+       import type { ChatFormCommand } from '$lib/types';
 
        /**
         * Slash-command picker; `query` (typed after `/`) filters the commands.
                onSelect: (command: ChatFormCommand) => void;
        }
 
-       let { class: className = '', isOpen, query, commands, onClose, onSelect }: Props = $props();
+       let { class: className = '', commands, isOpen, onClose, onSelect, query }: Props = $props();
 
        const commandIcon: Record<ChatFormCommandAction, typeof Sparkles> = {
-               [ChatFormCommandAction.PROMPT]: Sparkles,
                [ChatFormCommandAction.CWD]: FolderOpen,
-               [ChatFormCommandAction.MODEL]: MODEL_SELECTOR_ICON
+               [ChatFormCommandAction.MODEL]: MODEL_SELECTOR_ICON,
+               [ChatFormCommandAction.PROMPT]: Sparkles
        };
 
        const trimmedQuery = $derived((query ?? '').trim().toLowerCase());
 
        function stepEnabled(from: number, dir: number): number {
                const n = filteredCommands.length;
+
                if (n === 0) return -1;
+
                for (let i = 1; i <= n; i++) {
                        const idx = (from + dir * i + n) % n;
+
                        if (!filteredCommands[idx].disabled) return idx;
                }
+
                return -1;
        }
 
        const nav = usePickerNavigation({
-               isOpen: () => isOpen,
                count: () => filteredCommands.length,
-               step: (from, dir) => (from < 0 ? firstEnabledIndex() : stepEnabled(from, dir)),
+               isOpen: () => isOpen,
                onClose: () => onClose(),
-               onSelect: (index) => handleSelect(filteredCommands[index])
+               onSelect: (index) => handleSelect(filteredCommands[index]),
+               step: (from, dir) => (from < 0 ? firstEnabledIndex() : stepEnabled(from, dir))
        });
 
        $effect(() => {
        $effect(() => {
                if (nav.hoveredIndex < 0 || nav.hoveredIndex >= filteredCommands.length) {
                        nav.reset(firstEnabledIndex());
+
                        return;
                }
+
                if (filteredCommands[nav.hoveredIndex].disabled) {
                        nav.reset(firstEnabledIndex());
                }
@@ -85,6 +91,7 @@
 
        function handleSelect(command: ChatFormCommand) {
                if (command.disabled) return;
+
                onSelect(command);
                onClose();
        }
index 07cbaf492ce93f9410588e72480caf8bb562f387..c69b6a995dd9d8ebe8f58b9e05671c6e511e927b 100644 (file)
@@ -1,22 +1,22 @@
 <script lang="ts">
        import { File, Folder } from '@lucide/svelte';
-       import { abbreviateHome, runGlobSearchWithChildren, type GlobEntryResult } from '$lib/utils';
-       import { toolsStore } from '$lib/stores/tools.svelte';
-       import { BuiltInTool, FileMentionEntryType, GlobSearchType, KeyboardKey } from '$lib/enums';
-       import { isMobile } from '$lib/stores/viewport.svelte';
-       import { config } from '$lib/stores/settings.svelte';
+       import { ChatFormPickerList, ChatFormPickerListItem } from '$lib/components/app/chat';
+       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 HighlightedMatch from '$lib/components/app/forms/HighlightedMatch.svelte';
-       import { ChatFormPickerList, ChatFormPickerListItem } from '$lib/components/app/chat';
-       import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
-       import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
-       import type { FileMentionEntry } from '$lib/types';
        import {
                FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
                HOME_TILDE,
                SEARCH_DEBOUNCE_MS
        } 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';
+       import { config } from '$lib/stores/settings.svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { isMobile } from '$lib/stores/viewport.svelte';
+       import type { FileMentionEntry } from '$lib/types';
+       import { abbreviateHome, type GlobEntryResult, runGlobSearchWithChildren } from '$lib/utils';
 
        /**
         * Floating file/folder mention picker. The chat input is the search
 
        let {
                class: className = '',
-               isOpen,
-               query,
                customAnchor = null,
-               scopePath = null,
+               isOpen,
                onClose,
+               onOpened,
                onSelect,
-               onOpened
+               query,
+               scopePath = null
        }: Props = $props();
 
        const nav = usePickerNavigation({
-               isOpen: () => isOpen,
                count: () => displayedItems.length,
+               isOpen: () => isOpen,
                onClose: () => onClose(),
                onSelect: (index) => handleSelect(displayedItems[index])
        });
@@ -69,6 +69,7 @@
        // would otherwise reach the server as max_depth 0 = unlimited.
        const searchDepth = $derived.by(() => {
                const n = Number(config().mentionSearchMaxDepth);
+
                return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH;
        });
 
@@ -78,8 +79,8 @@
        const MENTION_SEARCH_LIMIT = 50;
 
        const search = useDebouncedSearch({
-               debounceMs: SEARCH_DEBOUNCE_MS,
                canRun: () => isOpen && fileSearchEnabled,
+               debounceMs: SEARCH_DEBOUNCE_MS,
                getQuery: () => trimmedQuery,
                run: async (query, signal, isCurrent) => {
                        try {
                                        searchDepth,
                                        MENTION_SEARCH_LIMIT,
                                        signal,
-                                       { type: GlobSearchType.ALL, descendOnTrailingSeparator: true }
+                                       { descendOnTrailingSeparator: true, type: GlobSearchType.ALL }
                                );
+
                                if (!isCurrent()) return;
+
                                if (res.error) {
                                        searchResults = [];
                                        searchError = res.error;
+
                                        return;
                                }
+
                                const toEntry = (e: GlobEntryResult): FileMentionEntry => ({
-                                       path: e.path,
                                        name: e.name,
+                                       path: e.path,
                                        type: e.type === 'dir' ? FileMentionEntryType.DIRECTORY : FileMentionEntryType.FILE
                                });
+
                                searchResults = res.entries.map(toEntry);
                                searchError = null;
                        } catch (err) {
                                if (!isCurrent() || signal.aborted) return;
+
                                searchResults = [];
                                searchError = err instanceof Error ? err.message : String(err);
                        }
                if (fileSearchKey === null) {
                        return 'File search is unavailable on this server (started without --tools)';
                }
+
                if (!fileSearchEnabled) {
                        return 'File search is disabled - enable "Search files" in Settings > Tools to use @-mentions';
                }
+
                return searchError ? `Search failed - ${searchError}` : 'No matching files or folders';
        });
 
 
        $effect(() => {
                if (typeof window === 'undefined') return;
+
                void toolsStore.resolveServerHome();
        });
 
 
        $effect(() => {
                const q = (query ?? '').trim();
+
                if (!isOpen || !q || !fileSearchEnabled) {
                        search.cancel();
                        searchResults = [];
                        searchError = null;
+
                        return;
                }
+
                search.setLoading(true);
                search.run(q);
        });
                // Enter-to-submit never fires mid-search.
                if (isOpen && event.key === KeyboardKey.ENTER) {
                        event.preventDefault();
+
                        if (nav.hoveredIndex >= 0 && displayedItems[nav.hoveredIndex]) {
                                handleSelect(displayedItems[nav.hoveredIndex]);
                        }
+
                        return true;
                }
 
index 11ca52049b04f7371ed30dc0ac1fca5272660132..82ba581d275d3ee84d25a35bc8d869edf841fc40 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import type { Snippet } from 'svelte';
-       import type { MCPServerSettingsEntry } from '$lib/types';
        import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { MCPServerSettingsEntry } from '$lib/types';
+       import type { Snippet } from 'svelte';
 
        interface Props {
                server: MCPServerSettingsEntry | undefined;
@@ -12,7 +12,7 @@
                subtitle?: Snippet;
        }
 
-       let { server, serverLabel, title, description, titleExtra, subtitle }: Props = $props();
+       let { description, server, serverLabel, subtitle, title, titleExtra }: Props = $props();
 
        let faviconUrl = $derived(server ? mcpStore.getServerFavicon(server.id) : null);
 </script>
index 04b85b24b235c993b61c3755ee0650b136ad5420..3ebb29b9b3c622697d7bfd877c665bda0a3502fb 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts" generics="T">
-       import type { Snippet } from 'svelte';
        import { SearchInput } from '$lib/components/app';
        import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
-       import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
        import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
+       import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
+       import type { Snippet } from 'svelte';
 
        interface Props {
                items: T[];
        }
 
        let {
-               items,
-               isLoading,
-               selectedIndex,
-               searchQuery = $bindable(),
-               showSearchInput,
-               searchPlaceholder = 'Search...',
-               emptyMessage,
                autofocus = false,
+               emptyMessage,
+               footer,
                inputRef = $bindable(null),
-               onSearchClose,
-               itemKey,
+               isLoading,
                item,
+               itemKey,
+               items,
+               onSearchClose,
+               scrollTrigger,
+               searchPlaceholder = 'Search...',
+               searchQuery = $bindable(),
+               selectedIndex,
+               showSearchInput,
                skeleton,
-               skeletonCount = 6,
-               footer,
-               scrollTrigger
+               skeletonCount = 6
        }: Props = $props();
 
        let listContainer = $state<HTMLDivElement | null>(null);
        // selectedIndex/items.length are untracked so hover and result replacement
        // never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
        useScrollActiveRow({
-               getTrigger: () => scrollTrigger,
+               dataIndex: 'picker',
                getContainer: () => listContainer,
-               getIndex: () => selectedIndex,
                getCount: () => items.length,
-               dataIndex: 'picker'
+               getIndex: () => selectedIndex,
+               getTrigger: () => scrollTrigger
        });
 </script>
 
index 0fb0ea0c0082c6b141b2a95b4b428be8d3caac74..51a51f8080a515e577b156f780a56f2add1b0f66 100644 (file)
        }
 
        let {
+               children,
                class: className = '',
-               isSelected = false,
+               dataIndex,
                disabled = false,
+               isSelected = false,
                onclick,
-               onmouseenter,
-               dataIndex,
-               children
+               onmouseenter
        }: Props = $props();
 </script>
 
index 5a2ab26fc266488d1a07a1aae3dc87834fae5d82..cbf7b972e5f54f6a60d36f3aa786328be06902db 100644 (file)
@@ -4,7 +4,7 @@
                showBadge?: boolean;
        }
 
-       let { titleWidth = 'w-48', showBadge = false }: Props = $props();
+       let { showBadge = false, titleWidth = 'w-48' }: Props = $props();
 </script>
 
 <div class="flex w-full items-start gap-3 rounded-lg px-3 py-2">
index 24ea8619f5754251756361e5f71f7b986685b7cf..b09d346f1328a84bd23f5042b80bee3fb1919343 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import type { Snippet } from 'svelte';
        import * as Popover from '$lib/components/ui/popover';
+       import type { Snippet } from 'svelte';
 
        interface Props {
                class?: string;
        }
 
        let {
+               children,
                class: className = '',
                isOpen = $bindable(false),
-               srLabel = 'Open picker',
                onClose,
                onKeydown,
-               children
+               srLabel = 'Open picker'
        }: Props = $props();
 </script>
 
index 22ec5311947a1cda3c40848e5c42201ab6587af1..126d08744a9305040e9d1e9042a86e24e043ad2c 100644 (file)
@@ -1,19 +1,19 @@
 <script lang="ts">
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { debounce, uuid } from '$lib/utils';
-       import { KeyboardKey } from '$lib/enums';
-       import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
-       import { SvelteMap } from 'svelte/reactivity';
        import {
-               ChatFormPickerPopover,
+               ChatFormPickerItemHeader,
                ChatFormPickerList,
                ChatFormPickerListItem,
-               ChatFormPickerItemHeader,
                ChatFormPickerListItemSkeleton,
+               ChatFormPickerPopover,
                ChatFormPromptPickerArgumentForm
        } from '$lib/components/app/chat';
        import Badge from '$lib/components/ui/badge/badge.svelte';
+       import { KeyboardKey } from '$lib/enums';
+       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { GetPromptResult, MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
+       import { debounce, uuid } from '$lib/utils';
+       import { SvelteMap } from 'svelte/reactivity';
 
        interface Props {
                class?: string;
        let {
                class: className = '',
                isOpen = false,
-               searchQuery = '',
                onClose,
-               onPromptLoadStart,
                onPromptLoadComplete,
-               onPromptLoadError
+               onPromptLoadError,
+               onPromptLoadStart,
+               searchQuery = ''
        }: Props = $props();
 
        let prompts = $state<MCPPromptInfo[]>([]);
@@ -89,7 +89,6 @@
 
                try {
                        const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
-
                        const initialized = await mcpStore.ensureInitialized(perChatOverrides);
 
                        if (!initialized) {
 
                        requestAnimationFrame(() => {
                                const firstInput = document.querySelector(`#arg-${args[0].name}`) as HTMLInputElement;
+
                                if (firstInput) {
                                        firstInput.focus();
                                }
                promptError = null;
 
                const placeholderId = uuid();
-
                const nonEmptyArgs = Object.fromEntries(
                        Object.entries(args).filter(([, value]) => value.trim() !== '')
                );
 
                try {
                        const result = await mcpStore.getPrompt(prompt.serverName, prompt.name, args);
+
                        onPromptLoadComplete?.(placeholderId, result);
                } catch (error) {
                        const errorMessage =
                                error instanceof Error ? error.message : 'Unknown error executing prompt';
+
                        onPromptLoadError?.(placeholderId, errorMessage);
                }
        }
 
                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
                        console.log('[ChatFormPickerMcpPrompts] Fetching completions for:', {
-                               serverName: selectedPrompt.serverName,
-                               promptName: selectedPrompt.name,
                                argName,
+                               promptName: selectedPrompt.name,
+                               serverName: selectedPrompt.serverName,
                                value
                        });
                }
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
                                console.log('[ChatFormPickerMcpPrompts] Autocomplete result:', {
                                        argName,
-                                       value,
                                        result,
-                                       suggestionsCount: result?.values.length ?? 0
+                                       suggestionsCount: result?.values.length ?? 0,
+                                       value
                                });
                        }
 
                        event.preventDefault();
                        event.stopPropagation();
                        handleCancelArgumentForm();
+
                        return;
                }
 
                        selectedIndex = selectedIndexBeforeArgumentForm;
                        selectedIndexBeforeArgumentForm = null;
                }
+
                selectedPrompt = null;
                promptArgs = {};
                promptError = null;
 
                if (event.key === KeyboardKey.ESCAPE) {
                        event.preventDefault();
+
                        if (selectedPrompt) {
                                // Return to prompt selection list, keeping the selected prompt active
                                handleCancelArgumentForm();
 
                if (event.key === KeyboardKey.ARROW_DOWN) {
                        event.preventDefault();
+
                        if (filteredPrompts.length > 0) {
                                selectedIndex = (selectedIndex + 1) % filteredPrompts.length;
                                scrollTrigger++;
 
                if (event.key === KeyboardKey.ARROW_UP) {
                        event.preventDefault();
+
                        if (filteredPrompts.length > 0) {
                                selectedIndex = selectedIndex === 0 ? filteredPrompts.length - 1 : selectedIndex - 1;
                                scrollTrigger++;
 
                if (event.key === KeyboardKey.ENTER && !selectedPrompt) {
                        event.preventDefault();
+
                        if (filteredPrompts[selectedIndex]) {
                                handlePromptClick(filteredPrompts[selectedIndex]);
                        }
        let filteredPrompts = $derived.by(() => {
                const sortedServers = mcpStore.getServers();
                const serverOrderMap = new Map(sortedServers.map((server, index) => [server.id, index]));
-
                const sortedPrompts = [...prompts].sort((a, b) => {
                        const orderA = serverOrderMap.get(a.serverName) ?? Number.MAX_SAFE_INTEGER;
                        const orderB = serverOrderMap.get(b.serverName) ?? Number.MAX_SAFE_INTEGER;
+
                        return orderA - orderB;
                });
-
                const query = (searchQuery || internalSearchQuery).toLowerCase();
+
                if (!query) return sortedPrompts;
 
                return sortedPrompts.filter(
index 92572b89522cc7766552c4e84af5c8e365351996..f665ce49c19d35c1122596b6d2d9c8bfa6c43235 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import type { MCPPromptInfo } from '$lib/types';
        import ChatFormPromptPickerArgumentInput from './ChatFormPromptPickerArgumentInput.svelte';
        import { Button } from '$lib/components/ui/button';
+       import type { MCPPromptInfo } from '$lib/types';
 
        interface Props {
                prompt: MCPPromptInfo;
        }
 
        let {
-               prompt,
-               promptArgs,
-               suggestions,
-               loadingSuggestions,
                activeAutocomplete,
                autocompleteIndex,
-               promptError,
-               onArgInput,
-               onArgKeydown,
+               loadingSuggestions,
                onArgBlur,
                onArgFocus,
+               onArgInput,
+               onArgKeydown,
+               onCancel,
                onSelectSuggestion,
                onSubmit,
-               onCancel
+               prompt,
+               promptArgs,
+               promptError,
+               suggestions
        }: Props = $props();
 </script>
 
index 638d10eeff88eda533e5f3dcbf9d73b9d0e2c2f1..074c69b841650b2450dc55cfd893503b0f1a435c 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import type { MCPPromptInfo } from '$lib/types';
-       import { fly } from 'svelte/transition';
        import { Input } from '$lib/components/ui/input';
        import { Label } from '$lib/components/ui/label';
+       import type { MCPPromptInfo } from '$lib/types';
+       import { fly } from 'svelte/transition';
 
        type PromptArgument = NonNullable<MCPPromptInfo['arguments']>[number];
 
 
        let {
                argument,
-               value = '',
-               suggestions = [],
-               isLoadingSuggestions = false,
-               isAutocompleteActive = false,
                autocompleteIndex = 0,
-               onInput,
-               onKeydown,
+               isAutocompleteActive = false,
+               isLoadingSuggestions = false,
                onBlur,
                onFocus,
-               onSelectSuggestion
+               onInput,
+               onKeydown,
+               onSelectSuggestion,
+               suggestions = [],
+               value = ''
        }: Props = $props();
 </script>
 
@@ -66,7 +66,7 @@
        {#if isAutocompleteActive && suggestions.length > 0}
                <div
                        class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
-                       transition:fly={{ y: -5, duration: 100 }}
+                       transition:fly={{ duration: 100, y: -5 }}
                >
                        {#each suggestions as suggestion, i (suggestion)}
                                <button
index 0d6cdcbfd680d269648c66c569b391667e89daf4..7ba97cf9b4a36340de2c291a28191862ed6ec88e 100644 (file)
        }
 
        let {
-               isCommandPickerOpen,
                commandQuery,
                commands = [],
-               onCommandPickerClose,
-               onCommandSelect,
-               isPromptPickerOpen,
-               promptSearchQuery,
+               isCommandPickerOpen,
                isMentionPickerOpen,
-               mentionQuery,
+               isPromptPickerOpen,
                mentionAnchor,
-               scopePath,
-               onPromptPickerClose,
-               onMentionPickerClose,
+               mentionQuery,
+               onCommandPickerClose,
+               onCommandSelect,
                onMentionOpened,
+               onMentionPickerClose,
                onMentionSelect,
-               onPromptLoadStart,
                onPromptLoadComplete,
-               onPromptLoadError
+               onPromptLoadError,
+               onPromptLoadStart,
+               onPromptPickerClose,
+               promptSearchQuery,
+               scopePath
        }: Props = $props();
 
        let commandPickerRef: ChatFormCommandPicker | undefined = $state(undefined);
index 8ef56d7ee797fa5d39e00f16bfe79bf57722f8b9..1ad5ba553295d1077b951522952ca951a7872a32 100644 (file)
@@ -52,6 +52,7 @@
        // the picker/paste flows can address either renderer through one handle.
        export function getCaretOffset(): number {
                if (!textareaElement) return 0;
+
                return textareaElement.selectionStart ?? textareaElement.value.length;
        }
 
index 108cf1b19cb2d7a3021640fea3e1decbb345a068..fb0b31817ea604d668ed13f92e5ab5d89de4a15c 100644 (file)
@@ -1,23 +1,9 @@
 <script lang="ts">
-       import { FolderOpen } from '@lucide/svelte';
-       import { ToolsService } from '$lib/services/tools.service';
-       import { toolsStore } from '$lib/stores/tools.svelte';
-       import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
-       import {
-               abbreviateHome,
-               buildCaseInsensitiveGlob,
-               joinPath,
-               lastPathSegment,
-               runGlobSearchWithChildren,
-               type GlobEntry
-       } from '$lib/utils';
-       import * as Popover from '$lib/components/ui/popover';
-       import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
-       import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
-       import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
-       import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
        import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte';
        import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte';
+       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,
                SEARCH_LIMIT,
                SEARCH_MAX_DEPTH
        } 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';
+       import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
+       import { ToolsService } from '$lib/services/tools.service';
+       import { toolsStore } from '$lib/stores/tools.svelte';
+       import {
+               abbreviateHome,
+               buildCaseInsensitiveGlob,
+               type GlobEntry,
+               joinPath,
+               lastPathSegment,
+               runGlobSearchWithChildren
+       } from '$lib/utils';
 
        // Microtask delay so the popover's focus scope tears down first.
        const FOCUS_DELAY_MS = 0;
 
        let {
                class: className = '',
-               disabled = false,
+               customAnchor = null,
                directory = null,
+               disabled = false,
                isOpen,
-               query = $bindable(''),
-               customAnchor = null,
                onChange,
                onClose,
-               onOpen
+               onOpen,
+               query = $bindable('')
        }: Props = $props();
 
        // File System Access API is opt-in (Chrome / Edge / Opera): the popover
@@ -88,8 +88,8 @@
        let listContainer = $state<HTMLDivElement | null>(null);
 
        const nav = usePickerNavigation({
-               isOpen: () => isOpen,
                count: () => queryResults.length,
+               isOpen: () => isOpen,
                onClose: closePicker,
                onSelect: (index) => commit(queryResults[index])
        });
        // Resolve home eagerly so the chip can abbreviate before the picker opens.
        $effect(() => {
                if (typeof window === 'undefined') return;
+
                void toolsStore.resolveServerHome();
        });
 
        // HTML `autofocus` is unreliable on dynamically shown elements.
        $effect(() => {
                if (!isOpen) return;
+
                setTimeout(() => searchInputRef?.focus(), FOCUS_DELAY_MS);
        });
 
        $effect(() => {
                if (!isOpen) return;
+
                const q = query.trim();
+
                nav.reset(-1);
+
                if (q && fileSearchEnabled) {
                        search.run(q);
                } else {
        });
 
        useScrollActiveRow({
-               getTrigger: () => nav.scrollTrigger,
+               dataIndex: 'result',
                getContainer: () => listContainer,
-               getIndex: () => nav.hoveredIndex,
                getCount: () => queryResults.length,
-               dataIndex: 'result'
+               getIndex: () => nav.hoveredIndex,
+               getTrigger: () => nav.scrollTrigger
        });
 
        let searchScope = $state(HOME_TILDE);
        // An exactly-typed directory is "entered": the shared search lists its
        // children too, so path navigation does not require a trailing slash.
        const search = useDebouncedSearch({
-               debounceMs: SEARCH_DEBOUNCE_MS,
                canRun: () => isOpen && fileSearchEnabled,
+               debounceMs: SEARCH_DEBOUNCE_MS,
                getQuery: () => query.trim(),
                run: async (q, signal, isCurrent) => {
                        const trimmed = q.trim();
+
                        if (!trimmed) {
                                queryResults = [];
                                searchError = null;
                                nav.reset(-1);
                                searchScope = homeBase ?? HOME_TILDE;
+
                                return;
                        }
 
                                        signal,
                                        { type: GlobSearchType.DIR }
                                );
+
                                if (!isCurrent()) return;
+
                                if (res.error) {
                                        queryResults = [];
                                        nav.reset(-1);
                                        searchError = res.error;
+
                                        return;
                                }
 
                                searchScope = res.exactDir ?? res.args.path;
                                queryResults = res.entries.map((e) => e.path).slice(0, MAX_RESULTS_SHOWN);
+
                                if (queryResults.length > 0) {
                                        nav.reset(0);
                                        nav.bumpScroll(); // scroll the list back to the top (first item is hovered)
                                } else {
                                        nav.reset(-1);
                                }
+
                                searchError = null;
                        } catch (err) {
                                if (!isCurrent() || signal.aborted) return;
+
                                queryResults = [];
                                nav.reset(-1);
                                searchError = err instanceof Error ? err.message : String(err);
 
        function setDirectory(value: string) {
                const trimmed = value.trim();
+
                if (!trimmed) return;
+
                onChange?.(trimmed);
        }
 
        async function resolveNativeName(name: string): Promise<string | null> {
                try {
                        const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
-                               path: homeBase ?? HOME_TILDE,
-                               type: GlobSearchType.DIR,
                                include: buildCaseInsensitiveGlob(name),
+                               limit: NATIVE_LIMIT,
                                max_depth: NATIVE_MAX_DEPTH,
-                               limit: NATIVE_LIMIT
+                               path: homeBase ?? HOME_TILDE,
+                               type: GlobSearchType.DIR
                        });
                        const base = typeof res.base === 'string' ? res.base : '';
                        const entries = Array.isArray(res.entries) ? (res.entries as GlobEntry[]) : [];
                        const match = entries.find(
                                (e) => lastPathSegment(e.path).toLowerCase() === name.toLowerCase()
                        );
+
                        return match ? joinPath(base, match.path) : null;
                } catch {
                        return null;
 
        async function browseNative() {
                if (disabled || !window.showDirectoryPicker) return;
+
                try {
                        const handle = await window.showDirectoryPicker();
                        const path = await resolveNativeName(handle.name);
+
                        if (path) {
                                setDirectory(path);
                                closePicker();
                } catch (err) {
                        // user cancelled - silently ignore; other errors are logged
                        if (err instanceof DOMException && err.name === 'AbortError') return;
+
                        console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err);
                }
        }
 
        function handleSubmit() {
                const value = query.trim();
+
                if (!value) {
                        closePicker();
+
                        return;
                }
+
                setDirectory(value);
                closePicker();
        }
        function handleKeydown(event: KeyboardEvent) {
                if (event.key === KeyboardKey.ENTER) {
                        event.preventDefault();
+
                        if (nav.hoveredIndex >= 0 && queryResults[nav.hoveredIndex]) {
                                commit(queryResults[nav.hoveredIndex]);
                        } else if (queryResults.length === 0) {
        function handleDismiss(event?: MouseEvent) {
                event?.stopPropagation();
                event?.preventDefault();
+
                if (directory) {
                        clearDirectory(event);
                }
index 2882f557c84fe74d0cf20e57cad4ae9628e8847b..23661d223d5329b4519566d249caa93e8de28dd5 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
        import { Folder, X } from '@lucide/svelte';
-       import { abbreviateWorkingDir } from '$lib/utils';
-       import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
-       import * as Tooltip from '$lib/components/ui/tooltip';
        import { ActionIcon } from '$lib/components/app/actions';
+       import * as Tooltip from '$lib/components/ui/tooltip';
+       import { SET_WORKING_DIRECTORY_LABEL } from '$lib/constants';
+       import { abbreviateWorkingDir } from '$lib/utils';
 
        interface Props {
                directory?: string | null;
 
        let {
                directory = null,
-               homeBase = null,
                disabled = false,
-               showTooltip = false,
-               onClear
+               homeBase = null,
+               onClear,
+               showTooltip = false
        }: Props = $props();
 
        const displayLabel = $derived(
index d62eb88242db3aeadd23ce087e31bae4b522b9ab..32de06189e5925f9371a639ad760f57aee54f293 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
        import { Folder } from '@lucide/svelte';
-       import { fly } from 'svelte/transition';
-       import { highlightMatch } from '$lib/utils';
        import { cn } from '$lib/components/ui/utils';
+       import { highlightMatch } from '$lib/utils';
+       import { fly } from 'svelte/transition';
 
        // Fly-in transition for the results list.
        const FLY_Y_PX = -4;
        }
 
        let {
-               results,
+               container = $bindable(null),
+               error,
                hoveredIndex,
                isSearching,
-               error,
-               rawQuery,
-               container = $bindable(null),
                onCommit,
-               onHover
+               onHover,
+               rawQuery,
+               results
        }: Props = $props();
 </script>
 
 <div
        bind:this={container}
        class="max-h-48 overflow-y-auto py-2"
-       transition:fly={{ y: FLY_Y_PX, duration: FLY_DURATION_MS }}
+       transition:fly={{ duration: FLY_DURATION_MS, y: FLY_Y_PX }}
 >
        {#if isSearching && results.length === 0}
                <div class="px-2 py-1.5 text-sm text-muted-foreground">Searching...</div>
index afe90f66fe258cceec656b6276bc1c97cf6253c0..13702102c4ef83bcd8e5dbda565e1055a772899d 100644 (file)
@@ -1,24 +1,24 @@
 <script lang="ts">
        import { goto } from '$app/navigation';
-       import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
-       import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { DatabaseService } from '$lib/services/database.service';
-       import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
-       import { REASONING_TAGS } from '$lib/constants/agentic';
-       import { MessageRole, AttachmentType, AgenticSectionType } from '$lib/enums';
        import {
                ChatMessageAssistant,
-               ChatMessageUser,
-               ChatMessageSystem,
+               ChatMessageMcpPrompt,
                ChatMessageSynthetic,
-               ChatMessageMcpPrompt
+               ChatMessageSystem,
+               ChatMessageUser
        } from '$lib/components/app/chat';
-       import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
-       import { deriveAgenticSections } from '$lib/utils';
-       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
+       import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
+       import { REASONING_TAGS } from '$lib/constants/agentic';
        import { ROUTES } from '$lib/constants/routes';
+       import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
+       import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
+       import { DatabaseService } from '$lib/services/database.service';
+       import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
+       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { isMobile } from '$lib/stores/viewport.svelte';
+       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
+       import { deriveAgenticSections } from '$lib/utils';
+       import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
 
        interface Props {
                class?: string;
 
        let {
                class: className = '',
-               message,
-               toolMessages = [],
                isLastAssistantMessage = false,
                isLastUserMessage = false,
+               message,
                nextAssistantMessage = null,
-               siblingInfo = null
+               siblingInfo = null,
+               toolMessages = []
        }: Props = $props();
 
        const chatActions = getChatActionsContext();
                                case AgenticSectionType.REASONING:
                                case AgenticSectionType.REASONING_PENDING:
                                        parts.push(`${REASONING_TAGS.START}\n${section.content}\n${REASONING_TAGS.END}`);
+
                                        break;
 
                                case AgenticSectionType.TEXT:
                                        parts.push(section.content);
+
                                        break;
 
                                case AgenticSectionType.TOOL_CALL:
        let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
 
        setMessageEditContext({
-               get isEditing() {
-                       return isEditing;
-               },
+               cancel: handleCancelEdit,
                get editedContent() {
                        return editedContent;
                },
                get editedUploadedFiles() {
                        return editedUploadedFiles;
                },
+               get isEditing() {
+                       return isEditing;
+               },
+               get messageRole() {
+                       return message.role;
+               },
                get originalContent() {
                        return message.role === MessageRole.ASSISTANT
                                ? (rawEditContent ?? message.content)
                get originalExtras() {
                        return message.extra || [];
                },
-               get showSaveOnlyOption() {
-                       return showSaveOnlyOption;
-               },
-               get showBranchAfterEditOption() {
-                       return showBranchAfterEditOption;
-               },
-               get shouldBranchAfterEdit() {
-                       return shouldBranchAfterEdit;
-               },
-               get messageRole() {
-                       return message.role;
-               },
                get rawEditContent() {
                        return rawEditContent;
                },
+               save: handleSaveEdit,
+               saveOnly: handleSaveEditOnly,
                setContent: (content: string) => {
                        editedContent = content;
                },
                setExtras: (extras: DatabaseMessageExtra[]) => {
                        editedExtras = extras;
                },
+               setShouldBranchAfterEdit: (value: boolean) => {
+                       shouldBranchAfterEdit = value;
+               },
                setUploadedFiles: (files: ChatUploadedFile[]) => {
                        editedUploadedFiles = files;
                },
-               setShouldBranchAfterEdit: (value: boolean) => {
-                       shouldBranchAfterEdit = value;
+               get shouldBranchAfterEdit() {
+                       return shouldBranchAfterEdit;
+               },
+               get showBranchAfterEditOption() {
+                       return showBranchAfterEditOption;
+               },
+               get showSaveOnlyOption() {
+                       return showSaveOnlyOption;
                },
-               save: handleSaveEdit,
-               saveOnly: handleSaveEditOnly,
-               cancel: handleCancelEdit,
                startEdit: handleEdit
        });
 
        let mcpPromptExtra = $derived.by(() => {
                if (message.role !== MessageRole.USER) return null;
+
                if (message.content.trim()) return null;
+
                if (!message.extra || message.extra.length !== 1) return null;
 
                const extra = message.extra[0];
 
        function handleEdit() {
                isEditing = true;
+
                // Clear temporary placeholder content for system messages
                if (message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER) {
                        editedContent = '';
        // After the system message flow ends, hand focus to the main chat form
        function focusMainChatForm() {
                if (isMobile.current) return;
+
                document.querySelector<HTMLTextAreaElement>('.chat-screen-form-wrapper textarea')?.focus();
        }
 
                        // If content is empty, remove without deleting children
                        if (!newContent) {
                                const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
+
                                isEditing = false;
+
                                if (conversationDeleted) {
                                        goto(ROUTES.START);
                                } else {
                                        focusMainChatForm();
                                }
+
                                return;
                        }
 
                        await DatabaseService.updateMessage(message.id, { content: newContent });
                        const index = conversationsStore.findMessageIndex(message.id);
+
                        if (index !== -1) {
                                conversationsStore.updateMessageAtIndex(index, { content: newContent });
                        }
+
                        focusMainChatForm();
                } else if (message.role === MessageRole.USER) {
                        const finalExtras = await getMergedExtras();
+
                        chatActions.editWithBranching(message, editedContent.trim(), finalExtras);
                } else {
                        // For assistant messages, preserve exact content including trailing whitespace
                if (message.role === MessageRole.USER) {
                        // For user messages, trim to avoid accidental whitespace
                        const finalExtras = await getMergedExtras();
+
                        chatActions.editUserMessagePreserveResponses(message, editedContent.trim(), finalExtras);
                }
 
index ec1214d180bf4ebf06f60032f3182e7d9701d409..14ca160ed856fe83d7064b1cc90292eb7842dc4c 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import {
-               ChatMessageAgenticContent,
                ChatMessageActionIcons,
+               ChatMessageAgenticContent,
                ChatMessageAssistantModel,
                ChatMessageAssistantProcessingInfo,
                ChatMessageAssistantRawOutput,
@@ -9,14 +9,13 @@
                ChatMessageEditForm
        } from '$lib/components/app';
        import { getMessageEditContext } from '$lib/contexts';
-       import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
-       import { chatStore, isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
-       import { modelLoadProgressText } from '$lib/utils';
        import { MessageRole } from '$lib/enums';
-       import { config } from '$lib/stores/settings.svelte';
-       import { isRouterMode } from '$lib/stores/server.svelte';
+       import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
+       import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
        import { modelsStore } from '$lib/stores/models.svelte';
-
+       import { isRouterMode } from '$lib/stores/server.svelte';
+       import { config } from '$lib/stores/settings.svelte';
+       import { modelLoadProgressText } from '$lib/utils';
        import { hasAgenticContent } from '$lib/utils';
 
        interface Props {
@@ -49,7 +48,6 @@
                deletionInfo,
                isLastAssistantMessage = false,
                message,
-               toolMessages = [],
                onConfirmDelete,
                onContinue,
                onCopy,
@@ -61,7 +59,8 @@
                onShowDeleteDialogChange,
                showDeleteDialog,
                siblingInfo = null,
-               textareaElement = $bindable()
+               textareaElement = $bindable(),
+               toolMessages = []
        }: Props = $props();
 
        // Get edit context
 
                if (!userMessageEl) {
                        lastUserMessageHeight = 0;
+
                        return;
                }
 
                const updateHeight = () => {
                        const rect = userMessageEl.getBoundingClientRect();
                        const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
+
                        lastUserMessageHeight = Math.round(rect.height + marginTop);
                };
 
                updateHeight();
 
                const resizeObserver = new ResizeObserver(updateHeight);
+
                resizeObserver.observe(userMessageEl);
 
                return () => {
index 76b45ec94e097669f72072ddf2944e0814a62856..d27e3e01d76930cf3637e9fcabed32f0ad3b4f35 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
        import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
-       import { copyToClipboard } from '$lib/utils';
-       import { modelsStore } from '$lib/stores/models.svelte';
        import { ServerModelStatus } from '$lib/enums';
+       import { modelsStore } from '$lib/stores/models.svelte';
+       import { copyToClipboard } from '$lib/utils';
 
        interface Props {
                displayedModel: string | null;
@@ -11,7 +11,7 @@
                onRegenerate: (modelOverride?: string) => void;
        }
 
-       let { displayedModel, isRouter, isLoading, onRegenerate }: Props = $props();
+       let { displayedModel, isLoading, isRouter, onRegenerate }: Props = $props();
 
        let pendingModel = $state<string | null>(null);
 
@@ -38,6 +38,7 @@
                        }
 
                        onRegenerate(modelName);
+
                        return true;
                }}
        />
index 356512ecb73dd8afa4fad284dcca4e855c8092c4..657157956426e33466d18cbc58c3b30b520ec2ef 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { fade } from 'svelte/transition';
        import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
+       import { fade } from 'svelte/transition';
 
        interface Props {
                modelLoadingText: string | null;
@@ -8,7 +8,7 @@
                position: 'top' | 'bottom';
        }
 
-       let { modelLoadingText, processingState, position }: Props = $props();
+       let { modelLoadingText, position, processingState }: Props = $props();
 
        const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
 </script>
index 30ce16be934e82c8430643a61ebe383a0de6988d..d69337960e446ff07643160d9cb18f270162b5f2 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { deriveAgenticSections, buildAssistantRawOutput } from '$lib/utils';
+       import { buildAssistantRawOutput, deriveAgenticSections } from '$lib/utils';
 
        interface Props {
                message: DatabaseMessage;
@@ -10,6 +10,7 @@
 
        let rawOutputContent = $derived.by(() => {
                const sections = deriveAgenticSections(message, toolMessages, [], false);
+
                return buildAssistantRawOutput(sections);
        });
 </script>
index 4cc4080c3b7c52f6da8e547c96a9f940348ecad9..5c5ac8c25c8256460aecfb69c16b103f4d78d57b 100644 (file)
@@ -10,7 +10,7 @@
                showMessageStats: boolean;
        }
 
-       let { message, isLoading, processingState, showMessageStats }: Props = $props();
+       let { isLoading, message, processingState, showMessageStats }: Props = $props();
 </script>
 
 {#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
index 0b0133060fda8628a0fef5b11046a82bf18978fb..3869e19a0df4d299dd032ca543551946d7649859 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { Folder, FolderX } from '@lucide/svelte';
-       import { parseCwdMessage } from '$lib/utils';
        import type { DatabaseMessage } from '$lib/types';
+       import { parseCwdMessage } from '$lib/utils';
 
        interface Props {
                class?: string;
index 2dcb36baf6800b8119afdd69724b0f98c24bfc06..d3aa7251e09674044fbd199fd7cc5a348ec7af27 100644 (file)
@@ -5,7 +5,7 @@
                ChatMessageMcpPromptContent
        } from '$lib/components/app';
        import { getMessageEditContext } from '$lib/contexts';
-       import { MessageRole, McpPromptVariant } from '$lib/enums';
+       import { McpPromptVariant, MessageRole } from '$lib/enums';
        import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
 
        interface Props {
 
        let {
                class: className = '',
-               message,
-               mcpPrompt,
-               siblingInfo = null,
-               showDeleteDialog,
                deletionInfo,
+               mcpPrompt,
+               message,
+               onConfirmDelete,
                onCopy,
-               onEdit,
                onDelete,
-               onConfirmDelete,
+               onEdit,
                onNavigateToSibling,
-               onShowDeleteDialogChange
+               onShowDeleteDialogChange,
+               showDeleteDialog,
+               siblingInfo = null
        }: Props = $props();
 
        // Get edit context
index 3d5dec3b6ac956e8cb3387d85178699088b07389..3d45ca4dac2ede8287a89520afb20659740ac09b 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
+       import { TruncatedText } from '$lib/components/app/misc';
        import { Card } from '$lib/components/ui/card';
-       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
+       import * as Tooltip from '$lib/components/ui/tooltip';
+       import { McpPromptVariant } from '$lib/enums';
        import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
        import { SvelteMap } from 'svelte/reactivity';
-       import { McpPromptVariant } from '$lib/enums';
-       import { TruncatedText } from '$lib/components/app/misc';
-       import * as Tooltip from '$lib/components/ui/tooltip';
 
        interface ContentPart {
                text: string;
 
        let {
                class: className = '',
-               prompt,
-               variant = McpPromptVariant.MESSAGE,
                isLoading = false,
-               loadError
+               loadError,
+               prompt,
+               variant = McpPromptVariant.MESSAGE
        }: Props = $props();
 
        let hoveredArgKey = $state<string | null>(null);
 
        let contentParts = $derived.by((): ContentPart[] => {
                if (!prompt.content || !hasArguments) {
-                       return [{ text: prompt.content || '', argKey: null }];
+                       return [{ argKey: null, text: prompt.content || '' }];
                }
 
                const parts: ContentPart[] = [];
+
                let remaining = prompt.content;
 
                const valueToKey = new SvelteMap<string, string>();
+
                for (const [key, value] of argumentEntries) {
                        if (value && value.trim()) {
                                valueToKey.set(value, key);
 
                        for (const value of sortedValues) {
                                const index = remaining.indexOf(value);
+
                                if (index !== -1 && (earliestMatch === null || index < earliestMatch.index)) {
-                                       earliestMatch = { index, value, key: valueToKey.get(value)! };
+                                       earliestMatch = { index, key: valueToKey.get(value)!, value };
                                }
                        }
 
                        if (earliestMatch) {
                                if (earliestMatch.index > 0) {
-                                       parts.push({ text: remaining.slice(0, earliestMatch.index), argKey: null });
+                                       parts.push({ argKey: null, text: remaining.slice(0, earliestMatch.index) });
                                }
 
-                               parts.push({ text: earliestMatch.value, argKey: earliestMatch.key });
+                               parts.push({ argKey: earliestMatch.key, text: earliestMatch.value });
                                remaining = remaining.slice(earliestMatch.index + earliestMatch.value.length);
                        } else {
-                               parts.push({ text: remaining, argKey: null });
+                               parts.push({ argKey: null, text: remaining });
 
                                break;
                        }
index 1597df2ab147ed5544982727695bd88fa255097e..aa4160c3f7be02214ccd86a3175d32dbc8840343 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { parseCwdMessage } from '$lib/utils';
-       import type { DatabaseMessage } from '$lib/types';
        import ChatMessageCwdChange from './ChatMessageCwdChange.svelte';
+       import type { DatabaseMessage } from '$lib/types';
+       import { parseCwdMessage } from '$lib/utils';
 
        interface Props {
                class?: string;
index 04185814dbf62e06498b9815ca1519759febdf2e..5a1e4f21386e9946afc6bb0c6b5595e297a1ffb3 100644 (file)
 
        let {
                class: className = '',
-               message,
-               siblingInfo = null,
-               showDeleteDialog,
                deletionInfo,
+               message,
+               onConfirmDelete,
                onCopy,
-               onEdit,
                onDelete,
-               onConfirmDelete,
+               onEdit,
                onNavigateToSibling,
                onShowDeleteDialogChange,
+               showDeleteDialog,
+               siblingInfo = null,
                textareaElement = $bindable()
        }: Props = $props();
 
index 1d6cccc9f3ca67ccf73def798c80737eaee8a2d6..5bbaa4ea311ba48ccb120d4da4adb22b0867fa7e 100644 (file)
@@ -1,12 +1,4 @@
 <script lang="ts">
-       import { BuiltInTool } from '$lib/enums';
-       import {
-               extractSearchQuery,
-               extractSearchResults,
-               isWebSearchToolName,
-               type AgenticSection
-       } from '$lib/utils';
-       import type { DatabaseMessageExtra } from '$lib/types';
        import ChatMessageToolCallBlockDefault from './ChatMessageToolCallBlockDefault.svelte';
        import ChatMessageToolCallBlockEditFile from './ChatMessageToolCallBlockEditFile.svelte';
        import ChatMessageToolCallBlockExecShellCommand from './ChatMessageToolCallBlockExecShellCommand.svelte';
        import ChatMessageToolCallBlockRunJavascript from './ChatMessageToolCallBlockRunJavascript.svelte';
        import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
        import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
+       import { BuiltInTool } from '$lib/enums';
+       import type { DatabaseMessageExtra } from '$lib/types';
+       import {
+               type AgenticSection,
+               extractSearchQuery,
+               extractSearchResults,
+               isWebSearchToolName
+       } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -28,7 +28,7 @@
                onToggle?: () => void;
        }
 
-       let { section, attachments, open, isStreaming, isExecuting, onToggle }: Props = $props();
+       let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();
 
        const searchResults = $derived(extractSearchResults(section.toolResult));
        const searchQuery = $derived(extractSearchQuery(section.toolArgs));
index 92652a0a86ab5d354ffe526070b1deedfb71c90c..34dfde78b8958bd4065ba1e1752ef14f0e4f21d0 100644 (file)
@@ -3,19 +3,19 @@
        // Renders section.toolArgs / section.toolResult directly using the
        // shared chrome shell.
 
+       import ToolCallBlock from './ToolCallBlock.svelte';
        import { Loader2 } from '@lucide/svelte';
        import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
-       import { FileTypeText, ToolResultKind } from '$lib/enums';
        import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
+       import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
+       import { FileTypeText, ToolResultKind } from '$lib/enums';
+       import type { DatabaseMessageExtra } from '$lib/types';
        import {
+               type AgenticSection,
                classifyToolResult,
                formatJsonPretty,
-               parseToolResultWithImages,
-               type AgenticSection
+               parseToolResultWithImages
        } from '$lib/utils';
-       import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
-       import type { DatabaseMessageExtra } from '$lib/types';
-       import ToolCallBlock from './ToolCallBlock.svelte';
 
        interface Props {
                section: AgenticSection;
@@ -25,7 +25,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, attachments, onToggle }: Props = $props();
+       let { attachments, isStreaming, onToggle, open, section }: Props = $props();
 
        const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
        const outputKind = $derived(classifyToolResult(section.toolResult));
index f8618864c95374416eb1f1956ef9944a6ad76001..5ee9d4936e8047a071c98f4c16441948eecc1b70 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
+       import { parseEditFileMeta } from './parsers/edit-file';
+       import ToolCallBlock from './ToolCallBlock.svelte';
        import { XCircle } from '@lucide/svelte';
        import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
-       import { computeLineDiff, prefixFor, abbreviateHome, type AgenticSection } from '$lib/utils';
        import { toolsStore } from '$lib/stores/tools.svelte';
-       import { parseEditFileMeta } from './parsers/edit-file';
-       import ToolCallBlock from './ToolCallBlock.svelte';
+       import { abbreviateHome, type AgenticSection, computeLineDiff, prefixFor } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -13,7 +13,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const editFileMeta = $derived(parseEditFileMeta(section));
        const home = $derived(toolsStore.serverHome);
index 8128ee30dd18a800fca86512ebf4614b297f5811..b2fa8b331c3ffa40aa2064ed241b9fe5eadd8fb1 100644 (file)
@@ -6,26 +6,26 @@
        // The scroll-to-bottom auto-scroll logic mirrors what was here
        // before extraction.
 
-       import { Check, Loader2, XCircle, AlertTriangle } from '@lucide/svelte';
+       import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
+       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 { config } from '$lib/stores/settings.svelte';
        import { TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+       import { config } from '$lib/stores/settings.svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
+       import type { DatabaseMessageExtra } from '$lib/types';
        import {
                abbreviateHome,
+               type AgenticSection,
+               type ExecShellExitStatus,
                highlightCode,
                isExitCodeSummaryLine,
                parseExecShellCommandError,
                parseExecShellCommandExitStatus,
                parseToolResultWithImages,
-               type AgenticSection,
-               type ExecShellExitStatus,
                type ToolResultLine
        } from '$lib/utils';
-       import { toolsStore } from '$lib/stores/tools.svelte';
-       import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
-       import type { DatabaseMessageExtra } from '$lib/types';
-       import ToolCallBlock from './ToolCallBlock.svelte';
 
        interface Props {
                section: AgenticSection;
@@ -39,7 +39,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, isExecuting = false, attachments, onToggle }: Props = $props();
+       let { attachments, isExecuting = false, isStreaming, onToggle, open, section }: Props = $props();
 
        // `isLive` covers all in-flight phases: pre-chunk spinner and
        // streaming itself. Frozen output (tool done while agent continues)
 
        function isAtBottom(): boolean {
                if (!scrollEl) return false;
+
                return (
                        scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
                        SCROLL_BOTTOM_THRESHOLD_PX
 
        function scrollToBottomOnFrame() {
                if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
+
                pendingFrame = requestAnimationFrame(() => {
                        pendingFrame = null;
 
 
        function handleScrollEvent() {
                if (!scrollEl) return;
+
                const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
+
                if (isScrollingUp && !isAtBottom()) {
                        userScrolledUp = true;
                } else if (isAtBottom()) {
                        userScrolledUp = false;
                }
+
                lastScrollTop = scrollEl.scrollTop;
        }
 
        $effect(() => {
                void section.toolResult;
+
                if (!scrollEl || !autoScroll) return;
+
                scrollToBottomOnFrame();
        });
 
                if (!scrollEl || !autoScroll) return;
 
                const observer = new MutationObserver(() => scrollToBottomOnFrame());
+
                observer.observe(scrollEl, {
+                       characterData: true,
                        childList: true,
-                       subtree: true,
-                       characterData: true
+                       subtree: true
                });
 
                return () => observer.disconnect();
index 7b674e8184d5e4788cd7497c0c37ef97e8f520f5..37f7350fb7079177bd9279037b464dffc6921249 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { XCircle } from '@lucide/svelte';
-       import { abbreviateHome, type AgenticSection } from '$lib/utils';
-       import { toolsStore } from '$lib/stores/tools.svelte';
        import { parseFileGlobSearchMeta } from './parsers/file-glob-search';
        import ToolCallBlock from './ToolCallBlock.svelte';
+       import { XCircle } from '@lucide/svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { abbreviateHome, type AgenticSection } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -12,7 +12,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const fileGlobMeta = $derived(parseFileGlobSearchMeta(section));
        const home = $derived(toolsStore.serverHome);
index e0c701deaa580effe5b9178501a36998864c3d25..4c525daa7b07921e25b2cd5ec31f72e1978359e9 100644 (file)
@@ -8,7 +8,7 @@
                isStreaming?: boolean;
        }
 
-       let { section, isStreaming = false }: Props = $props();
+       let { isStreaming = false, section }: Props = $props();
 
        const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
        const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
 
                try {
                        const parsed: unknown = JSON.parse(toolResultString);
+
                        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                                const obj = parsed as Record<string, unknown>;
+
                                if (typeof obj.error === 'string') return { errorMessage: obj.error };
+
                                if (typeof obj.result === 'string') return { dateString: obj.result.trim() };
                        }
                } catch {
index d45e361574875f40349f59a09570c5e34ad98196..7a975dc1b52846d7fb9fe04eabe4dfeffbcdf5bb 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
        import { Info, Loader2 } from '@lucide/svelte';
        import { AgenticSectionType } from '$lib/enums';
-       import { abbreviateHome, type AgenticSection } from '$lib/utils';
        import { toolsStore } from '$lib/stores/tools.svelte';
+       import { abbreviateHome, type AgenticSection } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
                isStreaming?: boolean;
        }
 
-       let { section, isStreaming = false }: Props = $props();
+       let { isStreaming = false, section }: Props = $props();
 
        const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
        const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
 
                try {
                        const parsed: unknown = JSON.parse(toolResultString);
+
                        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                                const obj = parsed as Record<string, unknown>;
+
                                if (typeof obj.error === 'string') return { errorMessage: obj.error };
+
                                return {
-                                       os: typeof obj.os === 'string' ? obj.os : undefined,
-                                       cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined
+                                       cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
+                                       os: typeof obj.os === 'string' ? obj.os : undefined
                                };
                        }
                } catch {
index ac805a2e4d37e859002df122cb16bd010ca55713..86db568dea79565c89369ff59b4eacbb331ef791 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { XCircle } from '@lucide/svelte';
-       import { abbreviateHome, type AgenticSection } from '$lib/utils';
-       import { toolsStore } from '$lib/stores/tools.svelte';
        import { parseGrepSearchMeta } from './parsers/grep-search';
        import ToolCallBlock from './ToolCallBlock.svelte';
+       import { XCircle } from '@lucide/svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { abbreviateHome, type AgenticSection } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -12,7 +12,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const grepMeta = $derived(parseGrepSearchMeta(section));
        const home = $derived(toolsStore.serverHome);
index a99ff9ceedcd398460220f709e875dc182674468..c4aa28a358f1a4e455f5c7c1d16e3c2975c4e79f 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
+       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 { type AgenticSection } from '$lib/utils';
-       import { parseReadFileMeta } from './parsers/read-file';
-       import ToolCallBlock from './ToolCallBlock.svelte';
 
        interface Props {
                section: AgenticSection;
@@ -12,7 +12,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const readFileMeta = $derived(parseReadFileMeta(section));
 </script>
index 707d83d7377ef327841527c4099a3b22cdc61348..38724e8d7717381e4dd47b41d898fca88eda44d9 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { XCircle, Terminal } from '@lucide/svelte';
-       import { SyntaxHighlightedCode } from '$lib/components/app';
-       import { FileTypeText } from '$lib/enums';
-       import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
-       import { getBuiltinToolUi, type AgenticSection } from '$lib/utils';
        import { parseRunJavascriptMeta } from './parsers/run-javascript';
        import ToolCallBlock from './ToolCallBlock.svelte';
+       import { Terminal, XCircle } from '@lucide/svelte';
+       import { SyntaxHighlightedCode } from '$lib/components/app';
+       import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
+       import { FileTypeText } from '$lib/enums';
+       import { type AgenticSection, getBuiltinToolUi } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -14,7 +14,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const runJsMeta = $derived(parseRunJavascriptMeta(section));
        const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
index 60862dd0635354e3d2b12e3328df29b4e9154711..2a9cc493fd2b6cbe9b2157e54127908088581ef1 100644 (file)
@@ -1,17 +1,17 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
        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 { AgenticSectionType } from '$lib/enums';
        import { mcpStore } from '$lib/stores/mcp.svelte';
        import {
-               extractSearchResults,
+               type AgenticSection,
                extractSearchQuery,
+               extractSearchResults,
                faviconForUrl,
                sanitizeExternalUrl,
-               type SearchResult,
-               type AgenticSection
+               type SearchResult
        } from '$lib/utils';
 
        interface Props {
@@ -21,7 +21,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open = $bindable(false), isStreaming = false, onToggle }: Props = $props();
+       let { isStreaming = false, onToggle, open = $bindable(false), section }: Props = $props();
 
        const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
        const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
@@ -43,6 +43,7 @@
        // retrospective.
        const title = $derived.by(() => {
                const verb = showSpinner ? 'Searching' : 'Searched';
+
                return query ? `${verb} web for "${query}"` : `${verb} web`;
        });
 
 
        function formatPublishDate(iso: string | undefined): string | null {
                if (!iso) return null;
+
                try {
                        const date = new Date(iso);
+
                        if (Number.isNaN(date.getTime())) return iso;
+
                        return date.toLocaleDateString(undefined, {
-                               year: 'numeric',
+                               day: 'numeric',
                                month: 'short',
-                               day: 'numeric'
+                               year: 'numeric'
                        });
                } catch {
                        return iso;
index 74b9452093ef532e9a62e79d834daad6d5bcc414..742685fcc9c70c28cf50c0dad37e911476e84a2a 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
+       import { parseWriteFileMeta } from './parsers/write-file';
+       import ToolCallBlock from './ToolCallBlock.svelte';
        import { XCircle } from '@lucide/svelte';
        import { SyntaxHighlightedCode } from '$lib/components/app';
        import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
-       import { abbreviateHome, type AgenticSection } from '$lib/utils';
        import { toolsStore } from '$lib/stores/tools.svelte';
-       import { parseWriteFileMeta } from './parsers/write-file';
-       import ToolCallBlock from './ToolCallBlock.svelte';
+       import { abbreviateHome, type AgenticSection } from '$lib/utils';
 
        interface Props {
                section: AgenticSection;
@@ -14,7 +14,7 @@
                onToggle?: () => void;
        }
 
-       let { section, open, isStreaming, onToggle }: Props = $props();
+       let { isStreaming, onToggle, open, section }: Props = $props();
 
        const writeFileMeta = $derived(parseWriteFileMeta(section));
        const home = $derived(toolsStore.serverHome);
index 3b5e87e894cb7c6b85645ab6c5aa8d60d6c6b4f2..c292544ac6eb735ebbe53a8ad56d739cf835deee 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 { AgenticSectionType } from '$lib/enums';
-       import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
        import { mcpStore } from '$lib/stores/mcp.svelte';
-       import type { Component, Snippet } from 'svelte';
        import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
+       import type { Component, Snippet } from 'svelte';
 
        type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
 
        }
 
        let {
-               section,
-               open,
+               children,
+               extraLiveStreaming = false,
                isStreaming,
                meta,
-               extraLiveStreaming = false,
+               onToggle,
+               open,
+               section,
                spinIconWhenActive = false,
-               wrapper: Wrapper = CollapsibleContentBlock,
                title,
                titleSnippet,
-               onToggle,
-               children
+               wrapper: Wrapper = CollapsibleContentBlock
        }: Props = $props();
 
        const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
        // signals activity; only terminal states get a pill.
        function subtitleFor(errorMessage?: string): string | undefined {
                if (showSpinner) return undefined;
+
                if (errorMessage) return 'failed';
+
                if (isStreamingCall && !isStreaming) return 'incomplete';
+
                return undefined;
        }
 
        {onToggle}
 >
        {@render children(meta, {
-               isStreaming,
+               isCodeStreaming,
                isPending,
-               isStreamingCall,
-               isCodeStreaming
+               isStreaming,
+               isStreamingCall
        })}
 </Wrapper>
index 6114f17b5bde1a19ba9a912496409300259b0df5..d8ca812b645482679893145e6b412d8e20b0b06f 100644 (file)
@@ -5,8 +5,8 @@
 // stay focused on its own format quirks.
 
 import { BuiltInTool } from '$lib/enums';
-import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
 import type { AgenticSection } from '$lib/utils/agentic';
+import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
 
 /**
  * Strict (final-state) JSON parser for a tool-args blob. Mirrors the
@@ -17,9 +17,11 @@ import type { AgenticSection } from '$lib/utils/agentic';
 function parseFinalToolArgs(blob: string): Record<string, unknown> | null {
        try {
                const parsed: unknown = JSON.parse(blob);
+
                if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                        return parsed as Record<string, unknown>;
                }
+
                return null;
        } catch {
                return null;
@@ -43,6 +45,7 @@ export function parseToolArgs(
        options: { partial?: boolean } = {}
 ): Record<string, unknown> | null {
        if (section.toolName !== expected || !section.toolArgs) return null;
+
        return options.partial
                ? parsePartialJsonArgs(section.toolArgs)
                : parseFinalToolArgs(section.toolArgs);
index 4bff25bb54cdab18b1fc752cfd245accee917958..0c1fb9e432d9274bd231f30fd23c150ae1a85859 100644 (file)
@@ -3,10 +3,10 @@
 // rendering), plus the result blob for `result` / `edits_applied` /
 // `error` fields.
 
-import { BuiltInTool } from '$lib/enums';
-import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
-import { tryParseToolResultObject, type AgenticSection } from '$lib/utils';
 import { parseToolArgs } from './_shared';
+import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
+import { BuiltInTool } from '$lib/enums';
+import { type AgenticSection, tryParseToolResultObject } from '$lib/utils';
 
 export type EditFileEdit = {
        oldText: string;
@@ -24,48 +24,57 @@ export type EditFileMeta = {
 
 export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
        const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
+
        if (!args) return null;
 
        const rawPath = args.path ?? args.file_path ?? args.filePath;
+
        if (typeof rawPath !== 'string' || !rawPath) return null;
 
        const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
-
        // Filter the streamed edits array strictly: each entry must be an
        // object with a non-empty `old_text`. Edits without an old_text
        // would diff against empty and render as a full re-write.
        const rawEdits = Array.isArray(args.edits) ? args.edits : [];
        const edits: EditFileEdit[] = [];
+
        for (const e of rawEdits) {
                if (!e || typeof e !== 'object' || Array.isArray(e)) continue;
+
                const obj = e as Record<string, unknown>;
                const oldText = typeof obj.old_text === 'string' ? obj.old_text : '';
+
                if (!oldText) continue;
+
                const newText = typeof obj.new_text === 'string' ? obj.new_text : '';
-               edits.push({ oldText, newText });
+
+               edits.push({ newText, oldText });
        }
 
        const resultObj = tryParseToolResultObject(section.toolResult);
+
        let resultMessage: string | undefined;
        let editsApplied: number | undefined;
        let errorMessage: string | undefined;
+
        if (typeof resultObj?.error === 'string') {
                errorMessage = resultObj.error;
        } else if (resultObj) {
                if (typeof resultObj.result === 'string') {
                        resultMessage = resultObj.result;
                }
+
                if (Number.isFinite(Number(resultObj.edits_applied))) {
                        editsApplied = Number(resultObj.edits_applied);
                }
        }
 
        return {
-               fileName,
-               filePath: rawPath,
                edits,
-               resultMessage,
                editsApplied,
-               errorMessage
+               errorMessage,
+               fileName,
+               filePath: rawPath,
+               resultMessage
        };
 }
index e8adbd18b06b20f994df9bb61006eabe77861d4d..aa7cd704933f070dab83eca628c45695b81fb035 100644 (file)
@@ -5,9 +5,9 @@
 // file only deals with what's strictly about *calling* the tool, since
 // the error / exit status elide from call-section to result-section.
 
+import { parseToolArgs } from './_shared';
 import { BuiltInTool } from '$lib/enums';
 import type { AgenticSection } from '$lib/utils';
-import { parseToolArgs } from './_shared';
 
 export type ExecShellCommandMeta = {
        command: string;
@@ -15,9 +15,12 @@ export type ExecShellCommandMeta = {
 
 export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
        const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
+
        if (!args) return null;
 
        const commandRaw = args.command ?? args.cmd ?? args.shell_command;
+
        if (typeof commandRaw !== 'string' || !commandRaw) return null;
+
        return { command: commandRaw };
 }
index 1ad92b74cf0562c28bcac2cea2a7b3f6615463e2..bbd50ce89172bbd080981bafa6e0e25fdb1e66f1 100644 (file)
@@ -4,9 +4,9 @@
 // parser keeps the original raw-text fallback for MCP servers that
 // emit unparseable output.
 
-import { BuiltInTool } from '$lib/enums';
-import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
 import { parseToolArgs } from './_shared';
+import { BuiltInTool } from '$lib/enums';
+import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
 
 export type FileGlobSearchMeta = {
        path: string;
@@ -19,11 +19,13 @@ export type FileGlobSearchMeta = {
 
 export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
        const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
+
        if (!args) return null;
 
        const path = typeof args.path === 'string' ? args.path : '';
        const include = typeof args.include === 'string' && args.include ? args.include : '**';
        const exclude = typeof args.exclude === 'string' && args.exclude ? args.exclude : undefined;
+
        if (!path) return null;
 
        let matches: string[] = [];
@@ -31,17 +33,21 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
        let errorMessage: string | undefined;
 
        const toolResultString = section.toolResult;
+
        if (toolResultString) {
                try {
                        const parsed: unknown = JSON.parse(toolResultString);
+
                        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                                const obj = parsed as Record<string, unknown>;
+
                                if (typeof obj.error === 'string') {
                                        errorMessage = obj.error;
                                } else if (typeof obj.plain_text_response === 'string') {
                                        const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
                                                totalMatches = total;
                                        });
+
                                        matches = split.lines;
                                }
                        }
@@ -50,9 +56,10 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
                        const split = splitSearchSummaryList(toolResultString, (total) => {
                                totalMatches = total;
                        });
+
                        matches = split.lines;
                }
        }
 
-       return { path, include, exclude, matches, totalMatches, errorMessage };
+       return { errorMessage, exclude, include, matches, path, totalMatches };
 }
index 0e606e193c68b741985c27e7d4066820b270d842..69c95210f32cac2a8385eb14a00914470a3a90a5 100644 (file)
@@ -5,9 +5,9 @@
 // fallback so MCP servers that return unparseable output still get
 // surfaced.
 
-import { BuiltInTool } from '$lib/enums';
-import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
 import { parseToolArgs } from './_shared';
+import { BuiltInTool } from '$lib/enums';
+import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
 
 export type GrepSearchMatch = {
        file: string;
@@ -28,10 +28,12 @@ export type GrepSearchMeta = {
 
 export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
        const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
+
        if (!args) return null;
 
        const path = typeof args.path === 'string' ? args.path : '';
        const pattern = typeof args.pattern === 'string' ? args.pattern : '';
+
        if (!path || !pattern) return null;
 
        const include = typeof args.include === 'string' && args.include ? args.include : '**';
@@ -43,17 +45,21 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
        let errorMessage: string | undefined;
 
        const toolResultString = section.toolResult;
+
        if (toolResultString) {
                try {
                        const parsed: unknown = JSON.parse(toolResultString);
+
                        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                                const obj = parsed as Record<string, unknown>;
+
                                if (typeof obj.error === 'string') {
                                        errorMessage = obj.error;
                                } else if (typeof obj.plain_text_response === 'string') {
                                        const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
                                                totalMatches = total;
                                        });
+
                                        matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
                                }
                        }
@@ -64,19 +70,20 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
                        const split = splitSearchSummaryList(toolResultString, (total) => {
                                totalMatches = total;
                        });
+
                        matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
                }
        }
 
        return {
+               errorMessage,
+               exclude,
+               include,
+               matches,
                path,
                pattern,
-               include,
-               exclude,
                showLineNumbers,
-               matches,
-               totalMatches,
-               errorMessage
+               totalMatches
        };
 }
 
@@ -85,24 +92,29 @@ function parseGrepLine(line: string, showLineNumbers: boolean): GrepSearchMatch
        //   <file>:<content>          when return_line_numbers=false
        //   <file>:<lineno>:<content> when return_line_numbers=true
        const firstColon = line.indexOf(':');
+
        if (firstColon === -1) {
-               return { file: line, content: '' };
+               return { content: '', file: line };
        }
+
        const file = line.slice(0, firstColon);
        const tail = line.slice(firstColon + 1);
 
        if (!showLineNumbers) {
-               return { file, content: tail };
+               return { content: tail, file };
        }
 
        const secondColon = tail.indexOf(':');
+
        if (secondColon === -1) {
-               return { file, content: tail };
+               return { content: tail, file };
        }
+
        const lineNum = parseInt(tail.slice(0, secondColon), 10);
+
        return {
+               content: tail.slice(secondColon + 1),
                file,
-               line: Number.isFinite(lineNum) ? lineNum : undefined,
-               content: tail.slice(secondColon + 1)
+               line: Number.isFinite(lineNum) ? lineNum : undefined
        };
 }
index d37dcf5010ea188be51dd8e59250e8906b713f7a..cbde40df46c9d39947b1eaee745597fc16411f85 100644 (file)
@@ -3,14 +3,14 @@
 // `start_line`+`line_count`). Args are parsed partially so a header
 // can render incrementally as the file path streams in.
 
-import { BuiltInTool } from '$lib/enums';
+import { parseToolArgs } from './_shared';
 import {
        DEFAULT_LANGUAGE,
        FILE_PATH_SEPARATOR_REGEX,
        TEXT_LANGUAGE_PREFIX_REGEX
 } from '$lib/constants';
-import { getFileTypeByExtension, type AgenticSection } from '$lib/utils';
-import { parseToolArgs } from './_shared';
+import { BuiltInTool } from '$lib/enums';
+import { type AgenticSection, getFileTypeByExtension } from '$lib/utils';
 
 export type ReadFileMeta = {
        fileName: string;
@@ -20,13 +20,14 @@ export type ReadFileMeta = {
 
 export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
        const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
+
        if (!args) return null;
 
        const rawPath = args.path ?? args.file_path ?? args.filePath;
+
        if (typeof rawPath !== 'string' || !rawPath) return null;
 
        const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
-
        // Models emit range arguments under several aliases. Accept all to
        // stay forgiving across prompt variations.
        const startRaw = args.start_line ?? args.line_start ?? args.startLine ?? args.from_line;
@@ -34,19 +35,22 @@ export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null
        const countRaw = args.line_count ?? args.count ?? args.num_lines;
 
        let lineRange: { start: number; end: number } | null = null;
+
        const sNum = Number(startRaw);
        const eNum = Number(endRaw);
+
        if (startRaw != null && endRaw != null && Number.isFinite(sNum) && Number.isFinite(eNum)) {
-               lineRange = { start: sNum, end: eNum };
+               lineRange = { end: eNum, start: sNum };
        } else if (startRaw != null && countRaw != null) {
                const cNum = Number(countRaw);
+
                if (Number.isFinite(sNum) && Number.isFinite(cNum)) {
-                       lineRange = { start: sNum, end: sNum + cNum - 1 };
+                       lineRange = { end: sNum + cNum - 1, start: sNum };
                }
        }
 
        const fileType = getFileTypeByExtension(fileName);
        const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
 
-       return { fileName, lineRange, language };
+       return { fileName, language, lineRange };
 }
index 9bcba8f03cc45dfbd8eed5f99400d2772afc7c46..a74153d4dcd35656ed8778e813e50d67e8962208 100644 (file)
@@ -5,9 +5,9 @@
 // failure renders as a flat line beginning with `Error:`. Both shapes
 // are handled.
 
+import { parseToolArgs } from './_shared';
 import { BuiltInTool } from '$lib/enums';
 import type { AgenticSection } from '$lib/utils';
-import { parseToolArgs } from './_shared';
 
 export type RunJavascriptMeta = {
        code: string;
@@ -17,30 +17,37 @@ export type RunJavascriptMeta = {
 
 export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
        const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
+
        if (!args) return null;
 
        const code = typeof args.code === 'string' ? args.code : '';
+
        if (!code) return null;
 
        const timeoutRaw = Number(args.timeout_ms);
        const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : undefined;
 
        let errorMessage: string | undefined;
+
        const toolResultString = section.toolResult;
+
        if (toolResultString) {
                // Branches matter here: a JSON object can carry `error`, but a
                // JSON array always represents successful output (sandbox returns
                // the array of values). Only when the result isn't a JSON object
                // do we scan raw lines for the `Error:` prefix.
                let parsedObject: Record<string, unknown> | null = null;
+
                try {
                        const parsed: unknown = JSON.parse(toolResultString);
+
                        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                                parsedObject = parsed as Record<string, unknown>;
                        }
                } catch {
                        parsedObject = null;
                }
+
                if (typeof parsedObject?.error === 'string') {
                        errorMessage = parsedObject.error;
                } else if (!parsedObject) {
@@ -48,9 +55,10 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe
                                .split('\n')
                                .map((line) => line.trim())
                                .find((line) => line.startsWith('Error:'));
+
                        if (errorLine) errorMessage = errorLine.slice('Error:'.length).trim();
                }
        }
 
-       return { code, timeoutMs, errorMessage };
+       return { code, errorMessage, timeoutMs };
 }
index 95edc3d95eafac787896f61a7801b1983a87f3be..cdec283a02433b4dce1e2f4eabe8d58327ef4bd4 100644 (file)
@@ -3,14 +3,14 @@
 // finishes) and surfaces `bytes`, `result`, and `error` from the
 // result blob.
 
-import { BuiltInTool } from '$lib/enums';
+import { parseToolArgs } from './_shared';
 import {
        DEFAULT_LANGUAGE,
        FILE_PATH_SEPARATOR_REGEX,
        TEXT_LANGUAGE_PREFIX_REGEX
 } from '$lib/constants';
-import { getFileTypeByExtension, tryParseToolResultObject, type AgenticSection } from '$lib/utils';
-import { parseToolArgs } from './_shared';
+import { BuiltInTool } from '$lib/enums';
+import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
 
 export type WriteFileMeta = {
        fileName: string;
@@ -24,18 +24,19 @@ export type WriteFileMeta = {
 
 export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
        const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
+
        if (!args) return null;
 
        // Tool contracts drifted over time: some models emit `path`,
        // others `file_path` / `filePath`. Accept all three.
        const rawPath = args.path ?? args.file_path ?? args.filePath;
+
        if (typeof rawPath !== 'string' || !rawPath) return null;
 
        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;
-
        const resultObj = tryParseToolResultObject(section.toolResult);
        const bytesWritten =
                resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
@@ -43,12 +44,12 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
        const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined;
 
        return {
+               bytesWritten,
+               content,
+               errorMessage,
                fileName,
                filePath: rawPath,
                language,
-               content,
-               bytesWritten,
-               resultMessage,
-               errorMessage
+               resultMessage
        };
 }
index f7590c3a3158ee397063938a933907f83fc7a4be..7e79ab2309b0c1f618575666500b011685f7e902 100644 (file)
@@ -6,9 +6,9 @@
                ChatMessageUserBubble
        } from '$lib/components/app/chat';
        import { getMessageEditContext } from '$lib/contexts';
+       import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
        import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
        import { isLoading } from '$lib/stores/chat.svelte';
-       import { MessageRole, ChatMessageStatisticsMode } from '$lib/enums';
        import { config } from '$lib/stores/settings.svelte';
 
        interface Props {
 
        let {
                class: className = '',
-               message,
-               siblingInfo = null,
                deletionInfo,
                isLastUserMessage = false,
+               message,
                nextAssistantMessage = null,
-               showDeleteDialog,
-               onEdit,
-               onDelete,
                onConfirmDelete,
+               onCopy,
+               onDelete,
+               onEdit,
                onForkConversation,
-               onShowDeleteDialogChange,
                onNavigateToSibling,
-               onCopy
+               onShowDeleteDialogChange,
+               showDeleteDialog,
+               siblingInfo = null
        }: Props = $props();
 
        // Get contexts
        // For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
        let storedReadingStats = $derived.by(() => {
                const timings = nextAssistantMessage?.timings;
+
                if (!timings?.prompt_n || !timings?.prompt_ms) return null;
 
                const agentic = timings.agentic;
 
                return {
-                       promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n,
-                       promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms
+                       promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms,
+                       promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n
                };
        });
 
index 49cc39723a47d3b0323cbdadf141ef6cc8b03012..a2e91b94c696394f5bfc4dc2cc49b0a67ca122ff 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { Card } from '$lib/components/ui/card';
        import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
+       import { Card } from '$lib/components/ui/card';
        import { config } from '$lib/stores/settings.svelte';
        import type { DatabaseMessageExtra } from '$lib/types/database';
 
        }
 
        let {
-               content,
                attachments = [],
-               renderMarkdown = false,
-               textColorClass = 'text-foreground',
                cardBgClass = 'dark:bg-primary/15',
-               maxHeightStyle = ''
+               content,
+               maxHeightStyle = '',
+               renderMarkdown = false,
+               textColorClass = 'text-foreground'
        }: Props = $props();
 
        let isMultiline = $state(false);
@@ -31,6 +31,7 @@
 
                if (content.includes('\n')) {
                        isMultiline = true;
+
                        return;
                }
 
index 1cc79fe6bca48aa690401e7b57c22a279beda398..f3e8c4e91d398673a809d5fe742dec2585be5a51 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
        import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
+       import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
        import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
 
        interface Props {
@@ -16,9 +16,9 @@
                class: className = '',
                content,
                extras = [],
-               onSendImmediately,
+               onDelete,
                onEdit,
-               onDelete
+               onSendImmediately
        }: Props = $props();
 
        const editCtx = useMessageEditContext({
index 17d8e21d7b1446421d5693d9cacd19d36616c2b1..3a60e674cda0fac6d7a80d490ac5ac988833b067 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
        import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import type { Snippet, Component } from 'svelte';
+       import type { Component, Snippet } from 'svelte';
 
        interface Props {
                icon: Component<{ class?: string }>;
@@ -8,7 +8,7 @@
                actions: Snippet;
        }
 
-       let { icon: IconComponent, message, actions }: Props = $props();
+       let { actions, icon: IconComponent, message }: Props = $props();
 </script>
 
 <div class="my-2 rounded-lg border border-border bg-card p-3">
index bbb1f0ac2bd6b576608899e7b5ad2a3617859a46..cb8ad09cd3e79ffc8a0b1903a831eab22feafcda 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
+       import ChatMessageActionCard from './ChatMessageActionCard.svelte';
        import { RotateCw } from '@lucide/svelte';
        import { Button } from '$lib/components/ui/button';
-       import ChatMessageActionCard from './ChatMessageActionCard.svelte';
 
        interface Props {
                onDecision: (shouldContinue: boolean) => void;
index 7f25c4549b785ab288245322549ba3f8299fb856..ef79fabeda3481401e0152706a71a044000a5b42 100644 (file)
@@ -3,10 +3,10 @@
        import { ChatMessageActionCard } from '$lib/components/app';
        import { Button, buttonVariants } from '$lib/components/ui/button';
        import * as ButtonGroup from '$lib/components/ui/button-group';
-       import { cn } from '$lib/components/ui/utils';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
-       import { ToolSource, ToolPermissionDecision } from '$lib/enums';
+       import { cn } from '$lib/components/ui/utils';
        import { TOOL_SERVER_LABELS } from '$lib/constants';
+       import { ToolPermissionDecision, ToolSource } from '$lib/enums';
        import { toolsStore } from '$lib/stores/tools.svelte';
 
        interface Props {
@@ -15,7 +15,7 @@
                onDecision: (decision: ToolPermissionDecision) => void;
        }
 
-       let { toolName, serverLabel, onDecision }: Props = $props();
+       let { onDecision, serverLabel, toolName }: Props = $props();
 </script>
 
 <ChatMessageActionCard icon={ShieldQuestion}>
@@ -40,7 +40,7 @@
 
                                <DropdownMenu.Trigger
                                        class={cn(
-                                               buttonVariants({ variant: 'secondary', size: 'sm' }),
+                                               buttonVariants({ size: 'sm', variant: 'secondary' }),
                                                'inline-flex cursor-pointer items-center !rounded-l-none !shadow-none !px-2'
                                        )}
                                        aria-label="More allow options"
index 503a2d086b1a287e6f2a77929074bcb001cf06f5..d08dafcff54f399fa3fecf658dff3fd423d711a6 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { Edit, Copy, RefreshCw, Trash2, ArrowRight, GitBranch } from '@lucide/svelte';
+       import { ArrowRight, Copy, Edit, GitBranch, RefreshCw, Trash2 } from '@lucide/svelte';
        import {
                ActionIcon,
                ChatMessageActionIconsBranchingControls,
                DialogConfirmation
        } from '$lib/components/app';
-       import { Switch } from '$lib/components/ui/switch';
        import { Checkbox } from '$lib/components/ui/checkbox';
        import Input from '$lib/components/ui/input/input.svelte';
        import Label from '$lib/components/ui/label/label.svelte';
+       import { Switch } from '$lib/components/ui/switch';
        import { MessageRole } from '$lib/enums';
        import { activeConversation } from '$lib/stores/conversations.svelte';
 
                actionsPosition,
                deletionInfo,
                justify,
-               onCopy,
-               onEdit,
                onConfirmDelete,
                onContinue,
+               onCopy,
                onDelete,
+               onEdit,
                onForkConversation,
                onNavigateToSibling,
-               onShowDeleteDialogChange,
+               onRawOutputToggle,
                onRegenerate,
+               onShowDeleteDialogChange,
+               rawOutputEnabled = false,
                role,
-               siblingInfo = null,
                showDeleteDialog,
                showRawOutputSwitch = false,
-               rawOutputEnabled = false,
-               onRawOutputToggle
+               siblingInfo = null
        }: Props = $props();
 
        let showForkDialog = $state(false);
@@ -77,7 +77,7 @@
        }
 
        function handleConfirmFork() {
-               onForkConversation?.({ name: forkName.trim(), includeAttachments: forkIncludeAttachments });
+               onForkConversation?.({ includeAttachments: forkIncludeAttachments, name: forkName.trim() });
                showForkDialog = false;
        }
 </script>
index 465dcab73bfd0beb785e5a8f28813abb815fa00e..37a6a7d94aa10a250d50d5fb905a77937c6f5a1f 100644 (file)
@@ -8,7 +8,7 @@
                onNavigateToSibling?: (siblingId: string) => void;
        }
 
-       let { class: className = '', siblingInfo, onNavigateToSibling }: Props = $props();
+       let { class: className = '', onNavigateToSibling, siblingInfo }: Props = $props();
 
        let hasPrevious = $derived(siblingInfo && siblingInfo.currentIndex > 0);
        let hasNext = $derived(siblingInfo && siblingInfo.currentIndex < siblingInfo.totalSiblings - 1);
index 74d5bdfe85075e352a9f400152483fea7ecb1730..2b4a22dd449746da97cafe8aa86aa6103ecc62cf 100644 (file)
@@ -1,29 +1,28 @@
 <script lang="ts">
+       import ChatMessageToolCallBlock from './ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte';
+       import ChatMessageReasoningBlock from './ChatMessageReasoningBlock.svelte';
        import {
-               ChatMessageStatistics,
-               MarkdownContent,
+               ChatMessageActionCardContinueRequest,
                ChatMessageActionCardPermissionRequest,
-               ChatMessageActionCardContinueRequest
+               ChatMessageStatistics,
+               MarkdownContent
        } from '$lib/components/app';
-
        import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
-       import type {
-               ChatMessageAgenticTimings,
-               ChatMessageAgenticTurnStats,
-               DatabaseMessage
-       } from '$lib/types';
-       import { deriveAgenticSections, type AgenticSection } from '$lib/utils';
        import {
-               agenticPendingPermissionRequest,
-               agenticResolvePermission,
+               agenticExecutingToolCallId,
+               agenticLastError,
                agenticPendingContinueRequest,
+               agenticPendingPermissionRequest,
                agenticResolveContinue,
-               agenticLastError,
-               agenticExecutingToolCallId
+               agenticResolvePermission
        } from '$lib/stores/agentic.svelte';
        import { config } from '$lib/stores/settings.svelte';
-       import ChatMessageReasoningBlock from './ChatMessageReasoningBlock.svelte';
-       import ChatMessageToolCallBlock from './ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte';
+       import type {
+               ChatMessageAgenticTimings,
+               ChatMessageAgenticTurnStats,
+               DatabaseMessage
+       } from '$lib/types';
+       import { type AgenticSection, deriveAgenticSections } from '$lib/utils';
 
        interface Props {
                message: DatabaseMessage;
        }
 
        let {
-               message,
-               toolMessages = [],
+               isLastAssistantMessage = false,
                isStreaming = false,
-               isLastAssistantMessage = false
+               message,
+               toolMessages = []
        }: Props = $props();
 
        let expandedStates: Record<number, boolean> = $state({});
@@ -60,6 +59,7 @@
        $effect(() => {
                if (pendingPermission !== prevPendingRef) {
                        prevPendingRef = pendingPermission;
+
                        if (pendingPermission) {
                                permissionDismissed = false;
                        }
@@ -81,6 +81,7 @@
        $effect(() => {
                if (pendingContinue !== prevContinueRef) {
                        prevContinueRef = pendingContinue;
+
                        if (pendingContinue) {
                                continueDismissed = false;
                        }
 
        const turnGroups: TurnGroup[] = $derived.by(() => {
                const groups: TurnGroup[] = [];
+
                let currentTurn: AgenticSection[] = [];
                let currentIndices: number[] = [];
                let prevWasTool = false;
                                section.type === AgenticSectionType.TOOL_CALL_STREAMING;
 
                        if (!isTool && prevWasTool && currentTurn.length > 0) {
-                               groups.push({ sections: currentTurn, flatIndices: currentIndices });
+                               groups.push({ flatIndices: currentIndices, sections: currentTurn });
                                currentTurn = [];
                                currentIndices = [];
                        }
                }
 
                if (currentTurn.length > 0) {
-                       groups.push({ sections: currentTurn, flatIndices: currentIndices });
+                       groups.push({ flatIndices: currentIndices, sections: currentTurn });
                }
 
                return groups;
 
        function buildTurnAgenticTimings(stats: ChatMessageAgenticTurnStats): ChatMessageAgenticTimings {
                return {
-                       turns: 1,
+                       llm: stats.llm,
+                       toolCalls: stats.toolCalls,
                        toolCallsCount: stats.toolCalls.length,
                        toolsMs: stats.toolsMs,
-                       toolCalls: stats.toolCalls,
-                       llm: stats.llm
+                       turns: 1
                };
        }
 </script>
index 962f2a28538c9c7f6dba1d94c902a8b89ca4c757..f44cd432e64035ba078fd26119d6f537a2874a05 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { X, AlertTriangle } from '@lucide/svelte';
+       import { AlertTriangle, X } from '@lucide/svelte';
+       import { ChatForm, DialogConfirmation } from '$lib/components/app';
        import { Button } from '$lib/components/ui/button';
        import { Switch } from '$lib/components/ui/switch';
-       import { ChatForm, DialogConfirmation } from '$lib/components/app';
        import { getMessageEditContext } from '$lib/contexts';
        import { KeyboardKey, MessageRole } from '$lib/enums';
        import { chatStore } from '$lib/stores/chat.svelte';
@@ -19,6 +19,7 @@
 
        let hasUnsavedChanges = $derived.by(() => {
                if (editCtx.editedContent !== editCtx.originalContent) return true;
+
                if (editCtx.editedUploadedFiles.length > 0) return true;
 
                const extrasChanged =
 
        function handleAttachmentRemove(index: number) {
                const newExtras = [...editCtx.editedExtras];
+
                newExtras.splice(index, 1);
                editCtx.setExtras(newExtras);
        }
 
        function handleUploadedFileRemove(fileId: string) {
                const newFiles = editCtx.editedUploadedFiles.filter((f) => f.id !== fileId);
+
                editCtx.setUploadedFiles(newFiles);
        }
 
        async function handleFilesAdd(files: File[]) {
                const processed = await processFilesToChatUploaded(files);
+
                editCtx.setUploadedFiles([...editCtx.editedUploadedFiles, ...processed]);
        }
 
index 3ec417327f05fc0ddf23a797044f11197cd27050..b44f0428fd1810bc4a673996057d6fdf4127c3b0 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
        import { Lightbulb } from '@lucide/svelte';
        import { CollapsibleContentBlock, MarkdownContent } from '$lib/components/app';
-       import { AgenticSectionType } from '$lib/enums';
        import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+       import { AgenticSectionType } from '$lib/enums';
        import { config } from '$lib/stores/settings.svelte';
        import type { DatabaseMessageExtra } from '$lib/types';
        import type { AgenticSection } from '$lib/utils';
        }
 
        let {
-               section,
-               open,
-               isStreaming,
-               hasReasoningError = false,
                attachments,
-               onToggle
+               hasReasoningError = false,
+               isStreaming,
+               onToggle,
+               open,
+               section
        }: Props = $props();
 
        const currentConfig = config();
                if (isPending && !isStreaming) {
                        return hasReasoningError ? REASONING_SUBTITLE_ERROR : REASONING_SUBTITLE_CANCELLED;
                }
+
                if (section.wasInterrupted) {
                        return hasReasoningError ? REASONING_SUBTITLE_ERROR : REASONING_SUBTITLE_CANCELLED;
                }
+
                return isStreaming ? '' : undefined;
        });
        const shimmerTitle = $derived(isPending && isStreaming);
@@ -55,6 +57,7 @@
 
        function isAtBottom(): boolean {
                if (!scrollEl) return false;
+
                return (
                        scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
                        SCROLL_BOTTOM_THRESHOLD_PX
 
        function scrollToBottomOnFrame() {
                if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
+
                pendingFrame = requestAnimationFrame(() => {
                        pendingFrame = null;
+
                        // User may scroll between scheduling and paint.
                        if (scrollEl && !userScrolledUp) {
                                scrollEl.scrollTop = scrollEl.scrollHeight;
 
        function handleScrollEvent() {
                if (!scrollEl) return;
+
                const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
+
                if (isScrollingUp && !isAtBottom()) {
                        userScrolledUp = true;
                } else if (isAtBottom()) {
                        userScrolledUp = false;
                }
+
                lastScrollTop = scrollEl.scrollTop;
        }
 
        $effect(() => {
                void section.content;
+
                if (!scrollEl || !isPending || !isStreaming) return;
+
                scrollToBottomOnFrame();
        });
 
                if (!scrollEl || !isPending || !isStreaming) return;
 
                const observer = new MutationObserver(() => scrollToBottomOnFrame());
+
                observer.observe(scrollEl, {
+                       characterData: true,
                        childList: true,
-                       subtree: true,
-                       characterData: true
+                       subtree: true
                });
 
                return () => observer.disconnect();
index 7ef73a49945bd1c64611ed8ae0c65b19933b1a29..1358f015177f4dd3260de7e3a73440c1e32eaf8a 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { Clock, Gauge, WholeWord, BookOpenText, Sparkles, Wrench, Layers } from '@lucide/svelte';
+       import { BookOpenText, Clock, Gauge, Layers, Sparkles, WholeWord, Wrench } from '@lucide/svelte';
        import { ChatMessageStatisticsBadge } from '$lib/components/app';
        import * as Tooltip from '$lib/components/ui/tooltip';
-       import { ChatMessageStatsView, ChatMessageStatisticsMode } from '$lib/enums';
+       import { DEFAULT_PERFORMANCE_TIME, MS_PER_SECOND } from '$lib/constants';
+       import { ChatMessageStatisticsMode, ChatMessageStatsView } from '$lib/enums';
        import type { ChatMessageAgenticTimings } from '$lib/types/chat';
        import { formatPerformanceTime } from '$lib/utils';
-       import { MS_PER_SECOND, DEFAULT_PERFORMANCE_TIME } from '$lib/constants';
        import type { Component } from 'svelte';
 
        interface Props {
        }
 
        let {
-               predictedTokens,
-               predictedMs,
-               promptTokens,
-               promptMs,
+               agenticTimings,
+               hideSummary = false,
+               initialView = ChatMessageStatsView.GENERATION,
                isLive = false,
                isProcessingPrompt = false,
-               initialView = ChatMessageStatsView.GENERATION,
-               agenticTimings,
+               mode = ChatMessageStatisticsMode.SWITCHABLE,
                onActiveViewChange,
-               hideSummary = false,
-               mode = ChatMessageStatisticsMode.SWITCHABLE
+               predictedMs,
+               predictedTokens,
+               promptMs,
+               promptTokens
        }: Props = $props();
 
        let isSwitchable = $derived(mode === ChatMessageStatisticsMode.SWITCHABLE);
                <div class="inline-flex items-center rounded-sm bg-muted-foreground/15 p-0.5">
                        {#if hasPromptStats || isLive}
                                {@render viewButton({
-                                       view: ChatMessageStatsView.READING,
                                        icon: BookOpenText,
                                        label: 'Reading',
-                                       tooltipText: 'Processing'
+                                       tooltipText: 'Processing',
+                                       view: ChatMessageStatsView.READING
                                })}
                        {/if}
 
                        {@render viewButton({
-                               view: ChatMessageStatsView.GENERATION,
+                               disabled: isGenerationDisabled,
                                icon: Sparkles,
                                label: 'Generation',
                                tooltipText: isGenerationDisabled ? 'Waiting for tokens...' : 'Generation',
-                               disabled: isGenerationDisabled
+                               view: ChatMessageStatsView.GENERATION
                        })}
 
                        {#if hasAgenticStats}
                                {@render viewButton({
-                                       view: ChatMessageStatsView.TOOLS,
                                        icon: Wrench,
                                        label: 'Tools',
-                                       tooltipText: 'Tool calls'
+                                       tooltipText: 'Tool calls',
+                                       view: ChatMessageStatsView.TOOLS
                                })}
 
                                {#if !hideSummary}
                                        {@render viewButton({
-                                               view: ChatMessageStatsView.SUMMARY,
                                                icon: Layers,
                                                label: 'Summary',
-                                               tooltipText: 'Agentic summary'
+                                               tooltipText: 'Agentic summary',
+                                               view: ChatMessageStatsView.SUMMARY
                                        })}
                                {/if}
                        {/if}
index db7d01690a56993bb6cf9d4c49b25cc0885deb17..0aa4cda877cfa90ab9e8a8c9ef7f283dfabccb57 100644 (file)
@@ -11,7 +11,7 @@
                tooltipLabel?: string;
        }
 
-       let { class: className = '', icon: IconComponent, value, tooltipLabel }: Props = $props();
+       let { class: className = '', icon: IconComponent, tooltipLabel, value }: Props = $props();
 
        function handleClick() {
                void copyToClipboard(String(value));
index 2b5ccb978e16480d9f8b6b4e1ef7e1364657c7b9..3514165a5b480d7046dff91da155ba1adbae1e0c 100644 (file)
@@ -2,21 +2,21 @@
        import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
        import { setChatActionsContext } from '$lib/contexts';
        import { MessageRole } from '$lib/enums';
+       import {
+               agenticClearSteeringMessage,
+               agenticInjectSteeringMessage,
+               agenticPendingSteeringMessageContent,
+               agenticPendingSteeringMessageExtras
+       } from '$lib/stores/agentic.svelte';
        import { chatStore } from '$lib/stores/chat.svelte';
        import {
-               chatPendingMessageContent,
-               chatPendingMessageExtras,
                chatClearPendingMessage,
-               chatInjectPendingMessage
+               chatInjectPendingMessage,
+               chatPendingMessageContent,
+               chatPendingMessageExtras
        } from '$lib/stores/chat.svelte';
-       import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
+       import { activeConversation, conversationsStore } from '$lib/stores/conversations.svelte';
        import { config } from '$lib/stores/settings.svelte';
-       import {
-               agenticPendingSteeringMessageContent,
-               agenticPendingSteeringMessageExtras,
-               agenticClearSteeringMessage,
-               agenticInjectSteeringMessage
-       } from '$lib/stores/agentic.svelte';
        import {
                buildSiblingInfoMap,
                copyToClipboard,
                onMessagesReady?: (messageCount: number) => void;
        }
 
-       let { messages = [], onUserAction, onMessagesReady }: Props = $props();
+       let { messages = [], onMessagesReady, onUserAction }: Props = $props();
 
        let allConversationMessages = $state<DatabaseMessage[]>([]);
 
        const currentConfig = config();
 
        setChatActionsContext({
+               continueAssistantMessage: async (message: DatabaseMessage) => {
+                       onUserAction?.();
+                       await chatStore.continueAssistantMessage(message.id);
+                       refreshAllMessages();
+               },
+
                copy: async (message: DatabaseMessage) => {
                        const asPlainText = Boolean(currentConfig.copyTextAttachmentsAsPlainText);
                        const clipboardContent = formatMessageForClipboard(
@@ -44,6 +50,7 @@
                                message.extra,
                                asPlainText
                        );
+
                        await copyToClipboard(clipboardContent, 'Message copied to clipboard');
                },
 
                        refreshAllMessages();
                },
 
-               navigateToSibling: async (siblingId: string) => {
-                       await conversationsStore.navigateToSibling(siblingId);
+               editUserMessagePreserveResponses: async (
+                       message: DatabaseMessage,
+                       newContent: string,
+                       newExtras?: DatabaseMessageExtra[]
+               ) => {
+                       onUserAction?.();
+                       await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
+                       refreshAllMessages();
                },
 
                editWithBranching: async (
                        refreshAllMessages();
                },
 
-               editUserMessagePreserveResponses: async (
+               forkConversation: async (
                        message: DatabaseMessage,
-                       newContent: string,
-                       newExtras?: DatabaseMessageExtra[]
+                       options: { name: string; includeAttachments: boolean }
                ) => {
-                       onUserAction?.();
-                       await chatStore.editUserMessagePreserveResponses(message.id, newContent, newExtras);
-                       refreshAllMessages();
+                       await conversationsStore.forkConversation(message.id, options);
                },
 
-               regenerateWithBranching: async (message: DatabaseMessage, modelOverride?: string) => {
-                       onUserAction?.();
-                       await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
-                       refreshAllMessages();
+               navigateToSibling: async (siblingId: string) => {
+                       await conversationsStore.navigateToSibling(siblingId);
                },
 
-               continueAssistantMessage: async (message: DatabaseMessage) => {
+               regenerateWithBranching: async (message: DatabaseMessage, modelOverride?: string) => {
                        onUserAction?.();
-                       await chatStore.continueAssistantMessage(message.id);
+                       await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
                        refreshAllMessages();
-               },
-
-               forkConversation: async (
-                       message: DatabaseMessage,
-                       options: { name: string; includeAttachments: boolean }
-               ) => {
-                       await conversationsStore.forkConversation(message.id, options);
                }
        });
 
                const filteredMessages = currentConfig.showSystemMessage
                        ? messages
                        : messages.filter((msg) => msg.type !== MessageRole.SYSTEM);
-
                // Build display entries, grouping agentic sessions into single entries.
                // An agentic session = assistant(with tool_calls) → tool → assistant → tool → ... → assistant(final)
                const result: Array<{
                        if (msg.role === MessageRole.TOOL) continue;
 
                        const toolMessages: DatabaseMessage[] = [];
+
                        if (msg.role === MessageRole.ASSISTANT && hasAgenticContent(msg)) {
                                let j = i + 1;
 
                        }
 
                        const siblingInfo = siblingInfoByMessageId.get(msg.id) ?? {
+                               currentIndex: 0,
                                message: msg,
                                siblingIds: [msg.id],
-                               currentIndex: 0,
                                totalSiblings: 1
                        };
 
                        result.push({
-                               message: msg,
-                               toolMessages,
                                isLastAssistantMessage: false,
                                isLastUserMessage: false,
+                               message: msg,
                                nextAssistantMessage: null,
-                               siblingInfo
+                               siblingInfo,
+                               toolMessages
                        });
                }
 
                let lastAssistantIdx = -1;
+
                for (let i = result.length - 1; i >= 0; i--) {
                        if (result[i].message.role === MessageRole.ASSISTANT) {
                                result[i].isLastAssistantMessage = true;
                                lastAssistantIdx = i;
+
                                break;
                        }
                }
                        for (let j = i + 1; j < result.length; j++) {
                                if (result[j].message.role === MessageRole.ASSISTANT) {
                                        result[i].nextAssistantMessage = result[j].message;
+
                                        break;
                                }
                        }
 </script>
 
 <div>
-       {#each displayMessages as { message, toolMessages, isLastAssistantMessage, isLastUserMessage, nextAssistantMessage, siblingInfo } (message.id)}
+       {#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
                <ChatMessage
                        class="mx-auto mt-12 w-full max-w-3xl"
                        {message}
index 6e32fc7aa362639d0aa5cb77d517f97e6320ce06..48ab40ec097c7f69e4c53655dd00637b693d5696 100644 (file)
@@ -1,41 +1,41 @@
 <script lang="ts">
+       import ChatScreenActionScrollDown from './ChatScreenActionScrollDown.svelte';
+       import ChatScreenDialogsAndAlerts from './ChatScreenDialogsAndAlerts.svelte';
+       import ChatScreenGreeting from './ChatScreenGreeting.svelte';
        import { page } from '$app/state';
        import {
-               ChatScreenForm,
                ChatMessages,
                ChatScreenDragOverlay,
+               ChatScreenForm,
+               ChatScreenServerError,
                ChatScreenStreamResumeStatus,
-               ServerLoadingSplash,
-               ChatScreenServerError
+               ServerLoadingSplash
        } from '$lib/components/app';
+       import { LANDING_SETTLE_MAX_MS, LANDING_STABLE_FRAMES, ROUTES } from '$lib/constants';
        import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
        import { useChatScreenActiveModel } from '$lib/hooks/use-chat-screen-active-model.svelte';
        import { useChatScreenDragAndDrop } from '$lib/hooks/use-chat-screen-drag-and-drop.svelte';
        import { useChatScreenFileUpload } from '$lib/hooks/use-chat-screen-file-upload.svelte';
        import { useChatScreenScroll } from '$lib/hooks/use-chat-screen-scroll.svelte';
        import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
-       import { device } from '$lib/stores/device.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
        import {
                chatStore,
                errorDialog,
-               isLoading,
                isChatStreaming,
-               isEditing
+               isEditing,
+               isLoading
        } from '$lib/stores/chat.svelte';
        import {
-               conversationsStore,
+               activeConversation,
                activeMessages,
-               activeConversation
+               conversationsStore
        } from '$lib/stores/conversations.svelte';
+       import { device } from '$lib/stores/device.svelte';
+       import { serverError, serverLoading } from '$lib/stores/server.svelte';
        import { config } from '$lib/stores/settings.svelte';
-       import { serverLoading, serverError } from '$lib/stores/server.svelte';
+       import { isMobile } from '$lib/stores/viewport.svelte';
        import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
        import { onDestroy, onMount, tick } from 'svelte';
-       import ChatScreenGreeting from './ChatScreenGreeting.svelte';
-       import ChatScreenActionScrollDown from './ChatScreenActionScrollDown.svelte';
-       import ChatScreenDialogsAndAlerts from './ChatScreenDialogsAndAlerts.svelte';
-       import { LANDING_SETTLE_MAX_MS, LANDING_STABLE_FRAMES, ROUTES } from '$lib/constants';
 
        let { showCenteredEmpty = false } = $props();
 
        let isCurrentConversationLoading = $derived(isLoading() || isChatStreaming());
        let chatFormBottomPosition = $derived.by(() => {
                if (!isMobile.current) return '1rem';
+
                if (device.isStandalone) return '1.5rem';
+
                if (device.isIOSSafari) return '0.25rem';
+
                return '0.5rem';
        });
 
        const scroll = useChatScreenScroll(autoScroll);
        const activeModel = useChatScreenActiveModel();
        const fileUpload = useChatScreenFileUpload({
+               activeModelId: () => activeModel.activeModelId,
                capabilities: () => ({
-                       hasVision: activeModel.hasVisionModality,
                        hasAudio: activeModel.hasAudioModality,
-                       hasVideo: activeModel.hasVideoModality
-               }),
-               activeModelId: () => activeModel.activeModelId
+                       hasVideo: activeModel.hasVideoModality,
+                       hasVision: activeModel.hasVisionModality
+               })
        });
        const dragAndDrop = useChatScreenDragAndDrop({
                onDrop: fileUpload.handleFileUpload
                if (!isMobile.current) return;
 
                const container = scroll.chatScrollContainer;
+
                if (!container) return;
 
                const distanceFromBottom =
                        container.scrollHeight - container.clientHeight - container.scrollTop;
+
                isMobileUserScrolledUp = distanceFromBottom > 300;
        }
 
                if (result?.emptyFiles && result.emptyFiles.length > 0) {
                        emptyFileNames = result.emptyFiles;
                        showEmptyFileDialog = true;
+
                        if (files) {
                                const emptyFileNamesSet = new Set(result.emptyFiles);
+
                                fileUpload.uploadedFiles = fileUpload.uploadedFiles.filter(
                                        (file) => !emptyFileNamesSet.has(file.name)
                                );
                        }
+
                        return false;
                }
 
                handleSendLikeScroll();
 
                await chatStore.sendMessage(message, result?.extras);
+
                return true;
        }
 
        // height settles, bailing out on user scroll or conversation change.
        async function handleMessagesReady(messageCount: number) {
                if (messageCount === 0) return;
+
                const id = activeConversation()?.id ?? null;
+
                if (!id || id === lastScrolledConversationId) return;
+
                lastScrolledConversationId = id;
                await tick();
                autoScroll.scrollToBottom();
 
                const container = scroll.chatScrollContainer;
+
                if (!container) return;
+
                const started = performance.now();
+
                let stableFrames = 0;
                let lastHeight = container.scrollHeight;
+
                const settle = () => {
                        if (autoScroll.userScrolledUp) return;
+
                        if (activeConversation()?.id !== id) return;
+
                        autoScroll.scrollToBottom();
                        const height = container.scrollHeight;
+
                        stableFrames = height === lastHeight ? stableFrames + 1 : 0;
                        lastHeight = height;
+
                        if (stableFrames >= LANDING_STABLE_FRAMES) return;
+
                        if (performance.now() - started > LANDING_SETTLE_MAX_MS) return;
+
                        requestAnimationFrame(settle);
                };
+
                requestAnimationFrame(settle);
        }
 
 
                setTimeout(() => {
                        const container = scroll.chatScrollContainer;
+
                        if (!container) return;
 
                        const lastUserBubble = container.querySelector(
                                const baseHeight = container.scrollHeight - innerHeight;
 
                                container.scrollTo({
-                                       top: bubbleHeight > 0 ? baseHeight - bubbleHeight : baseHeight,
-                                       behavior: 'smooth'
+                                       behavior: 'smooth',
+                                       top: bubbleHeight > 0 ? baseHeight - bubbleHeight : baseHeight
                                });
                        } else if (lastUserBubble) {
                                // On desktop, place the last user message near the top of the viewport
                                const topPadding = 24;
                                const bubbleRect = lastUserBubble.getBoundingClientRect();
+
                                container.scrollTo({
-                                       top: Math.max(0, container.scrollTop + bubbleRect.top - topPadding),
-                                       behavior: 'smooth'
+                                       behavior: 'smooth',
+                                       top: Math.max(0, container.scrollTop + bubbleRect.top - topPadding)
                                });
                        } else {
                                autoScroll.scrollToBottom();
                if (draft.message || draft.files.length > 0) {
                        chatStore.savePendingDraft(draft.message, draft.files);
                }
+
                await chatStore.addSystemPrompt();
        }
 
        $effect(() => {
                const shouldDisableAutoScroll =
                        config().disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
+
                autoScroll.setDisabled(shouldDisableAutoScroll);
+
                if (!shouldDisableAutoScroll) {
                        autoScroll.enable();
                }
 
        onMount(() => {
                const pendingDraft = chatStore.consumePendingDraft();
+
                if (pendingDraft) {
                        initialMessage = pendingDraft.message;
                        fileUpload.uploadedFiles = pendingDraft.files;
        onscroll={(e) => {
                scroll.handleScroll(e);
                handleMobileScroll();
+
                if (e.isTrusted && Date.now() > mobileScrollDownHintLockedUntil) {
                        mobileScrollDownHint = false;
                }
                                                onclick={() => {
                                                        mobileScrollDownHint = false;
                                                        scroll.chatScrollContainer?.scrollTo({
-                                                               top: scroll.chatScrollContainer.scrollHeight,
-                                                               behavior: 'smooth'
+                                                               behavior: 'smooth',
+                                                               top: scroll.chatScrollContainer.scrollHeight
                                                        });
                                                }}
                                        />
index dca24afd440fe5b8c1fe069bee92c1d84c6344d7..15f4813a18fdb462e5951419abe47aa232cd1c45 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { ArrowDown } from '@lucide/svelte';
        import ActionIcon from '$lib/components/app/actions/ActionIcon.svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
 
        let { onclick }: { onclick: (e?: MouseEvent) => void } = $props();
 </script>
index 6305a74380155b7b95cd249077a542c993b8bcce..667e8fed4b8d89c237c5164e3b8894c7d8d55475 100644 (file)
@@ -1,21 +1,21 @@
 <script lang="ts">
        import { Trash2 } from '@lucide/svelte';
-       import { ErrorDialogType } from '$lib/enums';
        import {
                DialogChatError,
                DialogConfirmation,
                DialogEmptyFileAlert,
                DialogFileUploadError
        } from '$lib/components/app';
+       import { ErrorDialogType } from '$lib/enums';
 
        let {
-               showDeleteDialog,
-               handleDeleteConfirm,
-               showEmptyFileDialog,
-               emptyFileNames,
                activeErrorDialog,
+               emptyFileNames,
+               fileUpload,
+               handleDeleteConfirm,
                handleErrorDialogOpenChange,
-               fileUpload
+               showDeleteDialog,
+               showEmptyFileDialog
        } = $props();
 </script>
 
index 8eb17eeae485c412fab2c8d1cbbfd0707b6cd6d6..679a7cd5fc260e050d375217372a054225bd9353 100644 (file)
@@ -2,9 +2,9 @@
        import { afterNavigate } from '$app/navigation';
        import { page } from '$app/state';
        import { ChatForm } from '$lib/components/app';
+       import { useDraftMessages } from '$lib/hooks/use-draft-messages.svelte';
        import { isMobile } from '$lib/stores/viewport.svelte';
        import { onMount } from 'svelte';
-       import { useDraftMessages } from '$lib/hooks/use-draft-messages.svelte';
 
        interface Props {
                class?: string;
                if (!formWrapperEl) return;
 
                const formEl = formWrapperEl.querySelector('form') as HTMLElement | null;
+
                if (!formEl) return;
 
                const updateHeight = () => {
                        const height = Math.round(formEl.getBoundingClientRect().height);
+
                        document.documentElement.style.setProperty('--chat-form-height', `${height}px`);
                };
 
                updateHeight();
 
                const resizeObserver = new ResizeObserver(updateHeight);
+
                resizeObserver.observe(formEl);
 
                return () => {
 
        const { clearDraft } = useDraftMessages({
                getChatId: () => chatId,
-               getMessage: () => message,
                getFiles: () => uploadedFiles,
-               setMessage: (m) => (message = m),
+               getInitialMessage: () => initialMessage,
+               getMessage: () => message,
                setFiles: (f) => (uploadedFiles = f),
-               getInitialMessage: () => initialMessage
+               setMessage: (m) => (message = m)
        });
 
        function handleFilesAdd(files: File[]) {
        }
 
        function handleSystemPromptClick() {
-               onSystemPromptAdd?.({ message, files: uploadedFiles });
+               onSystemPromptAdd?.({ files: uploadedFiles, message });
        }
 
        function handleUploadedFileRemove(fileId: string) {
        // message editor opened just before a navigation)
        function focusFormUnlessCaptured() {
                const active = document.activeElement;
+
                if (active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement) return;
+
                chatFormRef?.focus();
        }
 
index 45538a35151d7b48193dca28c612a12ef786ea71..ae5d91beee5e9f59e6ecb451592c9de22fec84d7 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        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 { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
 
        let hasError = $derived(!!serverError());
index b3abe4c660801541b34a518961d9c2799c03f08d..90611861016c33f840c4b7a09a289d905a8392e3 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { chatStore } from '$lib/stores/chat.svelte';
-       import { StreamConnectionState } from '$lib/enums';
        import { Loader2 } from '@lucide/svelte';
+       import { StreamConnectionState } from '$lib/enums';
+       import { chatStore } from '$lib/stores/chat.svelte';
 
        let state = $derived(chatStore.streamConnectionState);
 </script>
index ad703226192a77ee6f4e2b041a2262ff77a18f03..ec57985acabe589ec4e62f4b661add2644b2f6dc 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        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 type { Snippet } from 'svelte';
        import type { Component } from 'svelte';
 
        }
 
        let {
-               open = $bindable(false),
+               children,
                class: className = '',
                icon: IconComponent,
                iconClass = ICON_CLASS_DEFAULT,
                iconUrl = null,
-               title = '',
-               titleSnippet,
-               subtitle,
-               shimmerTitle = false,
                onToggle,
-               children
+               open = $bindable(false),
+               shimmerTitle = false,
+               subtitle,
+               title = '',
+               titleSnippet
        }: Props = $props();
 
        function hideBrokenIcon(event: Event) {
index 5cbe003bd7ad91d08220d52dd1590fbcbcd11309..5995dcebc98fac1ac009139b016c87e6647073c5 100644 (file)
        }
 
        let {
-               open = $bindable(false),
+               children,
                class: className = '',
                icon: IconComponent,
                iconClass = ICON_CLASS_DEFAULT,
                iconUrl = null,
-               title = '',
-               titleSnippet,
-               subtitle,
-               shimmerTitle = false,
                onToggle,
-               children
+               open = $bindable(false),
+               shimmerTitle = false,
+               subtitle,
+               title = '',
+               titleSnippet
        }: Props = $props();
 
        function hideBrokenIcon(event: Event) {
index 6ea4ac78e671c30aae67a4ad1d5c253ea01a8edd..c768a04471ac18b45f61db7ed30b429a0b11896f 100644 (file)
@@ -1,85 +1,85 @@
 <script lang="ts">
-       import { remark } from 'remark';
-       import remarkBreaks from 'remark-breaks';
-       import remarkGfm from 'remark-gfm';
-       import remarkMath from 'remark-math';
-       import rehypeHighlight from 'rehype-highlight';
-       import { all as lowlightAll } from 'lowlight';
-       import remarkRehype from 'remark-rehype';
-       import rehypeKatex from 'rehype-katex';
-       import rehypeStringify from 'rehype-stringify';
-       import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
-       import type { Root as MdastRoot } from 'mdast';
-       import { browser } from '$app/environment';
-       import { onDestroy, tick } from 'svelte';
-       import { SvelteMap } from 'svelte/reactivity';
-       import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
-       import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
-       import { rehypeFileBadge } from './plugins/rehype/file-badge';
+       import '$styles/katex-custom.scss';
+       import {
+               getCodeInfoFromTarget,
+               getHastNodeId,
+               getMdastNodeHash,
+               isAppendMode
+       } from './markdown-utils';
        import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
+       import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
        import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
-       import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
-       import { rehypeSvgPre } from './plugins/rehype/svg-pre';
        import { rehypeEnhanceSvgBlocks } from './plugins/rehype/enhance-svg-blocks';
-       import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
+       import { rehypeFileBadge } from './plugins/rehype/file-badge';
+       import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
        import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
+       import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
+       import { rehypeSvgPre } from './plugins/rehype/svg-pre';
+       import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
        import { remarkLiteralHtml } from './plugins/remark/literal-html';
+       import { browser } from '$app/environment';
        import {
-               getHastNodeId,
-               getMdastNodeHash,
-               isAppendMode,
-               getCodeInfoFromTarget
-       } from './markdown-utils';
-       import {
-               preprocessLaTeX,
-               getImageErrorFallbackHtml,
-               copyCodeToClipboard,
-               copyToClipboard,
-               splitGluedClosingCodeFences
-       } from '$lib/utils';
+               ActionIconCopyToClipboard,
+               CodeBlockActions,
+               DialogCodePreview,
+               DialogMermaidPreview
+       } from '$lib/components/app';
        import {
-               IMAGE_NOT_ERROR_BOUND_SELECTOR,
-               DATA_ERROR_BOUND_ATTR,
-               DATA_ERROR_HANDLED_ATTR,
                BOOL_TRUE_STRING,
-               SETTINGS_KEYS,
                CODE_BLOCK_HEADER_CLASS,
-               MERMAID_WRAPPER_CLASS,
+               DATA_ERROR_BOUND_ATTR,
+               DATA_ERROR_HANDLED_ATTR,
+               DIAGRAM_VIEW_MODE_ATTR,
+               DIAGRAM_VIEW_RENDERED,
+               DIAGRAM_VIEW_SOURCE,
+               IMAGE_NOT_ERROR_BOUND_SELECTOR,
                MERMAID_BLOCK_CLASS,
                MERMAID_LANGUAGE,
-               MERMAID_SYNTAX_ATTR,
                MERMAID_RENDERED_ATTR,
-               SVG_WRAPPER_CLASS,
+               MERMAID_SYNTAX_ATTR,
+               MERMAID_WRAPPER_CLASS,
+               SETTINGS_KEYS,
                SVG_BLOCK_CLASS,
+               SVG_INLINE_SHADOW_STYLE,
                SVG_LANGUAGE,
-               XML_LANGUAGE,
-               SVG_TAG_PREFIX,
-               SVG_SOURCE_ATTR,
                SVG_RENDERED_ATTR,
-               SVG_INLINE_SHADOW_STYLE,
+               SVG_SOURCE_ATTR,
+               SVG_TAG_PREFIX,
+               SVG_WRAPPER_CLASS,
                TOGGLE_SOURCE_BTN_CLASS,
-               DIAGRAM_VIEW_MODE_ATTR,
-               DIAGRAM_VIEW_RENDERED,
-               DIAGRAM_VIEW_SOURCE
+               XML_LANGUAGE
        } from '$lib/constants';
        import { ColorMode, UrlProtocol } from '$lib/enums';
        import { FileTypeText } from '$lib/enums/files.enums';
-       import { highlightCode, detectIncompleteCodeBlock, type IncompleteCodeBlock } from '$lib/utils';
+       import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
+       import { config } from '$lib/stores/settings.svelte';
+       import type { DatabaseMessageExtra } from '$lib/types/database';
+       import {
+               copyCodeToClipboard,
+               copyToClipboard,
+               getImageErrorFallbackHtml,
+               preprocessLaTeX,
+               splitGluedClosingCodeFences
+       } from '$lib/utils';
+       import { detectIncompleteCodeBlock, highlightCode, type IncompleteCodeBlock } from '$lib/utils';
        import { sanitizeSvg } from '$lib/utils/sanitize-svg';
        import { mountSvgShadow } from '$lib/utils/svg-shadow';
-       import '$styles/katex-custom.scss';
-       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
+       import type { Root as HastRoot, RootContent as HastRootContent } from 'hast';
        import githubLightCss from 'highlight.js/styles/github.css?inline';
+       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
+       import { all as lowlightAll } from 'lowlight';
+       import type { Root as MdastRoot } from 'mdast';
        import { mode } from 'mode-watcher';
-       import {
-               CodeBlockActions,
-               DialogCodePreview,
-               DialogMermaidPreview,
-               ActionIconCopyToClipboard
-       } from '$lib/components/app';
-       import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
-       import type { DatabaseMessageExtra } from '$lib/types/database';
-       import { config } from '$lib/stores/settings.svelte';
+       import rehypeHighlight from 'rehype-highlight';
+       import rehypeKatex from 'rehype-katex';
+       import rehypeStringify from 'rehype-stringify';
+       import { remark } from 'remark';
+       import remarkBreaks from 'remark-breaks';
+       import remarkGfm from 'remark-gfm';
+       import remarkMath from 'remark-math';
+       import remarkRehype from 'remark-rehype';
+       import { onDestroy, tick } from 'svelte';
+       import { SvelteMap } from 'svelte/reactivity';
 
        interface Props {
                attachments?: DatabaseMessageExtra[];
@@ -94,7 +94,7 @@
                contentHash?: string;
        }
 
-       let { content, attachments, class: className = '', disableMath = false }: Props = $props();
+       let { attachments, class: className = '', content, disableMath = false }: Props = $props();
 
        let containerRef = $state<HTMLDivElement>();
        let renderedBlocks = $state<MarkdownBlock[]>([]);
        let incompleteCodeBlock = $state<IncompleteCodeBlock | null>(null);
        const streamingSvgCode = $derived.by(() => {
                const block = incompleteCodeBlock;
+
                if (!block) return null;
+
                if (block.language === SVG_LANGUAGE) return block.code;
+
                if (block.language === XML_LANGUAGE && block.code.trimStart().startsWith(SVG_TAG_PREFIX))
                        return block.code;
+
                return null;
        });
        const liveSvgHtml = $derived(streamingSvgCode !== null ? sanitizeSvg(streamingSvgCode) : '');
 
                return proc
                        .use(rehypeHighlight, {
-                               languages: lowlightAll,
-                               aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] }
+                               aliases: { [FileTypeText.XML]: [FileTypeText.SVELTE, FileTypeText.VUE] },
+                               languages: lowlightAll
                        }) // Add syntax highlighting
                        .use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables
                        .use(rehypeEnhanceLinks) // Add target="_blank" to links
                if (!browser) return;
 
                const existingTheme = document.getElementById(themeStyleId);
+
                existingTheme?.remove();
        }
 
                if (!browser) return;
 
                const existingTheme = document.getElementById(themeStyleId);
+
                existingTheme?.remove();
 
                const style = document.createElement('style');
+
                style.id = themeStyleId;
                style.textContent = isDark ? githubDarkCss : githubLightCss;
 
                index: number
        ): Promise<{ html: string; hash: string }> {
                const hash = getMdastNodeHash(node, index);
-
                const cached = transformCache.get(hash);
+
                if (cached) {
-                       return { html: cached, hash };
+                       return { hash, html: cached };
                }
 
-               const singleNodeRoot = { type: 'root', children: [node] };
+               const singleNodeRoot = { children: [node], type: 'root' };
                const transformedRoot = (await processorInstance.run(singleNodeRoot as MdastRoot)) as HastRoot;
                const html = processorInstance.stringify(transformedRoot);
 
                transformCache.set(hash, html);
 
-               return { html, hash };
+               return { hash, html };
        }
 
        /**
                        unstableBlockHtml = '';
                        incompleteCodeBlock = null;
                        previousContent = '';
+
                        return;
                }
 
                                const ast = processorInstance.parse(normalizedPrefix) as MdastRoot;
                                const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
                                const nextBlocks: MarkdownBlock[] = [];
-
                                // Check if we're in append mode for cache reuse
                                const appendMode = isAppendMode(prefixMarkdown, previousContent);
                                const previousBlockCount = appendMode ? renderedBlocks.length : 0;
                                        }
 
                                        // Transform this block (with caching)
-                                       const { html, hash } = await transformMdastNode(processorInstance, child, index);
+                                       const { hash, html } = await transformMdastNode(processorInstance, child, index);
                                        const id = getHastNodeId(
                                                { position: (child as { position?: unknown }).position } as HastRootContent,
                                                index
                                        );
 
-                                       nextBlocks.push({ id, html, contentHash: hash });
+                                       nextBlocks.push({ contentHash: hash, html, id });
                                }
 
                                renderedBlocks = nextBlocks;
                const mdastChildren = (ast as { children?: unknown[] }).children ?? [];
                const stableCount = Math.max(mdastChildren.length - 1, 0);
                const nextBlocks: MarkdownBlock[] = [];
-
                // Check if we're in append mode for cache reuse
                const appendMode = isAppendMode(markdown, previousContent);
                const previousBlockCount = appendMode ? renderedBlocks.length : 0;
                        if (appendMode && index < previousBlockCount) {
                                const prevBlock = renderedBlocks[index];
                                const currentHash = getMdastNodeHash(child, index);
+
                                if (prevBlock?.contentHash === currentHash) {
                                        nextBlocks.push(prevBlock);
 
                        }
 
                        // Transform this block (with caching)
-                       const { html, hash } = await transformMdastNode(processorInstance, child, index);
+                       const { hash, html } = await transformMdastNode(processorInstance, child, index);
                        const id = getHastNodeId(
                                { position: (child as { position?: unknown }).position } as HastRootContent,
                                index
                        );
 
-                       nextBlocks.push({ id, html, contentHash: hash });
+                       nextBlocks.push({ contentHash: hash, html, id });
                }
 
                let unstableHtml = '';
 
                if (mdastChildren.length > stableCount) {
                        const unstableChild = mdastChildren[stableCount];
-                       const singleNodeRoot = { type: 'root', children: [unstableChild] };
+                       const singleNodeRoot = { children: [unstableChild], type: 'root' };
                        const transformedRoot = (await processorInstance.run(
                                singleNodeRoot as MdastRoot
                        )) as HastRoot;
         */
        async function handleMermaidClick(event: MouseEvent) {
                const target = event.target as HTMLElement;
-
                // Toggle a diagram block between its rendered view and its source view.
                // Shared by mermaid and svg, css drives the visibility from the wrapper mode.
                const toggleBtn = target.closest(`.${TOGGLE_SOURCE_BTN_CLASS}`);
+
                if (toggleBtn) {
                        event.preventDefault();
                        event.stopPropagation();
 
                        const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`);
+
                        if (!wrapper) return;
 
                        const isSource = wrapper.getAttribute(DIAGRAM_VIEW_MODE_ATTR) === DIAGRAM_VIEW_SOURCE;
                        const next = isSource ? DIAGRAM_VIEW_RENDERED : DIAGRAM_VIEW_SOURCE;
+
                        wrapper.setAttribute(DIAGRAM_VIEW_MODE_ATTR, next);
                        toggleBtn.setAttribute('aria-pressed', String(!isSource));
+
                        return;
                }
 
 
                if (copyBtn || previewBtn) {
                        const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`);
+
                        if (!wrapper) return;
 
                        const preElement = wrapper.querySelector<HTMLElement>(
                                `pre.${MERMAID_BLOCK_CLASS}[${MERMAID_SYNTAX_ATTR}]`
                        );
+
                        if (!preElement) return;
 
                        const mermaidSyntax = preElement.getAttribute(MERMAID_SYNTAX_ATTR) ?? '';
                                } catch (error) {
                                        console.error('Failed to copy mermaid syntax:', error);
                                }
+
                                return;
                        }
 
                                event.preventDefault();
                                event.stopPropagation();
                                const svg = preElement.querySelector('svg');
+
                                if (!svg) return;
+
                                mermaidPreviewSvgHtml = svg.outerHTML;
                                svgPreviewLive = false;
                                mermaidPreviewOpen = true;
+
                                return;
                        }
                }
 
                if (svgCopyBtn || svgPreviewBtn) {
                        const wrapper = target.closest(`.${SVG_WRAPPER_CLASS}`);
+
                        if (!wrapper) return;
 
                        const preElement = wrapper.querySelector<HTMLElement>(
                                `pre.${SVG_BLOCK_CLASS}[${SVG_SOURCE_ATTR}]`
                        );
+
                        if (!preElement) return;
 
                        if (svgCopyBtn) {
                                } catch (error) {
                                        console.error('Failed to copy svg source:', error);
                                }
+
                                return;
                        }
 
                                mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
                                svgPreviewLive = false;
                                mermaidPreviewOpen = true;
+
                                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}`);
+
                if (svgEl) {
                        const source = svgEl.getAttribute(SVG_SOURCE_ATTR);
+
                        if (source !== null) {
                                mermaidPreviewSvgHtml = sanitizeSvg(source);
                                svgPreviewLive = false;
                        } else {
                                svgPreviewLive = true;
                        }
+
                        mermaidPreviewOpen = true;
+
                        return;
                }
 
                // Otherwise, open preview when clicking on the mermaid diagram itself
                const mermaidEl = target.closest(`.${MERMAID_BLOCK_CLASS}`);
+
                if (!mermaidEl) return;
 
                const svg = mermaidEl.querySelector('svg');
+
                if (!svg) return;
 
                mermaidPreviewSvgHtml = svg.outerHTML;
         */
        function handleMermaidPreviewOpenChange(open: boolean) {
                mermaidPreviewOpen = open;
+
                if (!open) {
                        mermaidPreviewSvgHtml = '';
                        svgPreviewLive = false;
                const nodes = containerRef.querySelectorAll(
                        `pre.${MERMAID_BLOCK_CLASS}:not([${MERMAID_RENDERED_ATTR}])`
                );
+
                if (nodes.length === 0) return;
 
                // Mark nodes immediately to prevent duplicate renders if called again during streaming.
 
                // Read mode before await so Svelte tracks it reactively.
                const isDark = mode.current === ColorMode.DARK;
-
                // lazy load the mermaid dependecy only when needed to reduce bundle size.
                const { default: mermaid } = await import('mermaid');
 
                mermaid.initialize({
-                       startOnLoad: false,
-                       theme: isDark ? 'dark' : 'default',
-                       securityLevel: 'strict',
                        flowchart: {
-                               useMaxWidth: false,
-                               htmlLabels: true
-                       },
-                       sequence: {
+                               htmlLabels: true,
                                useMaxWidth: false
                        },
                        gantt: {
                                useMaxWidth: false
-                       }
+                       },
+                       securityLevel: 'strict',
+                       sequence: {
+                               useMaxWidth: false
+                       },
+                       startOnLoad: false,
+                       theme: isDark ? 'dark' : 'default'
                });
 
                try {
                const nodes = containerRef.querySelectorAll<HTMLElement>(
                        `pre.${SVG_BLOCK_CLASS}:not([${SVG_RENDERED_ATTR}])`
                );
+
                if (nodes.length === 0) return;
 
                nodes.forEach((node) => {
                        if (clean) {
                                node.textContent = '';
                                const host = document.createElement('div');
+
                                node.appendChild(host);
                                mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE);
                        }
         */
        function handleImageError(event: Event) {
                const img = event.target as HTMLImageElement;
+
                if (!img || !img.src) return;
 
                // Don't handle data URLs or already-handled images
                        img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING
                )
                        return;
+
                img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING;
 
                const src = img.src;
                // Create fallback element
                const fallback = document.createElement('div');
+
                fallback.className = 'image-load-error';
                fallback.innerHTML = getImageErrorFallbackHtml(src);
 
                try {
                        while (pendingMarkdown !== null) {
                                const nextMarkdown = pendingMarkdown;
+
                                pendingMarkdown = null;
 
                                await processMarkdown(nextMarkdown);
index 80052945f0c5c74c2f99e6d6df84d31f34780406..bd6f28ee7f659518f5b53477916f0147e4f2486a 100644 (file)
@@ -3,8 +3,8 @@
  * Uses dependency injection pattern to avoid direct component state access.
  */
 
+import { MERMAID_BLOCK_CLASS, MERMAID_SYNTAX_ATTR, MERMAID_WRAPPER_CLASS } from '$lib/constants';
 import { copyCodeToClipboard, copyToClipboard } from '$lib/utils';
-import { MERMAID_WRAPPER_CLASS, MERMAID_BLOCK_CLASS, MERMAID_SYNTAX_ATTR } from '$lib/constants';
 
 export interface PreviewState {
        previewDialogOpen: boolean;
@@ -37,12 +37,15 @@ export function createHandleCopyClick() {
                event.stopPropagation();
 
                const target = event.currentTarget as HTMLButtonElement | null;
+
                if (!target) return;
 
                const wrapper = target.closest('.code-block-wrapper');
+
                if (!wrapper) return;
 
                const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
+
                if (!codeElement) return;
 
                const rawCode = codeElement.textContent ?? '';
@@ -80,12 +83,15 @@ export function createHandlePreviewClick(previewState: PreviewState) {
                event.stopPropagation();
 
                const target = event.currentTarget as HTMLButtonElement | null;
+
                if (!target) return;
 
                const wrapper = target.closest('.code-block-wrapper');
+
                if (!wrapper) return;
 
                const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
+
                if (!codeElement) return;
 
                const rawCode = codeElement.textContent ?? '';
@@ -105,18 +111,19 @@ export function createHandlePreviewClick(previewState: PreviewState) {
 export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
        return async function handleMermaidClick(event: MouseEvent) {
                const target = event.target as HTMLElement;
-
                // Check if clicking on copy or preview button in mermaid block
                const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`);
                const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`);
 
                if (copyBtn || previewBtn) {
                        const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`);
+
                        if (!wrapper) return;
 
                        const preElement = wrapper.querySelector<HTMLElement>(
                                `pre.${MERMAID_BLOCK_CLASS}[${MERMAID_SYNTAX_ATTR}]`
                        );
+
                        if (!preElement) return;
 
                        const mermaidSyntax = preElement.getAttribute(MERMAID_SYNTAX_ATTR) ?? '';
@@ -129,6 +136,7 @@ export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
                                } catch (error) {
                                        console.error('Failed to copy mermaid syntax:', error);
                                }
+
                                return;
                        }
 
@@ -136,18 +144,23 @@ export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
                                event.preventDefault();
                                event.stopPropagation();
                                const svg = preElement.querySelector('svg');
+
                                if (!svg) return;
+
                                mermaidState.setMermaidPreviewSvgHtml(svg.outerHTML);
                                mermaidState.setMermaidPreviewOpen(true);
+
                                return;
                        }
                }
 
                // Otherwise, open preview when clicking on the mermaid diagram itself
                const mermaidEl = target.closest(`.${MERMAID_BLOCK_CLASS}`);
+
                if (!mermaidEl) return;
 
                const svg = mermaidEl.querySelector('svg');
+
                if (!svg) return;
 
                mermaidState.setMermaidPreviewSvgHtml(svg.outerHTML);
@@ -162,6 +175,7 @@ export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
 export function createHandleMermaidPreviewOpenChange(mermaidState: MermaidPreviewState) {
        return function handleMermaidPreviewOpenChange(open: boolean) {
                mermaidState.setMermaidPreviewOpen(open);
+
                if (!open) {
                        mermaidState.setMermaidPreviewSvgHtml('');
                }
@@ -180,16 +194,20 @@ export function createHandleImageError(
 ) {
        return async function handleImageError(event: Event) {
                const img = event.target as HTMLImageElement;
+
                if (!img) return;
 
                const blockId = img.closest('[data-block-id]')?.getAttribute('data-block-id');
+
                if (!blockId) return;
 
                const block = renderedBlocksState.renderedBlocks.find((b) => b.id === blockId);
+
                if (!block) return;
 
                // Skip if already handled
                if (img.dataset[DATA_ERROR_BOUND_ATTR] === BOOL_TRUE_STRING) return;
+
                img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
 
                // Get the fallback HTML and replace the image
@@ -197,19 +215,19 @@ export function createHandleImageError(
                        <span class="image-error-icon">⚠️</span>
                        <span class="image-error-text">Failed to load image</span>
                </div>`;
-
                // Replace the img element with fallback in the block's HTML
                const newHtml = block.html.replace(/img[^>]*src=["']([^"']*)[^>]*>/g, (match, src) => {
                        if (src === img.src) {
                                return fallbackHtml.replace('data-original-src=""', `data-original-src="${src}"`);
                        }
+
                        return match;
                });
-
                // Update the block
                const newBlocks = renderedBlocksState.renderedBlocks.map((b) =>
                        b.id === blockId ? { ...b, html: newHtml } : b
                );
+
                renderedBlocksState.setRenderedBlocks(newBlocks);
        };
 }
index dfb56d53caca7249d25409923087b054b644e702..96fcac6cec99b15c279040f1d70e349312e3f92e 100644 (file)
@@ -65,6 +65,7 @@ export function getCodeInfoFromTarget(target: HTMLElement): CodeInfo | null {
 
        if (!wrapper) {
                console.error('No wrapper found');
+
                return null;
        }
 
@@ -72,13 +73,13 @@ export function getCodeInfoFromTarget(target: HTMLElement): CodeInfo | null {
 
        if (!codeElement) {
                console.error('No code element found in wrapper');
+
                return null;
        }
 
        const rawCode = codeElement.textContent ?? '';
-
        const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
        const language = languageLabel?.textContent?.trim() || 'text';
 
-       return { rawCode, language };
+       return { language, rawCode };
 }
index f1dd867e817bd882527e7863f1488f62c673a66a..cd093655ca845252119f3e5db3508e06211c2646 100644 (file)
@@ -3,21 +3,21 @@
  * Contains common HAST element creation functions to avoid code duplication.
  */
 
-import type { Element, ElementContent } from 'hast';
 import {
-       CODE_BLOCK_HEADER_CLASS,
        CODE_BLOCK_ACTIONS_CLASS,
+       CODE_BLOCK_HEADER_CLASS,
        CODE_BLOCK_SCROLL_CONTAINER_CLASS,
+       CODE_ICON_SVG,
        CODE_LANGUAGE_CLASS,
        COPY_CODE_BTN_CLASS,
-       PREVIEW_CODE_BTN_CLASS,
-       TOGGLE_SOURCE_BTN_CLASS,
-       DIAGRAM_SOURCE_CLASS,
-       RELATIVE_CLASS,
        COPY_ICON_SVG,
+       DIAGRAM_SOURCE_CLASS,
+       PREVIEW_CODE_BTN_CLASS,
        PREVIEW_ICON_SVG,
-       CODE_ICON_SVG
+       RELATIVE_CLASS,
+       TOGGLE_SOURCE_BTN_CLASS
 } from '$lib/constants';
+import type { Element, ElementContent } from 'hast';
 
 export interface BlockIdGenerator {
        (id: number): string;
@@ -28,10 +28,10 @@ export interface BlockIdGenerator {
  */
 export function createIconElement(svg: string): Element {
        return {
-               type: 'element',
-               tagName: 'span',
+               children: [{ type: 'raw', value: svg } as unknown as ElementContent],
                properties: {},
-               children: [{ type: 'raw', value: svg } as unknown as ElementContent]
+               tagName: 'span',
+               type: 'element'
        };
 }
 
@@ -48,8 +48,7 @@ export function createButton(
        extraProperties: Record<string, string> = {}
 ): Element {
        return {
-               type: 'element',
-               tagName: 'button',
+               children: [createIconElement(iconSvg)],
                properties: {
                        className: [className],
                        [idAttribute]: id,
@@ -57,7 +56,8 @@ export function createButton(
                        type: 'button',
                        ...extraProperties
                },
-               children: [createIconElement(iconSvg)]
+               tagName: 'button',
+               type: 'element'
        };
 }
 
@@ -105,23 +105,24 @@ export function createSourceView(
        language: string
 ): Element {
        const code: Element = codeElement ?? {
-               type: 'element',
-               tagName: 'code',
+               children: [{ type: 'text', value: source }],
                properties: { className: ['hljs', `language-${language}`] },
-               children: [{ type: 'text', value: source }]
+               tagName: 'code',
+               type: 'element'
        };
+
        return {
-               type: 'element',
-               tagName: 'div',
-               properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
                children: [
                        {
-                               type: 'element',
-                               tagName: 'pre',
+                               children: [code],
                                properties: {},
-                               children: [code]
+                               tagName: 'pre',
+                               type: 'element'
                        }
-               ]
+               ],
+               properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
+               tagName: 'div',
+               type: 'element'
        };
 }
 
@@ -136,23 +137,23 @@ export function createBlockHeader(
        languageClassName: string = CODE_LANGUAGE_CLASS
 ): Element {
        return {
-               type: 'element',
-               tagName: 'div',
-               properties: { className: [CODE_BLOCK_HEADER_CLASS] },
                children: [
                        {
-                               type: 'element',
-                               tagName: 'span',
+                               children: [{ type: 'text', value: language }],
                                properties: { className: [languageClassName] },
-                               children: [{ type: 'text', value: language }]
+                               tagName: 'span',
+                               type: 'element'
                        },
                        {
-                               type: 'element',
-                               tagName: 'div',
+                               children: actions,
                                properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
-                               children: actions
+                               tagName: 'div',
+                               type: 'element'
                        }
-               ]
+               ],
+               properties: { className: [CODE_BLOCK_HEADER_CLASS] },
+               tagName: 'div',
+               type: 'element'
        };
 }
 
@@ -161,10 +162,10 @@ export function createBlockHeader(
  */
 export function createScrollContainer(preElement: Element, scrollContainerClass: string): Element {
        return {
-               type: 'element',
-               tagName: 'div',
+               children: [preElement],
                properties: { className: [scrollContainerClass] },
-               children: [preElement]
+               tagName: 'div',
+               type: 'element'
        };
 }
 
@@ -182,13 +183,13 @@ export function createWrapper(
        extraChildren: Element[] = []
 ): Element {
        return {
-               type: 'element',
-               tagName: 'div',
+               children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren],
                properties: {
                        className: [wrapperClass, RELATIVE_CLASS],
                        ...additionalAttributes
                } as Element['properties'],
-               children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren]
+               tagName: 'div',
+               type: 'element'
        };
 }
 
@@ -199,9 +200,12 @@ export function generateBlockId(prefix: string, windowKey: keyof Window): string
        if (typeof window !== 'undefined') {
                const idx = window[windowKey] as number | undefined;
                const next = (idx ?? 0) + 1;
+
                (window as unknown as Record<string, number>)[windowKey] = next;
+
                return `${prefix}-${next}`;
        }
+
        // Fallback for SSR - use timestamp + random
        return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
 }
index b72e806b6db80ff00e0105d93ab7328450b25e8c..769e535fd782668da752848814257286b5c95282 100644 (file)
  * avoiding the need to stringify and re-parse HTML.
  */
 
-import type { Plugin } from 'unified';
-import type { Root, Element, ElementContent } from 'hast';
-import { visit } from 'unist-util-visit';
-import { CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_BLOCK_WRAPPER_CLASS } from '$lib/constants';
 import {
        createBlockHeader,
        createCopyButton,
@@ -21,6 +17,10 @@ import {
        createWrapper,
        generateBlockId
 } from './code-block-utils';
+import { CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_BLOCK_WRAPPER_CLASS } from '$lib/constants';
+import type { Element, ElementContent, Root } from 'hast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
 
 declare global {
        interface Window {
@@ -30,6 +30,7 @@ declare global {
 
 function extractLanguage(codeElement: Element): string {
        const className = codeElement.properties?.className;
+
        if (!Array.isArray(className)) return 'text';
 
        for (const cls of className) {
index b5fbcbdaae73a2ccaa40b13f01cc7ae5992592b4..880a10cad6edcb82174c1bc51fb967ddbab71131 100644 (file)
@@ -5,8 +5,8 @@
  * ensuring external links open in new tabs safely.
  */
 
+import type { Element, Root } from 'hast';
 import type { Plugin } from 'unified';
-import type { Root, Element } from 'hast';
 import { visit } from 'unist-util-visit';
 
 /**
index 4007c20a19d76ffca05c91cba4c2120723c0dcfe..d1f85b7166e2c42e73d063bf6db300939df3f587 100644 (file)
  * avoiding the need to stringify and re-parse HTML.
  */
 
-import type { Plugin } from 'unified';
-import type { Root, Element, ElementContent } from 'hast';
-import { visit } from 'unist-util-visit';
-import {
-       MERMAID_WRAPPER_CLASS,
-       MERMAID_SCROLL_CONTAINER_CLASS,
-       MERMAID_BLOCK_CLASS,
-       MERMAID_LANGUAGE,
-       MERMAID_SYNTAX_ATTR,
-       MERMAID_ID_ATTR,
-       DIAGRAM_VIEW_MODE_ATTR,
-       DIAGRAM_VIEW_RENDERED
-} from '$lib/constants';
-import type { DiagramPreData } from './pre-transform';
 import {
        createBlockHeader,
        createCopyButton,
        createPreviewButton,
-       createToggleSourceButton,
        createSourceView,
+       createToggleSourceButton,
        createWrapper,
        generateBlockId
 } from './code-block-utils';
+import type { DiagramPreData } from './pre-transform';
+import {
+       DIAGRAM_VIEW_MODE_ATTR,
+       DIAGRAM_VIEW_RENDERED,
+       MERMAID_BLOCK_CLASS,
+       MERMAID_ID_ATTR,
+       MERMAID_LANGUAGE,
+       MERMAID_SCROLL_CONTAINER_CLASS,
+       MERMAID_SYNTAX_ATTR,
+       MERMAID_WRAPPER_CLASS
+} from '$lib/constants';
+import type { Element, ElementContent, Root } from 'hast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
 
 declare global {
        interface Window {
@@ -53,6 +53,7 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
                        if (node.tagName !== 'pre' || !parent || index === undefined) return;
 
                        const className = node.properties?.className;
+
                        if (!Array.isArray(className)) return;
 
                        const isMermaid = className.some(
@@ -62,11 +63,11 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
                        if (!isMermaid) return;
 
                        const mermaidId = generateBlockId(MERMAID_LANGUAGE, 'idxMermaidBlock');
-
                        // Extract the mermaid syntax (text content of the pre element)
                        const diagramText = node.children
                                .map((child) => {
                                        if (child.type === 'text') return child.value;
+
                                        return '';
                                })
                                .join('');
@@ -74,8 +75,8 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
                        // Store the mermaid syntax in data attribute for copy functionality
                        node.properties = {
                                ...node.properties,
-                               [MERMAID_SYNTAX_ATTR]: diagramText,
-                               [MERMAID_ID_ATTR]: mermaidId
+                               [MERMAID_ID_ATTR]: mermaidId,
+                               [MERMAID_SYNTAX_ATTR]: diagramText
                        };
 
                        const actions = [
@@ -83,7 +84,6 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
                                createToggleSourceButton(mermaidId, MERMAID_ID_ATTR, 'Toggle mermaid source'),
                                createPreviewButton(mermaidId, MERMAID_ID_ATTR, 'Preview diagram')
                        ];
-
                        const header = createBlockHeader(MERMAID_LANGUAGE, mermaidId, MERMAID_ID_ATTR, actions);
                        const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
                        const sourceView = createSourceView(preservedCode, diagramText, MERMAID_LANGUAGE);
@@ -93,8 +93,8 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
                                MERMAID_WRAPPER_CLASS,
                                MERMAID_SCROLL_CONTAINER_CLASS,
                                {
-                                       [MERMAID_ID_ATTR]: mermaidId,
-                                       [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED
+                                       [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED,
+                                       [MERMAID_ID_ATTR]: mermaidId
                                },
                                [sourceView]
                        );
index 55bcb6065fda13d0fd3fad23f905b28eace4b5e4..70976ad7c514d4402d10b52b35032bb53c072bdc 100644 (file)
@@ -9,29 +9,29 @@
  * Operates directly on the HAST tree and reuses the shared code-block builders.
  */
 
-import type { Plugin } from 'unified';
-import type { Root, Element, ElementContent } from 'hast';
-import { visit } from 'unist-util-visit';
-import {
-       SVG_WRAPPER_CLASS,
-       SVG_SCROLL_CONTAINER_CLASS,
-       SVG_BLOCK_CLASS,
-       SVG_LANGUAGE,
-       SVG_SOURCE_ATTR,
-       SVG_ID_ATTR,
-       DIAGRAM_VIEW_MODE_ATTR,
-       DIAGRAM_VIEW_RENDERED
-} from '$lib/constants';
-import type { DiagramPreData } from './pre-transform';
 import {
        createBlockHeader,
        createCopyButton,
        createPreviewButton,
-       createToggleSourceButton,
        createSourceView,
+       createToggleSourceButton,
        createWrapper,
        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 type { Element, ElementContent, Root } from 'hast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
 
 declare global {
        interface Window {
@@ -45,6 +45,7 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                        if (node.tagName !== 'pre' || !parent || index === undefined) return;
 
                        const className = node.properties?.className;
+
                        if (!Array.isArray(className)) return;
 
                        const isSvg = className.some((cls) => typeof cls === 'string' && cls === SVG_BLOCK_CLASS);
@@ -52,11 +53,11 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                        if (!isSvg) return;
 
                        const svgId = generateBlockId(SVG_LANGUAGE, 'idxSvgBlock');
-
                        // Extract the svg source (text content of the pre element)
                        const svgSource = node.children
                                .map((child) => {
                                        if (child.type === 'text') return child.value;
+
                                        return '';
                                })
                                .join('');
@@ -64,8 +65,8 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                        // Store the svg source in data attribute for copy and render
                        node.properties = {
                                ...node.properties,
-                               [SVG_SOURCE_ATTR]: svgSource,
-                               [SVG_ID_ATTR]: svgId
+                               [SVG_ID_ATTR]: svgId,
+                               [SVG_SOURCE_ATTR]: svgSource
                        };
 
                        const actions = [
@@ -73,7 +74,6 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                                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 preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
                        const sourceView = createSourceView(preservedCode, svgSource, SVG_LANGUAGE);
@@ -83,8 +83,8 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
                                SVG_WRAPPER_CLASS,
                                SVG_SCROLL_CONTAINER_CLASS,
                                {
-                                       [SVG_ID_ATTR]: svgId,
-                                       [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED
+                                       [DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED,
+                                       [SVG_ID_ATTR]: svgId
                                },
                                [sourceView]
                        );
index 9f9c59fc3b30eddc6099539261e38cb12d79c386..e8bc37eca75ce3eec71e28143fdff97e62763982 100644 (file)
@@ -8,7 +8,6 @@
  * no tab stop); the full path stays available on `title`.
  */
 
-import { decodeFileLinkPath, getMentionBadgeIconPaths, getMentionBadgeLabel } from '$lib/utils';
 import {
        FILE_URI_PREFIX,
        MENTION_BADGE_CLASSNAME,
@@ -19,8 +18,9 @@ import {
 } from '$lib/constants';
 import { settingsStore } from '$lib/stores/settings.svelte';
 import { toolsStore } from '$lib/stores/tools.svelte';
+import { decodeFileLinkPath, getMentionBadgeIconPaths, getMentionBadgeLabel } from '$lib/utils';
+import type { Element, Root } from 'hast';
 import type { Plugin } from 'unified';
-import type { Root, Element } from 'hast';
 import { visit } from 'unist-util-visit';
 
 // Trailing path separators mark a directory and are kept out of the label.
@@ -28,6 +28,7 @@ const TRAILING_SEPARATOR_REGEX = /\/+$/;
 
 function decodeHrefPath(href: string): string {
        const stripped = href.startsWith(FILE_URI_PREFIX) ? href.slice(FILE_URI_PREFIX.length) : href;
+
        return decodeFileLinkPath(stripped);
 }
 
@@ -35,6 +36,7 @@ function labelFromFileUrl(href: string): string {
        const decoded = decodeHrefPath(href);
        const trimmed = decoded.replace(TRAILING_SEPARATOR_REGEX, '');
        const slash = trimmed.lastIndexOf(PATH_SEPARATOR);
+
        return slash === -1 ? trimmed : trimmed.slice(slash + 1);
 }
 
@@ -42,18 +44,18 @@ function labelFromFileUrl(href: string): string {
 // icon, matching the convention the mention picker inserts with.
 function iconElement(href: string): Element {
        return {
-               type: 'element',
-               tagName: 'svg',
+               children: getMentionBadgeIconPaths(href).map((d) => ({
+                       children: [],
+                       properties: { d },
+                       tagName: 'path',
+                       type: 'element'
+               })),
                properties: {
                        ...MENTION_BADGE_SVG_ATTRIBUTES,
                        className: MENTION_BADGE_ICON_CLASSNAME.split(' ').filter(Boolean)
                },
-               children: getMentionBadgeIconPaths(href).map((d) => ({
-                       type: 'element',
-                       tagName: 'path',
-                       properties: { d },
-                       children: []
-               }))
+               tagName: 'svg',
+               type: 'element'
        };
 }
 
@@ -79,9 +81,6 @@ export const rehypeFileBadge: Plugin<[], Root> = () => {
                        node.children = [
                                iconElement(href),
                                {
-                                       type: 'element',
-                                       tagName: 'span',
-                                       properties: { className: ['shrink-0', 'truncate'] },
                                        children: [
                                                {
                                                        type: 'text',
@@ -92,7 +91,10 @@ export const rehypeFileBadge: Plugin<[], Root> = () => {
                                                                toolsStore.serverHome
                                                        )
                                                }
-                                       ]
+                                       ],
+                                       properties: { className: ['shrink-0', 'truncate'] },
+                                       tagName: 'span',
+                                       type: 'element'
                                }
                        ];
                });
index 7aa967bb81daa98e29688b024abe780dcc43d26a..755fa1ecdb88ec71452e941e39fe932d2432e05a 100644 (file)
@@ -1,5 +1,5 @@
+import type { Element, ElementContent, Root, Text } from 'hast';
 import type { Plugin } from 'unified';
-import type { Root, Element, ElementContent, Text } from 'hast';
 import { visit } from 'unist-util-visit';
 
 /**
@@ -17,9 +17,11 @@ export interface DiagramPreData {
  */
 function extractText(node: ElementContent): string {
        if (node.type === 'text') return node.value;
+
        if (node.type === 'element') {
                return (node.children ?? []).map(extractText).join('');
        }
+
        return '';
 }
 
@@ -57,6 +59,7 @@ export function createPreTransform(
                                if (!codeElement) return;
 
                                const className = codeElement.properties?.className;
+
                                if (!Array.isArray(className)) return;
 
                                const matches = className.some(
@@ -73,15 +76,15 @@ export function createPreTransform(
                                if (contentGuard && !contentGuard(text)) return;
 
                                const pre: Element = {
-                                       type: 'element',
-                                       tagName: 'pre',
-                                       properties: {
-                                               className: [targetClass]
-                                       },
                                        children: [{ type: 'text', value: text } as Text],
                                        // Keep the highlighted code element so the block can offer a source
                                        // view that matches the app code blocks without re highlighting.
-                                       data: { sourceCode: codeElement } satisfies DiagramPreData
+                                       data: { sourceCode: codeElement } satisfies DiagramPreData,
+                                       properties: {
+                                               className: [targetClass]
+                                       },
+                                       tagName: 'pre',
+                                       type: 'element'
                                };
 
                                (parent.children as ElementContent[])[index] = pre;
index 0a8b93ad547207d6ed71e840feee0391f7792f86..b63dddbdbe3b69310073bdead50926cd19d5e480 100644 (file)
@@ -6,8 +6,8 @@
  * (including those not in a predefined list) receive the attribute.
  */
 
+import type { Element, Root } from 'hast';
 import type { Plugin } from 'unified';
-import type { Root, Element } from 'hast';
 import { visit } from 'unist-util-visit';
 
 /**
index 36e7a3192bc1ac6ffed6ad81a96a14d80b26bf47..5d3ade0f148657be003aab85fb3203c9dd931fac 100644 (file)
@@ -1,7 +1,7 @@
+import { AttachmentType, UrlProtocol } from '$lib/enums';
+import type { DatabaseMessageExtra, DatabaseMessageExtraImageFile } from '$lib/types/database';
 import type { Root as HastRoot } from 'hast';
 import { visit } from 'unist-util-visit';
-import type { DatabaseMessageExtra, DatabaseMessageExtraImageFile } from '$lib/types/database';
-import { AttachmentType, UrlProtocol } from '$lib/enums';
 
 /**
  * Rehype plugin to resolve attachment image sources.
index eb0e2c699bf5d5fcfe4623563b73e17eb6a41c20..df2cd5cbd64a974a9d8b4cce2004a40dcf580b2d 100644 (file)
@@ -1,5 +1,5 @@
 import { createPreTransform } from './pre-transform';
-import { SVG_BLOCK_CLASS, SVG_LANGUAGE, XML_LANGUAGE, SVG_TAG_PREFIX } from '$lib/constants';
+import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_TAG_PREFIX, XML_LANGUAGE } from '$lib/constants';
 
 /**
  * Converts svg code blocks to <pre class="svg-block"> for client-side rendering.
index bc5d034653f20586a61878991242e833fb4258d4..1dd0247fea1784ff55a80e0cfaf1ca9bf741ca09 100644 (file)
  * // With this plugin: <br> becomes line break, <ul> becomes actual list
  */
 
-import type { Plugin } from 'unified';
+import { BR_PATTERN, LI_PATTERN, LIST_PATTERN } from '$lib/constants';
 import type { Element, ElementContent, Root, Text } from 'hast';
+import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 import { visitParents } from 'unist-util-visit-parents';
-import { BR_PATTERN, LIST_PATTERN, LI_PATTERN } from '$lib/constants';
 
 /**
  * Expands text containing `<br>` tags into an array of text nodes and br elements.
  */
 function expandBrTags(value: string): ElementContent[] {
        const matches = [...value.matchAll(BR_PATTERN)];
+
        if (!matches.length) return [{ type: 'text', value } as Text];
 
        const result: ElementContent[] = [];
+
        let cursor = 0;
 
        for (const m of matches) {
                if (m.index! > cursor) {
                        result.push({ type: 'text', value: value.slice(cursor, m.index) } as Text);
                }
-               result.push({ type: 'element', tagName: 'br', properties: {}, children: [] } as Element);
+
+               result.push({ children: [], properties: {}, tagName: 'br', type: 'element' } as Element);
                cursor = m.index! + m[0].length;
        }
 
@@ -101,10 +104,12 @@ function expandBrTags(value: string): ElementContent[] {
  */
 function parseList(value: string): Element | null {
        const match = value.trim().match(LIST_PATTERN);
+
        if (!match) return null;
 
        const body = match[1];
        const items: ElementContent[] = [];
+
        let cursor = 0;
 
        for (const liMatch of body.matchAll(LI_PATTERN)) {
@@ -112,10 +117,10 @@ function parseList(value: string): Element | null {
                if (body.slice(cursor, liMatch.index!).trim()) return null;
 
                items.push({
-                       type: 'element',
-                       tagName: 'li',
+                       children: expandBrTags(liMatch[1] ?? ''),
                        properties: {},
-                       children: expandBrTags(liMatch[1] ?? '')
+                       tagName: 'li',
+                       type: 'element'
                } as Element);
 
                cursor = liMatch.index! + liMatch[0].length;
@@ -124,7 +129,7 @@ function parseList(value: string): Element | null {
        // Reject if no items found or trailing garbage exists
        if (!items.length || body.slice(cursor).trim()) return null;
 
-       return { type: 'element', tagName: 'ul', properties: {}, children: items } as Element;
+       return { children: items, properties: {}, tagName: 'ul', type: 'element' } as Element;
 }
 
 /**
@@ -133,11 +138,13 @@ function parseList(value: string): Element | null {
 function processCell(cell: Element) {
        visitParents(cell, 'text', (textNode: Text, ancestors) => {
                const parent = ancestors[ancestors.length - 1];
+
                if (!parent || parent.type !== 'element') return;
 
                const parentEl = parent as Element;
                const siblings = parentEl.children as ElementContent[];
                const startIndex = siblings.indexOf(textNode as ElementContent);
+
                if (startIndex === -1) return;
 
                // Combine consecutive text nodes and <br> elements into one string
@@ -146,6 +153,7 @@ function processCell(cell: Element) {
 
                for (let i = startIndex; i < siblings.length; i++) {
                        const sib = siblings[i];
+
                        if (sib.type === 'text') {
                                combined += (sib as Text).value;
                                endIndex = i;
@@ -159,13 +167,16 @@ function processCell(cell: Element) {
 
                // Try parsing as list first (replaces entire combined range)
                const list = parseList(combined);
+
                if (list) {
                        siblings.splice(startIndex, endIndex - startIndex + 1, list);
+
                        return;
                }
 
                // Otherwise, just expand <br> tags in this text node
                const expanded = expandBrTags(textNode.value);
+
                if (expanded.length !== 1 || expanded[0] !== textNode) {
                        siblings.splice(startIndex, 1, ...expanded);
                }
index c974d8b1893ed5bfa44568e3791c0ef9a73177b2..5183fe5310427a86ec3a6fb1233a07381cd1413f 100644 (file)
@@ -1,7 +1,7 @@
+import { LINE_BREAK, NBSP, PHRASE_PARENTS, TAB_AS_SPACES } from '$lib/constants';
+import type { Break, Content, Paragraph, PhrasingContent, Root, Text } from 'mdast';
 import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
-import type { Break, Content, Paragraph, PhrasingContent, Root, Text } from 'mdast';
-import { LINE_BREAK, NBSP, PHRASE_PARENTS, TAB_AS_SPACES } from '$lib/constants';
 
 /**
  * remark plugin that rewrites raw HTML nodes into plain-text equivalents.
@@ -23,12 +23,14 @@ function preserveIndent(line: string): string {
                if (char === ' ') {
                        output += NBSP;
                        index += 1;
+
                        continue;
                }
 
                if (char === '\t') {
                        output += TAB_AS_SPACES;
                        index += 1;
+
                        continue;
                }
 
@@ -71,12 +73,12 @@ export const remarkLiteralHtml: Plugin<[], Root> = () => {
 
                        if (!PHRASE_PARENTS.has(parent.type as string)) {
                                const paragraph: Paragraph = {
-                                       type: 'paragraph',
                                        children: replacement as Paragraph['children'],
-                                       data: { literalHtml: true }
+                                       data: { literalHtml: true },
+                                       type: 'paragraph'
                                };
-
                                const siblings = parent.children as unknown as Content[];
+
                                siblings.splice(index, 1, paragraph as unknown as Content);
 
                                if (index > 0) {
index a30f585b93cb7c2df273246a30642592c13ca8f4..417600211933da62acf698b3d7b0dc905de3cc14 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import MermaidPreviewControls from './MermaidPreviewControls.svelte';
-       import { mountSvgShadow } from '$lib/utils/svg-shadow';
        import { SVG_DIALOG_SHADOW_STYLE } from '$lib/constants';
+       import { mountSvgShadow } from '$lib/utils/svg-shadow';
 
        interface Props {
                svgHtml: string;
@@ -51,6 +51,7 @@
                event.preventDefault();
 
                const delta = event.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
+
                scale = Math.min(Math.max(scale + delta, MIN_SCALE), MAX_SCALE);
        }
 
@@ -58,6 +59,7 @@
        // (Svelte 5 wheel listeners are passive by default, making preventDefault() a no-op)
        $effect(() => {
                const el = containerRef.current;
+
                if (!el) return;
 
                function onWheel(e: WheelEvent) {
@@ -65,6 +67,7 @@
                }
 
                el.addEventListener('wheel', onWheel, { passive: false });
+
                return () => el.removeEventListener('wheel', onWheel);
        });
 
index 39540e7a8cd51a22fb4668066edcd72a489bb605..1dd341f5a49c450efae981e19bd665b77b2f6111 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { Download } from '@lucide/svelte';
+       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 RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
 
        interface Props {
                scale: number;
                onResetView: () => void;
        }
 
-       let { scale, svgHtml, onZoomIn, onZoomOut, onResetView }: Props = $props();
+       let { onResetView, onZoomIn, onZoomOut, scale, svgHtml }: Props = $props();
 
        function downloadSvg() {
                if (!svgHtml) return;
+
                const blob = new Blob([svgHtml], { type: 'image/svg+xml' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
+
                a.href = url;
                a.download = 'diagram.svg';
                a.click();
index 2d5725f559d2b58cd28612890286fa57aca6dfdc..1261de4a899cec353dd2297a866b8e5919ce7813 100644 (file)
@@ -1,12 +1,11 @@
 <script lang="ts">
        import { browser } from '$app/environment';
-       import { mode } from 'mode-watcher';
-
-       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
-       import githubLightCss from 'highlight.js/styles/github.css?inline';
-       import { ColorMode } from '$lib/enums';
        import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
+       import { ColorMode } from '$lib/enums';
        import { highlightCode } from '$lib/utils';
+       import githubLightCss from 'highlight.js/styles/github.css?inline';
+       import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
+       import { mode } from 'mode-watcher';
 
        interface Props {
                code: string;
@@ -20,9 +19,9 @@
        }
 
        let {
+               class: className = '',
                code,
                language = 'text',
-               class: className = '',
                maxHeight = '60vh',
                maxWidth = '',
                streaming = false
                if (!browser) return;
 
                const existingThemes = document.querySelectorAll('style[data-highlight-theme-preview]');
+
                existingThemes.forEach((style) => style.remove());
 
                const style = document.createElement('style');
+
                style.setAttribute('data-highlight-theme-preview', 'true');
                style.textContent = isDark ? githubDarkCss : githubLightCss;
 
@@ -51,6 +52,7 @@
 
        function isAtBottom(): boolean {
                if (!scrollEl) return false;
+
                return (
                        scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
                        SCROLL_BOTTOM_THRESHOLD_PX
 
        function scrollToBottomOnFrame() {
                if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
+
                pendingFrame = requestAnimationFrame(() => {
                        pendingFrame = null;
+
                        // User may scroll between scheduling and paint.
                        if (scrollEl && !userScrolledUp) {
                                scrollEl.scrollTop = scrollEl.scrollHeight;
 
        function handleScrollEvent() {
                if (!scrollEl) return;
+
                const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
+
                if (isScrollingUp && !isAtBottom()) {
                        userScrolledUp = true;
                } else if (isAtBottom()) {
                        userScrolledUp = false;
                }
+
                lastScrollTop = scrollEl.scrollTop;
        }
 
 
        $effect(() => {
                void code;
+
                if (!streaming || userScrolledUp) return;
+
                scrollToBottomOnFrame();
        });
 
                if (!streaming || !scrollEl) return;
 
                const observer = new MutationObserver(() => scrollToBottomOnFrame());
+
                observer.observe(scrollEl, {
+                       characterData: true,
                        childList: true,
-                       subtree: true,
-                       characterData: true
+                       subtree: true
                });
 
                return () => observer.disconnect();
index 533301dfdad3a76f2404dc6b7a56d3f4de37517f..e0b4138baa26318e269273be6c7b1044478b370b 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { Dialog } from 'bits-ui';
        import { X } from '@lucide/svelte';
-       import * as DialogUI from '$lib/components/ui/dialog';
        import { ChatAttachmentsPreview } from '$lib/components/app';
+       import * as DialogUI from '$lib/components/ui/dialog';
        import { KeyboardKey } from '$lib/enums';
+       import { Dialog } from 'bits-ui';
 
        interface Props {
                open: boolean;
        }
 
        let {
-               open = $bindable(false),
-               uploadedFiles = [],
-               attachments = [],
                activeModelId,
-               previewFocusIndex = 0
+               attachments = [],
+               open = $bindable(false),
+               previewFocusIndex = 0,
+               uploadedFiles = []
        }: Props = $props();
 
        function handleClose() {
@@ -59,6 +59,7 @@
                }
 
                document.addEventListener('keydown', handleKeyDown);
+
                return () => document.removeEventListener('keydown', handleKeyDown);
        });
 </script>
index ff1005313e5ce742301ad709f865e3bd293637df..c2429fe4df91ddeb69abb5e12de5cf9269d1cc96 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { AlertTriangle, TimerOff } from '@lucide/svelte';
+       import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { ErrorDialogType } from '$lib/enums';
 
        interface Props {
@@ -11,7 +11,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), type, message, contextInfo, onOpenChange }: Props = $props();
+       let { contextInfo, message, onOpenChange, open = $bindable(), type }: Props = $props();
 
        const isTimeout = $derived(type === ErrorDialogType.TIMEOUT);
        const title = $derived(isTimeout ? 'TCP Timeout' : 'Server Error');
index fe5d9b504b88a8c988b2b4fc159e4a9064cf3492..3c6012d823340e6770fed3829bf75d6d2ef2791c 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { Dialog as DialogPrimitive } from 'bits-ui';
        import XIcon from '@lucide/svelte/icons/x';
+       import { Dialog as DialogPrimitive } from 'bits-ui';
 
        interface Props {
                open: boolean;
@@ -9,7 +9,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), code, language, onOpenChange }: Props = $props();
+       let { code, language, onOpenChange, open = $bindable() }: Props = $props();
 
        let iframeRef = $state<HTMLIFrameElement | null>(null);
 
index becc658d3c5738ed15fd0ceb185716417bd0d607..409cbc3a9698673634585d60be565b7dc06d9537 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import * as AlertDialog from '$lib/components/ui/alert-dialog';
-       import type { Component, Snippet } from 'svelte';
        import { KeyboardKey } from '$lib/enums';
+       import type { Component, Snippet } from 'svelte';
 
        interface Props {
                open: boolean;
        }
 
        let {
-               open = $bindable(),
-               title,
-               description,
-               confirmText = 'Confirm',
                cancelText = 'Cancel',
-               variant = 'default',
+               children,
+               confirmText = 'Confirm',
+               description,
                icon,
-               onConfirm,
                onCancel,
+               onConfirm,
                onKeydown,
-               children
+               open = $bindable(),
+               title,
+               variant = 'default'
        }: Props = $props();
 
        function handleKeydown(event: KeyboardEvent) {
@@ -37,6 +37,7 @@
 
                        onConfirm();
                }
+
                onKeydown?.(event);
        }
 
index d85340f3fb6af908bf500b18b3043f6694218d62..e4c4e0bbc3fb934fd06c7aa7fd377c9ba6d2af8a 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
+       import { Pencil } from '@lucide/svelte';
        import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { Button } from '$lib/components/ui/button';
        import { Input } from '$lib/components/ui/input';
-       import { Pencil } from '@lucide/svelte';
 
        interface Props {
                open: boolean;
        }
 
        let {
-               open = $bindable(),
                currentTitle,
-               value = $bindable(''),
+               onCancel,
                onConfirm,
-               onCancel
+               open = $bindable(),
+               value = $bindable('')
        }: Props = $props();
 
        let inputRef = $state<HTMLInputElement | null>(null);
@@ -42,7 +42,9 @@
 
        function handleSubmit(event: Event) {
                event.preventDefault();
+
                if (!canSubmit) return;
+
                value = value.trim();
                onConfirm();
        }
index 5f5b2f4ab315497e240bb779e85049b046554f03..4e40591ce9f28dda0b02c827d0315a9adaec8234 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as Dialog from '$lib/components/ui/dialog';
        import { ConversationSelection } from '$lib/components/app';
+       import * as Dialog from '$lib/components/ui/dialog';
 
        interface Props {
                conversations: DatabaseConversation[];
index f875b0abaeba33eb85ce179a15d16f55d0616e5d..9e1c8057a8c41cfcf057650399ed8a300caa452a 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { FileX } from '@lucide/svelte';
+       import * as AlertDialog from '$lib/components/ui/alert-dialog';
 
        interface Props {
                open: boolean;
@@ -8,7 +8,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), emptyFiles, onOpenChange }: Props = $props();
+       let { emptyFiles, onOpenChange, open = $bindable() }: Props = $props();
 
        function handleOpenChange(newOpen: boolean) {
                open = newOpen;
index fe36dce56e526a758eba65add98a91133727ce02..3b11e5dad43eefc04a51d8a31a270eb9a86de9e3 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
+       import { Shield, ShieldOff } from '@lucide/svelte';
        import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { Checkbox } from '$lib/components/ui/checkbox';
        import Label from '$lib/components/ui/label/label.svelte';
-       import { Shield, ShieldOff } from '@lucide/svelte';
 
        let {
-               open = $bindable(),
                includeSensitiveData = $bindable(false),
                onCancel,
-               onConfirm
+               onConfirm,
+               open = $bindable()
        }: {
                open: boolean;
                includeSensitiveData: boolean;
index 3bb2d357f537045e027dec0144d31aa27733b8e9..0d5a9984cb4bbe9d911108fbe68d48f20a34796c 100644 (file)
@@ -12,7 +12,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), fileErrorData, onOpenChange }: Props = $props();
+       let { fileErrorData, onOpenChange, open = $bindable() }: Props = $props();
 
        function handleOpenChange(newOpen: boolean) {
                open = newOpen;
index 7bf284089c97d0a8eba79829e303ede81380215e..537dd4c50c0fd2dae485bebed0322ff2587ca166 100644 (file)
@@ -1,18 +1,18 @@
 <script lang="ts">
-       import * as Dialog from '$lib/components/ui/dialog';
        import { Download } from '@lucide/svelte';
+       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 { mcpStore } from '$lib/stores/mcp.svelte';
-       import { SyntaxHighlightedCode, ActionIconCopyToClipboard } from '$lib/components/app';
+       import type { DatabaseMessageExtraMcpResource } from '$lib/types';
        import {
+               downloadResourceContent,
                getLanguageFromFilename,
                isCodeResource,
-               isImageResource,
-               downloadResourceContent
+               isImageResource
        } from '$lib/utils';
-       import { MimeTypeIncludes, MimeTypeText } from '$lib/enums';
-       import { DEFAULT_RESOURCE_FILENAME } from '$lib/constants';
-       import type { DatabaseMessageExtraMcpResource } from '$lib/types';
 
        interface Props {
                open: boolean;
                extra: DatabaseMessageExtraMcpResource;
        }
 
-       let { open = $bindable(), onOpenChange, extra }: Props = $props();
+       let { extra, onOpenChange, open = $bindable() }: Props = $props();
 
        const serverName = $derived(mcpStore.getServerDisplayName(extra.serverName));
        const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
 
        function getLanguage(): string {
                if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return MimeTypeIncludes.JSON;
+
                if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return MimeTypeIncludes.JAVASCRIPT;
+
                if (extra.mimeType?.includes(MimeTypeIncludes.TYPESCRIPT)) return MimeTypeIncludes.TYPESCRIPT;
 
                const name = extra.name || extra.uri || '';
index f741b544bb7a55271fc35ef6e8be13bab8f644c1..c208250cdd8a985004732b86b7b3265e4ea43c1f 100644 (file)
@@ -1,24 +1,24 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { FolderOpen, Plus, Loader2, Braces } from '@lucide/svelte';
-       import { toast } from 'svelte-sonner';
-       import * as Dialog from '$lib/components/ui/dialog';
+       import { Braces, FolderOpen, Loader2, Plus } from '@lucide/svelte';
+       import {
+               McpResourcePreview,
+               McpResourcesBrowser,
+               McpResourceTemplateForm
+       } from '$lib/components/app';
        import { Button } from '$lib/components/ui/button';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import * as Dialog from '$lib/components/ui/dialog';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
        import {
                mcpResources,
-               mcpTotalResourceCount,
-               mcpResourceStore
+               mcpResourceStore,
+               mcpTotalResourceCount
        } from '$lib/stores/mcp-resources.svelte';
-       import {
-               McpResourcesBrowser,
-               McpResourcePreview,
-               McpResourceTemplateForm
-       } from '$lib/components/app';
+       import type { MCPResourceContent, MCPResourceInfo, MCPResourceTemplateInfo } from '$lib/types';
        import { getResourceDisplayName } from '$lib/utils';
-       import type { MCPResourceInfo, MCPResourceContent, MCPResourceTemplateInfo } from '$lib/types';
        import { SvelteSet } from 'svelte/reactivity';
+       import { toast } from 'svelte-sonner';
 
        interface Props {
                open?: boolean;
@@ -27,7 +27,7 @@
                preSelectedUri?: string;
        }
 
-       let { open = $bindable(false), onOpenChange, onAttach, preSelectedUri }: Props = $props();
+       let { onAttach, onOpenChange, open = $bindable(false), preSelectedUri }: Props = $props();
 
        let selectedResources = new SvelteSet<string>();
        let lastSelectedUri = $state<string | null>(null);
                                if (mcpResourceStore.isAttached(templatePreviewUri)) {
                                        toast.info('Resource already attached');
                                        handleOpenChange(false);
+
                                        return;
                                }
 
                                const resourceInfo: MCPResourceInfo = {
-                                       uri: templatePreviewUri,
                                        name: templatePreviewUri.split('/').pop() || templatePreviewUri,
-                                       serverName: selectedTemplate.serverName
+                                       serverName: selectedTemplate.serverName,
+                                       uri: templatePreviewUri
                                };
-
                                const attachment = mcpResourceStore.addAttachment(resourceInfo);
+
                                mcpResourceStore.updateAttachmentContent(attachment.id, templatePreviewContent);
 
                                toast.success(`Resource attached: ${resourceInfo.name}`);
                return allResources.sort((a, b) => {
                        const aName = getResourceDisplayName(a);
                        const bName = getResourceDisplayName(b);
+
                        return aName.localeCompare(bName);
                });
        }
                                        <!-- Template resolved: show preview -->
                                        <McpResourcePreview
                                                resource={{
-                                                       uri: templatePreviewUri ?? '',
                                                        name: templatePreviewUri?.split('/').pop() || (templatePreviewUri ?? ''),
-                                                       serverName: selectedTemplate?.serverName || ''
+                                                       serverName: selectedTemplate?.serverName || '',
+                                                       uri: templatePreviewUri ?? ''
                                                }}
                                                preloadedContent={templatePreviewContent}
                                        />
index 9ec57a5582351c95862263a0ac5902588368fb40..09780173a5effc6d21bef357e1afea91c23e89aa 100644 (file)
@@ -1,10 +1,8 @@
 <script lang="ts">
+       import { browser } from '$app/environment';
+       import { McpServerCardCompact, McpServerForm } from '$lib/components/app/mcp';
        import { Button } from '$lib/components/ui/button';
        import * as Dialog from '$lib/components/ui/dialog';
-       import { McpServerCardCompact, McpServerForm } from '$lib/components/app/mcp';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { parseHeadersToArray, uuid, canonicalizeServerUrl } from '$lib/utils';
        import {
                BEARER_PREFIX,
                BOOL_FALSE_STRING,
                RECOMMENDED_MCP_SERVERS,
                REDACTED_HEADERS
        } from '$lib/constants';
-       import { browser } from '$app/environment';
        import { HealthCheckStatus } from '$lib/enums';
+       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { canonicalizeServerUrl, parseHeadersToArray, uuid } from '$lib/utils';
 
        interface Props {
                open: boolean;
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), onOpenChange }: Props = $props();
+       let { onOpenChange, open = $bindable() }: Props = $props();
 
        let newServerUrl = $state('');
        let newServerName = $state('');
 
        let selectedRecommendationId = $derived.by(() => {
                const url = newServerUrl.trim();
+
                if (!url) return null;
+
                const targetCanonical = canonicalizeServerUrl(url);
+
                return (
                        RECOMMENDED_MCP_SERVERS.find((rec) => canonicalizeServerUrl(rec.url) === targetCanonical)
                                ?.id ?? null
@@ -72,6 +75,7 @@
 
        let newServerUrlError = $derived.by(() => {
                if (!newServerUrl.trim()) return 'URL is required';
+
                try {
                        new URL(newServerUrl);
 
        // Backward-compatible read: older versions stored a JSON array of dismissed ids.
        function readRecommendationsDismissed(): boolean {
                if (!browser) return false;
+
                const raw = localStorage.getItem(DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY);
 
                if (!raw) return false;
 
                if (raw === BOOL_TRUE_STRING) return true;
+
                if (raw === BOOL_FALSE_STRING) return false;
 
                try {
                        const parsed = JSON.parse(raw);
+
                        return Array.isArray(parsed) && parsed.length > 0;
                } catch {
                        return false;
                const previewId = `${MCP_SERVER_ID_PREFIX}-preview-${run}`;
                const timer = setTimeout(async () => {
                        await mcpStore.runHealthCheck({
-                               id: previewId,
                                enabled: false,
-                               url,
                                headers: headers || undefined,
+                               id: previewId,
+                               url,
                                useProxy
                        });
 
                        newServerUseProxy = false;
                        newServerWantsAuthorization = false;
                }
+
                open = value;
                onOpenChange?.(value);
        }
                const newServerId = uuid() ?? `${MCP_SERVER_ID_PREFIX}-${Date.now()}`;
 
                mcpStore.addServer({
-                       id: newServerId,
-                       enabled: true,
-                       url: newServerUrl.trim(),
                        // A name equal to the autofilled server-reported one is not a
                        // customization: keep following the automatic label.
                        displayName:
                                newServerName.trim() && newServerName.trim() !== nameAutoFilled.trim()
                                        ? newServerName.trim()
                                        : undefined,
+                       enabled: true,
                        headers: newServerHeaders.trim() || undefined,
+                       id: newServerId,
+                       url: newServerUrl.trim(),
                        useProxy: newServerUseProxy
                });
 
index 9cbeebc36af2d7209cdd63db704e6840df911a24..09e53442ac659f06835ce4dac328534398b85e51 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as Dialog from '$lib/components/ui/dialog/index.js';
        import { MermaidPreview } from '$lib/components/app/content';
+       import * as Dialog from '$lib/components/ui/dialog/index.js';
 
        interface Props {
                open: boolean;
@@ -8,7 +8,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), svgHtml, onOpenChange }: Props = $props();
+       let { onOpenChange, open = $bindable(), svgHtml }: Props = $props();
 </script>
 
 <Dialog.Root bind:open {onOpenChange}>
index 5a10859a080929512ce10ae3cb40661e8831d27d..9318c6bd616988da98c35a95ee39b10eefc448cf 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
+       import { ActionIconCopyToClipboard, BadgesModality } from '$lib/components/app';
        import * as Dialog from '$lib/components/ui/dialog';
        import * as Table from '$lib/components/ui/table';
-       import { BadgesModality, ActionIconCopyToClipboard } from '$lib/components/app';
+       import { modelOptions, modelsLoading, modelsStore } from '$lib/stores/models.svelte';
        import { serverStore } from '$lib/stores/server.svelte';
-       import { modelsStore, modelOptions, modelsLoading } from '$lib/stores/models.svelte';
-       import { formatFileSize, formatParameters, formatNumber } from '$lib/utils';
        import type { ApiLlamaCppServerProps } from '$lib/types';
+       import { formatFileSize, formatNumber, formatParameters } from '$lib/utils';
 
        interface Props {
                open?: boolean;
@@ -14,7 +14,7 @@
                modelId?: string | null;
        }
 
-       let { open = $bindable(), onOpenChange, modelId = null }: Props = $props();
+       let { modelId = null, onOpenChange, open = $bindable() }: Props = $props();
 
        let isRouter = $derived(serverStore.isRouterMode);
 
                if (isRouter && modelId) {
                        return models.find((m) => m.model === modelId) ?? null;
                }
+
                return models[0] ?? null;
        });
 
        // Get modalities from modelStore using the model ID from the first model
        let modalities = $derived.by(() => {
                if (!firstModel?.id) return [];
+
                return modelsStore.getModelModalitiesArray(firstModel.id);
        });
 
@@ -67,6 +69,7 @@
                                        isLoadingRouterProps = false;
                                });
                }
+
                if (!open) {
                        routerModelProps = null;
                }
index 583c5fcb0394338bf5644cae7dec9338141d81b2..e77923bb34cc0c1516db94e43fc3349ab335d275 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { URL_PARAMS } from '$lib/constants';
-       import * as AlertDialog from '$lib/components/ui/alert-dialog';
        import { AlertTriangle, ArrowRight } from '@lucide/svelte';
        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';
 
        interface Props {
                open: boolean;
@@ -13,7 +13,7 @@
                onOpenChange?: (open: boolean) => void;
        }
 
-       let { open = $bindable(), modelName, availableModels = [], onOpenChange }: Props = $props();
+       let { availableModels = [], modelName, onOpenChange, open = $bindable() }: Props = $props();
 
        function handleOpenChange(newOpen: boolean) {
                open = newOpen;
@@ -23,6 +23,7 @@
        function handleSelectModel(model: string) {
                // Build URL with selected model, preserving other params
                const url = new URL(page.url);
+
                url.searchParams.set(URL_PARAMS.MODEL, model);
 
                handleOpenChange(false);
index 880d21a7e772cd1c9729312908c54447c34bbee5..cb42890570fced94de7e3fcc787a553deb305727 100644 (file)
@@ -8,9 +8,9 @@
        }
 
        let {
-               text,
+               matchClass = 'rounded bg-yellow-200/60 px-0.5 text-foreground dark:bg-yellow-500/30',
                query,
-               matchClass = 'rounded bg-yellow-200/60 px-0.5 text-foreground dark:bg-yellow-500/30'
+               text
        }: Props = $props();
 
        let segments = $derived(highlightMatch(text, query));
index 5d047c59a961182635b8dbda3e930c1989deb65b..6134067964ed5b2b7c065363bbceadb5225baea8 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { fly } from 'svelte/transition';
        import { Input } from '$lib/components/ui/input';
        import { Label } from '$lib/components/ui/label';
+       import { fly } from 'svelte/transition';
 
        interface Props {
                name: string;
        }
 
        let {
-               name,
-               value = '',
-               suggestions = [],
-               isLoadingSuggestions = false,
-               isAutocompleteActive = false,
                autocompleteIndex = 0,
-               onInput,
-               onKeydown,
+               isAutocompleteActive = false,
+               isLoadingSuggestions = false,
+               name,
                onBlur,
                onFocus,
-               onSelectSuggestion
+               onInput,
+               onKeydown,
+               onSelectSuggestion,
+               suggestions = [],
+               value = ''
        }: Props = $props();
 </script>
 
@@ -60,7 +60,7 @@
        {#if isAutocompleteActive && suggestions.length > 0}
                <div
                        class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
-                       transition:fly={{ y: -5, duration: 100 }}
+                       transition:fly={{ duration: 100, y: -5 }}
                >
                        {#each suggestions as suggestion, i (suggestion)}
                                <button
index f06480774f0db29fc2359ad6967d84ec47f8e16a..f83f53d94423db4488728b94678d19fa6593a0cd 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { tick } from 'svelte';
        import { Plus, Trash2 } from '@lucide/svelte';
        import { Input } from '$lib/components/ui/input';
+       import { KEY_VALUE_PAIR_KEY_MAX_LENGTH, KEY_VALUE_PAIR_VALUE_MAX_LENGTH } from '$lib/constants';
+       import type { KeyValuePair } from '$lib/types';
        import {
                autoResizeTextarea,
                sanitizeKeyValuePairKey,
                sanitizeKeyValuePairValue
        } from '$lib/utils';
-       import { KEY_VALUE_PAIR_KEY_MAX_LENGTH, KEY_VALUE_PAIR_VALUE_MAX_LENGTH } from '$lib/constants';
-       import type { KeyValuePair } from '$lib/types';
+       import { tick } from 'svelte';
 
        interface Props {
                class?: string;
        }
 
        let {
-               class: className = '',
-               pairs,
-               onPairsChange,
-               keyPlaceholder = 'Key',
-               valuePlaceholder = 'Value',
                addButtonLabel = 'Add',
+               class: className = '',
                emptyMessage = 'No items configured.',
+               keyPlaceholder = 'Key',
+               onPairsChange,
+               pairs,
                sectionLabel,
-               sectionLabelOptional = true
+               sectionLabelOptional = true,
+               valuePlaceholder = 'Value'
        }: Props = $props();
 
        // Pre-allocate the ref array so `bind:ref={keyInputRefs[index]}` never reads `undefined`
@@ -43,6 +43,7 @@
                // Capture the target index before mutating so deletions earlier in the
                // list can't make keyInputRefs.length drift past the newly-appended row.
                const newIndex = pairs.length;
+
                onPairsChange([...pairs, { key: '', value: '' }]);
                await tick();
                keyInputRefs[newIndex]?.focus();
@@ -62,6 +63,7 @@
 
        function trimPairKey(index: number, key: string) {
                const trimmed = key.trim();
+
                if (trimmed === key) return;
 
                const newPairs = [...pairs];
@@ -80,6 +82,7 @@
 
        function trimPairValue(index: number, value: string) {
                const trimmed = value.trim();
+
                if (trimmed === value) return;
 
                const newPairs = [...pairs];
index 2d29672c4d9eeae481244a4eb272ec02ed94624d..488249b884860473f756b62049f0378f9efa4c73 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Input } from '$lib/components/ui/input';
        import { Search, X } from '@lucide/svelte';
+       import { Input } from '$lib/components/ui/input';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
 
        interface Props {
                autofocus?: boolean;
 
        let {
                autofocus,
-               value = $bindable(''),
-               placeholder = 'Search...',
-               onInput,
-               onClose,
-               onKeyDown,
                class: className,
                id,
+               isCancelAlwaysVisible = false,
+               onClose,
+               onInput,
+               onKeyDown,
+               placeholder = 'Search...',
                ref = $bindable(null),
-               isCancelAlwaysVisible = false
+               value = $bindable('')
        }: Props = $props();
 
        let showClearButton = $derived(isCancelAlwaysVisible || !!value || !!onClose);
index d2113ade1583677bcf5a3d1848bd36bb5ca925b6..b3f61c2a9b0756f79de6bbfa9054f644f60921a7 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       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 { HealthCheckStatus } from '$lib/enums';
        import { conversationsStore } from '$lib/stores/conversations.svelte';
        import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { HealthCheckStatus } from '$lib/enums';
-       import { MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants';
-       import McpLogo from './McpLogo.svelte';
 
        interface Props {
                class?: string;
@@ -21,6 +21,7 @@
        let healthyEnabledMcpServers = $derived(
                enabledMcpServersForChat.filter((s) => {
                        const healthState = mcpStore.getHealthCheckState(s.id);
+
                        return healthState.status !== HealthCheckStatus.ERROR;
                })
        );
index d17b24ebb04d906d054affcfede660d9f3e3533f..ae3ad0072a00c3d39d8b4c0e33d1aa58b521a9c9 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { Wrench, Database, MessageSquare, FileText, Sparkles, ListChecks } from '@lucide/svelte';
-       import type { MCPCapabilitiesInfo } from '$lib/types';
+       import { Database, FileText, ListChecks, MessageSquare, Sparkles, Wrench } from '@lucide/svelte';
        import { Badge } from '$lib/components/ui/badge';
+       import type { MCPCapabilitiesInfo } from '$lib/types';
 
        interface Props {
                capabilities?: MCPCapabilitiesInfo;
index 305c9db3aee749c788b2b2e9f7d9b0a2a299bca4..168d11b12c0a4373a539d0ff6cc0025b6eb1caad 100644 (file)
@@ -2,7 +2,7 @@
        import { ChevronDown, ChevronRight } from '@lucide/svelte';
        import * as Collapsible from '$lib/components/ui/collapsible';
        import type { MCPConnectionLog } from '$lib/types';
-       import { formatTime, getMcpLogLevelIcon, getMcpLogLevelClass } from '$lib/utils';
+       import { formatTime, getMcpLogLevelClass, getMcpLogLevelIcon } from '$lib/utils';
 
        interface Props {
                logs: MCPConnectionLog[];
@@ -11,7 +11,7 @@
                class?: string;
        }
 
-       let { logs, connectionTimeMs, defaultExpanded = false, class: className }: Props = $props();
+       let { class: className, connectionTimeMs, defaultExpanded = false, logs }: Props = $props();
 
        let isExpanded = $derived(defaultExpanded);
 
index d2d400bff5677c87ca3de3ed892a7b9775c61a21..eb533d4aeb6310bfe225a17927ce4d7618c389bd 100644 (file)
@@ -1,18 +1,18 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { FileText, Loader2, AlertCircle, Download } from '@lucide/svelte';
+       import { AlertCircle, Download, FileText, Loader2 } from '@lucide/svelte';
+       import { ActionIconCopyToClipboard } from '$lib/components/app';
        import { Button } from '$lib/components/ui/button';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       import { MimeTypeApplication, MimeTypeText } from '$lib/enums';
        import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { MCPResourceContent, MCPResourceInfo } from '$lib/types';
        import {
-               isImageMimeType,
                createBase64DataUrl,
-               getResourceTextContent,
+               downloadResourceContent,
                getResourceBlobContent,
-               downloadResourceContent
+               getResourceTextContent,
+               isImageMimeType
        } from '$lib/utils';
-       import { MimeTypeApplication, MimeTypeText } from '$lib/enums';
-       import { ActionIconCopyToClipboard } from '$lib/components/app';
-       import type { MCPResourceInfo, MCPResourceContent } from '$lib/types';
 
        interface Props {
                resource: MCPResourceInfo | null;
@@ -21,7 +21,7 @@
                class?: string;
        }
 
-       let { resource, preloadedContent, class: className }: Props = $props();
+       let { class: className, preloadedContent, resource }: Props = $props();
 
        let content = $state<MCPResourceContent[] | null>(null);
        let isLoading = $state(false);
@@ -48,6 +48,7 @@
 
                try {
                        const result = await mcpStore.readResource(uri);
+
                        if (result) {
                                content = result;
                        } else {
@@ -62,7 +63,9 @@
 
        function handleDownload() {
                const text = getResourceTextContent(content);
+
                if (!text || !resource) return;
+
                downloadResourceContent(
                        text,
                        resource.mimeType || MimeTypeText.PLAIN,
index f6263251424cbba242c947bea41b7817df86f94d..a2e7f2be79470fe4eb9544495f0f9c391dc7e8dc 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { Button } from '$lib/components/ui/button';
        import { InputWithSuggestions } from '$lib/components/app';
+       import { Button } from '$lib/components/ui/button';
+       import { MIN_AUTOCOMPLETE_INPUT_LENGTH } from '$lib/constants';
        import { KeyboardKey } from '$lib/enums';
        import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { MIN_AUTOCOMPLETE_INPUT_LENGTH } from '$lib/constants';
        import type { MCPResourceTemplateInfo } from '$lib/types';
        import {
                debounce,
-               extractTemplateVariables,
                expandTemplate,
+               extractTemplateVariables,
                isTemplateComplete
        } from '$lib/utils';
 
@@ -18,7 +18,7 @@
                onCancel: () => void;
        }
 
-       let { template, onResolve, onCancel }: Props = $props();
+       let { onCancel, onResolve, template }: Props = $props();
 
        const variables = $derived(extractTemplateVariables(template.uriTemplate));
 
index 24538e8d71b0928b5a85c15f72c19dbbb1b9c8b7..f17199a900955842743a7bf6c3555c88f23a6187 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
+       import McpResourcesBrowserEmptyState from './McpResourcesBrowserEmptyState.svelte';
+       import McpResourcesBrowserHeader from './McpResourcesBrowserHeader.svelte';
+       import McpResourcesBrowserServerItem from './McpResourcesBrowserServerItem.svelte';
        import { mcpStore } from '$lib/stores/mcp.svelte';
        import { mcpResources, mcpResourcesLoading } from '$lib/stores/mcp-resources.svelte';
-       import type { MCPServerResources, MCPResourceInfo, MCPResourceTemplateInfo } from '$lib/types';
-       import { SvelteMap, SvelteSet } from 'svelte/reactivity';
+       import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
        import { parseResourcePath } from '$lib/utils';
-       import McpResourcesBrowserHeader from './McpResourcesBrowserHeader.svelte';
-       import McpResourcesBrowserEmptyState from './McpResourcesBrowserEmptyState.svelte';
-       import McpResourcesBrowserServerItem from './McpResourcesBrowserServerItem.svelte';
+       import { SvelteMap, SvelteSet } from 'svelte/reactivity';
 
        interface Props {
                onSelect?: (resource: MCPResourceInfo, shiftKey?: boolean) => void;
        }
 
        let {
+               class: className,
+               expandToUri,
                onSelect,
-               onToggle,
                onTemplateSelect,
-               selectedUris = new Set(),
+               onToggle,
                selectedTemplateUri,
-               expandToUri,
-               class: className
+               selectedUris = new Set()
        }: Props = $props();
 
        let expandedServers = new SvelteSet<string>();
@@ -51,7 +51,6 @@
                                        serverName.toLowerCase().includes(query)
                                );
                        });
-
                        const filteredTemplates = serverRes.templates.filter((t) => {
                                return (
                                        t.name?.toLowerCase().includes(query) ||
        function autoExpandToResource(uri: string) {
                for (const [serverName, serverRes] of resources.entries()) {
                        const resource = serverRes.resources.find((r) => r.uri === uri);
+
                        if (resource) {
                                expandedServers.add(serverName);
 
                                const pathParts = parseResourcePath(uri);
+
                                if (pathParts.length > 1) {
                                        let currentPath = '';
+
                                        for (let i = 0; i < pathParts.length - 1; i++) {
                                                currentPath = `${currentPath}/${pathParts[i]}`;
                                                const folderId = `${serverName}:${currentPath}`;
+
                                                expandedFolders.add(folderId);
                                        }
                                }
+
                                break;
                        }
                }
index e683bcd424bcbf6a72f1977db1bcfcfc09966dbd..f10566ee69024e217f7b9307a8280bbd8cded64f 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { RefreshCw, Loader2 } from '@lucide/svelte';
-       import { Button } from '$lib/components/ui/button';
+       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';
 
        interface Props {
                isLoading: boolean;
index 00391e9f8d6b7826aa3035c809b3e6c5446a6e97..0c35559eead17308a8913b83268d1297a5c68117 100644 (file)
@@ -1,19 +1,19 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { FolderOpen, ChevronDown, ChevronRight, Loader2, Braces } from '@lucide/svelte';
-       import { Checkbox } from '$lib/components/ui/checkbox';
-       import * as Collapsible from '$lib/components/ui/collapsible';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
-       import { SvelteSet } from 'svelte/reactivity';
        import {
-               type ResourceTreeNode,
                buildResourceTree,
                countTreeResources,
+               type ResourceTreeNode,
                sortTreeChildren
        } from './mcp-resources-browser';
-       import { getDisplayName, getResourceIcon } from '$lib/utils';
+       import { Braces, ChevronDown, ChevronRight, FolderOpen, Loader2 } from '@lucide/svelte';
        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 { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
+       import { getDisplayName, getResourceIcon } from '$lib/utils';
+       import { SvelteSet } from 'svelte/reactivity';
 
        interface Props {
                serverName: string;
        }
 
        let {
-               serverName,
-               serverRes,
-               isExpanded,
-               selectedUris,
-               selectedTemplateUri,
                expandedFolders,
-               onToggleServer,
-               onToggleFolder,
+               isExpanded,
                onSelect,
-               onToggle,
                onTemplateSelect,
-               searchQuery = ''
+               onToggle,
+               onToggleFolder,
+               onToggleServer,
+               searchQuery = '',
+               selectedTemplateUri,
+               selectedUris,
+               serverName,
+               serverRes
        }: Props = $props();
 
        let serverDisplayName = $derived(mcpStore.getServerDisplayName(serverName));
 
        const templateInfos = $derived<MCPResourceTemplateInfo[]>(
                serverRes.templates.map((t) => ({
-                       uriTemplate: t.uriTemplate,
-                       name: t.name,
-                       title: t.title,
+                       annotations: t.annotations,
                        description: t.description,
+                       icons: t.icons,
                        mimeType: t.mimeType,
+                       name: t.name,
                        serverName,
-                       annotations: t.annotations,
-                       icons: t.icons
+                       title: t.title,
+                       uriTemplate: t.uriTemplate
                }))
        );
 
index 804fa7fe2fc541fc0f1b8eb364b147208bfee951..e76af5202e20981c08ccf6db1e5b1e4e84c400e9 100644 (file)
@@ -19,26 +19,30 @@ export function buildResourceTree(
        serverName: string,
        searchQuery?: string
 ): ResourceTreeNode {
-       const root: ResourceTreeNode = { name: 'root', children: new Map() };
+       const root: ResourceTreeNode = { children: new Map(), name: 'root' };
 
        if (!searchQuery || !searchQuery.trim()) {
                for (const resource of resourceList) {
                        const pathParts = parseResourcePath(resource.uri);
+
                        let current = root;
 
                        for (let i = 0; i < pathParts.length - 1; i++) {
                                const part = pathParts[i];
+
                                if (!current.children.has(part)) {
-                                       current.children.set(part, { name: part, children: new Map() });
+                                       current.children.set(part, { children: new Map(), name: part });
                                }
+
                                current = current.children.get(part)!;
                        }
 
                        const fileName = pathParts[pathParts.length - 1] || resource.name;
+
                        current.children.set(resource.uri, {
+                               children: new Map(),
                                name: fileName,
-                               resource: { ...resource, serverName },
-                               children: new Map()
+                               resource: { ...resource, serverName }
                        });
                }
 
@@ -52,23 +56,26 @@ export function buildResourceTree(
                if (!resourceMatchesSearch(resource, query)) continue;
 
                const pathParts = parseResourcePath(resource.uri);
+
                let current = root;
 
                for (let i = 0; i < pathParts.length - 1; i++) {
                        const part = pathParts[i];
+
                        if (!current.children.has(part)) {
-                               current.children.set(part, { name: part, children: new Map(), isFiltered: true });
+                               current.children.set(part, { children: new Map(), isFiltered: true, name: part });
                        }
+
                        current = current.children.get(part)!;
                }
 
                const fileName = pathParts[pathParts.length - 1] || resource.name;
 
                current.children.set(resource.uri, {
-                       name: fileName,
-                       resource: { ...resource, serverName },
                        children: new Map(),
-                       isFiltered: true
+                       isFiltered: true,
+                       name: fileName,
+                       resource: { ...resource, serverName }
                });
        }
 
@@ -76,6 +83,7 @@ export function buildResourceTree(
                if (node.resource) return true;
 
                const toDelete: string[] = [];
+
                for (const [name, child] of node.children.entries()) {
                        if (!cleanupEmptyFolders(child)) {
                                toDelete.push(name);
@@ -96,6 +104,7 @@ export function buildResourceTree(
 
 export function countTreeResources(node: ResourceTreeNode): number {
        if (node.resource) return 1;
+
        let count = 0;
 
        for (const child of node.children.values()) {
@@ -111,6 +120,7 @@ export function sortTreeChildren(children: ResourceTreeNode[]): ResourceTreeNode
                const bIsFolder = !b.resource && b.children.size > 0;
 
                if (aIsFolder && !bIsFolder) return -1;
+
                if (!aIsFolder && bIsFolder) return 1;
 
                return a.name.localeCompare(b.name);
index 5d4c892093d4f9b82d15c6be2847ad566ce8f5dc..3dc1bc7d0cc1332a035e1c1a139cca432e88fca5 100644 (file)
@@ -1,20 +1,20 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { tick } from 'svelte';
-       import * as Card from '$lib/components/ui/card';
-       import { Skeleton } from '$lib/components/ui/skeleton';
-       import type { MCPServerSettingsEntry, HealthCheckState } from '$lib/types';
-       import { HealthCheckStatus } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
        import {
+               McpConnectionLogs,
                McpServerCardActions,
                McpServerCardDeleteDialog,
                McpServerCardEditForm,
                McpServerCardHeader,
                McpServerCardToolsList,
-               McpConnectionLogs,
                McpServerInfo
        } 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 { HealthCheckStatus } from '$lib/enums';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { HealthCheckState, MCPServerSettingsEntry } from '$lib/types';
+       import { tick } from 'svelte';
 
        interface Props {
                server: MCPServerSettingsEntry;
@@ -24,7 +24,7 @@
                onDelete: () => void;
        }
 
-       let { server, enabled, onToggle, onUpdate, onDelete }: Props = $props();
+       let { enabled, onDelete, onToggle, onUpdate, server }: Props = $props();
 
        let healthState = $derived<HealthCheckState>(mcpStore.getHealthCheckState(server.id));
        let displayName = $derived(mcpStore.getServerLabel(server));
 
        function saveEditing(url: string, headers: string, useProxy: boolean, name?: string) {
                onUpdate({
-                       url: url,
                        // undefined = prefill untouched, keep any existing custom name;
                        // empty string = field cleared, back to the automatic label
                        displayName: name === undefined ? server.displayName : name.trim() || undefined,
                        headers: headers || undefined,
+                       url: url,
                        useProxy: useProxy
                });
                isEditing = false;
index 6f137fa21b7b5e1a2543f6a7507e358714bb6140..fbfc8beda1c6c3c6665fb7a157e2cb60741bcb58 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { Trash2, RefreshCw, Pencil } from '@lucide/svelte';
+       import { Pencil, RefreshCw, Trash2 } from '@lucide/svelte';
        import { Button } from '$lib/components/ui/button';
 
        interface Props {
@@ -9,7 +9,7 @@
                onDelete: () => void;
        }
 
-       let { isHealthChecking, onEdit, onRefresh, onDelete }: Props = $props();
+       let { isHealthChecking, onDelete, onEdit, onRefresh }: Props = $props();
 </script>
 
 <div class="flex shrink-0 items-center gap-1">
index e70d63540b07883e290756f8dda07d6fcdd76eba..5157c5779bb0c3ed328b62150ba666132a78b002 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import * as Card from '$lib/components/ui/card';
-       import { mode } from 'mode-watcher';
        import type { RecommendedMCPServer } from '$lib/types';
+       import { mode } from 'mode-watcher';
 
        interface Props {
                server: RecommendedMCPServer;
                dimmed?: boolean;
        }
 
-       let { server, onClick, selected = false, dimmed = false }: Props = $props();
+       let { dimmed = false, onClick, selected = false, server }: Props = $props();
 
        let activeIconUrl = $derived.by(() => {
                const isDark = mode.current === 'dark';
 
                if (isDark && server.iconUrlDark) return server.iconUrlDark;
+
                if (!isDark && server.iconUrlLight) return server.iconUrlLight;
 
                return server.iconUrl;
index 8f650148a2f828e9348eee44b1c9127785b11223..0b3d3d00e2aac56f4b5c58f600b58c873ae9e237 100644 (file)
@@ -8,7 +8,7 @@
                onConfirm: () => void;
        }
 
-       let { open = $bindable(), displayName, onOpenChange, onConfirm }: Props = $props();
+       let { displayName, onConfirm, onOpenChange, open = $bindable() }: Props = $props();
 </script>
 
 <AlertDialog.Root bind:open {onOpenChange}>
index 19778f95b006dd448db8682d82f9aae2076e2c25..8705d365175937a6c1337d4cfde5f7c4f0fe3586 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { Button } from '$lib/components/ui/button';
        import { McpServerForm } from '$lib/components/app/mcp';
+       import { Button } from '$lib/components/ui/button';
        import { parseHeadersToArray } from '$lib/utils';
 
        interface Props {
        }
 
        let {
+               onCancel,
+               onSave,
                serverId,
-               serverUrl,
-               serverUseProxy = false,
                serverLabel = '',
-               onSave,
-               onCancel
+               serverUrl,
+               serverUseProxy = false
        }: Props = $props();
 
        let editUrl = $derived(serverUrl);
 
        let urlError = $derived.by(() => {
                if (!editUrl.trim()) return 'URL is required';
+
                try {
                        new URL(editUrl);
+
                        return null;
                } catch {
                        return 'Invalid URL format';
index 5544bcec42199bed4f013b598ac60313ee7b1c92..4e27243f5cb4d8db6bea5d45eb7e04486cac1686 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Switch } from '$lib/components/ui/switch';
-       import { Badge } from '$lib/components/ui/badge';
        import { McpCapabilitiesBadges, McpServerIdentity } from '$lib/components/app/mcp';
-       import { MCP_TRANSPORT_LABELS, MCP_TRANSPORT_ICONS } from '$lib/constants';
+       import { Badge } from '$lib/components/ui/badge';
+       import { Switch } from '$lib/components/ui/switch';
+       import { MCP_TRANSPORT_ICONS, MCP_TRANSPORT_LABELS } from '$lib/constants';
        import { MCPTransportType } from '$lib/enums';
-       import type { MCPServerInfo, MCPCapabilitiesInfo } from '$lib/types';
+       import type { MCPCapabilitiesInfo, MCPServerInfo } from '$lib/types';
 
        interface Props {
                displayName: string;
        }
 
        let {
+               capabilities,
+               disabled = false,
                displayName,
-               faviconUrl,
                enabled,
-               disabled = false,
+               faviconUrl,
                onToggle,
                serverInfo,
-               capabilities,
                transportType
        }: Props = $props();
 </script>
index d0397c17a980dad230c741cb21aa3dfed14dfebc..e4882bcb79a75f043c89fe7fc045cec7d5d55ed8 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { ChevronDown, ChevronRight } from '@lucide/svelte';
-       import * as Collapsible from '$lib/components/ui/collapsible';
        import { Badge } from '$lib/components/ui/badge';
+       import * as Collapsible from '$lib/components/ui/collapsible';
 
        interface Tool {
                name: string;
index 2b8e1226bab81f0e01e7bf4262616c40ee7f0f45..528257e01d859bf00fa68e8a3bf3fae3d82865c1 100644 (file)
@@ -1,10 +1,7 @@
 <script lang="ts">
+       import { KeyValuePairs } from '$lib/components/app';
        import { Input } from '$lib/components/ui/input';
        import { Switch } from '$lib/components/ui/switch';
-       import { KeyValuePairs } from '$lib/components/app';
-       import type { KeyValuePair } from '$lib/types';
-       import { parseHeadersToArray, serializeHeaders } from '$lib/utils';
-       import { UrlProtocol } from '$lib/enums';
        import {
                AUTHORIZATION_HEADER,
                BEARER_PREFIX,
@@ -12,7 +9,10 @@
                MCP_SERVER_URL_PLACEHOLDER,
                REDACTED_HEADERS
        } from '$lib/constants';
+       import { UrlProtocol } from '$lib/enums';
        import { mcpStore } from '$lib/stores/mcp.svelte';
+       import type { KeyValuePair } from '$lib/types';
+       import { parseHeadersToArray, serializeHeaders } from '$lib/utils';
 
        interface Props {
                url: string;
        }
 
        let {
-               url,
                headers,
+               id = 'server',
                name = '',
-               onNameChange,
                namePlaceholder = 'Name reported by the server',
-               useProxy = false,
-               onUrlChange,
                onHeadersChange,
+               onNameChange,
+               onUrlChange,
                onUseProxyChange,
+               required = false,
+               url,
                urlError = null,
-               id = 'server',
-               wantsAuthorization = $bindable(false),
-               required = false
+               useProxy = false,
+               wantsAuthorization = $bindable(false)
        }: Props = $props();
 
        let isWebSocket = $derived(
@@ -99,7 +99,9 @@
 
        let bearerToken = $derived.by(() => {
                const auth = headerPairs.find(ownedByBearerUi);
+
                if (!auth) return '';
+
                return auth.value.trim().slice(BEARER_PREFIX.length).trim();
        });
 
        // behavior would otherwise pick one arbitrarily, so we strip first.
        function updateBearerToken(token: string) {
                const filtered = headerPairs.filter((p) => !matchesAuthorizationKey(p.key));
-
                const trimmed = token.trim();
 
                if (trimmed) {
                        // Only drop the entry this UI owns; a non-Bearer Authorization row
                        // authored in the KV section must survive a toggle off untouched.
                        const filtered = headerPairs.filter((p) => !ownedByBearerUi(p));
+
                        updateHeaderPairs(filtered);
                }
        }
                pairs={headerPairs.filter((p) => !ownedByBearerUi(p))}
                onPairsChange={(pairs) => {
                        const auth = headerPairs.find(ownedByBearerUi);
+
                        updateHeaderPairs(auth ? [...pairs, auth] : pairs);
                }}
                keyPlaceholder="Header name"
index 3f128e02c967aec3dde0de76b2de1eeb7e6c70e6..c87ab92030b52d171107929577ada73d71f180ad 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
        import { ExternalLink } from '@lucide/svelte';
-       import { Badge } from '$lib/components/ui/badge';
        import { McpLogo } from '$lib/components/app/mcp';
        import { TruncatedText } from '$lib/components/app/misc';
-       import { sanitizeExternalUrl } from '$lib/utils';
+       import { Badge } from '$lib/components/ui/badge';
        import type { MCPServerInfo } from '$lib/types';
+       import { sanitizeExternalUrl } from '$lib/utils';
 
        interface Props {
                displayName?: string;
        let {
                displayName,
                faviconUrl = null,
-               serverInfo,
                iconClass = 'h-5 w-5',
                iconRounded = 'rounded-sm',
+               nameClass,
+               serverInfo,
                showVersion = true,
-               showWebsite = true,
-               nameClass
+               showWebsite = true
        }: Props = $props();
 
        let safeWebsiteUrl = $derived(
index aecae6e57b0a0a7d264e63dfe44c8b235eb82920..fe0a45532ee060a5a81ef2e100b57261a2da01b0 100644 (file)
@@ -7,7 +7,7 @@
                class?: string;
        }
 
-       let { instructions, class: className }: Props = $props();
+       let { class: className, instructions }: Props = $props();
 
        let isExpanded = $state(false);
 </script>
index fa12d1c6249061d5ae569e70b82f0d4ed5d9d8ba..cd421f022bb27f36437388329f9cfa4bb7b79355 100644 (file)
@@ -10,7 +10,7 @@
                onPreview?: (code: string, language: string) => void;
        }
 
-       let { code, language, disabled = false, onPreview }: Props = $props();
+       let { code, disabled = false, language, onPreview }: Props = $props();
 
        const showPreview = $derived(language?.toLowerCase() === FileTypeText.HTML);
 </script>
index b6052b4a97bf182dfdf732ea500511c502715245..a3096ffb332aebcd63390f7de4ecd71b183e6bd1 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
+       import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
        import { Button } from '$lib/components/ui/button';
        import { Checkbox } from '$lib/components/ui/checkbox';
-       import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
        import { ScrollArea } from '$lib/components/ui/scroll-area';
-       import { SvelteSet } from 'svelte/reactivity';
        import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
+       import { SvelteSet } from 'svelte/reactivity';
 
        interface Props {
                conversations: DatabaseConversation[];
 
        let {
                conversations,
+               isOpen = true,
                messageCountMap = new Map(),
                mode,
                onCancel,
-               onConfirm,
-               isOpen = true
+               onConfirm
        }: Props = $props();
 
        let searchQuery = $state('');
@@ -34,6 +34,7 @@
        let filteredConversations = $derived(
                conversations.filter((conv) => {
                        const name = conv.name || 'Untitled conversation';
+
                        return name.toLowerCase().includes(searchQuery.toLowerCase());
                })
        );
        );
 
        const marquee = useMarqueeSelection({
-               selectedIds: () => selectedIds,
+               enabled: () => isOpen,
                orderedIds: () => orderedIds,
-               enabled: () => isOpen
+               selectedIds: () => selectedIds
        });
 
        function toggleAll() {
                const newSet = new SvelteSet(selectedIds);
+
                if (allSelected) {
                        filteredConversations.forEach((conv) => newSet.delete(conv.id));
                } else {
                        filteredConversations.forEach((conv) => newSet.add(conv.id));
                }
+
                selectedIds = newSet;
        }
 
        function handleConfirm() {
                const selected = conversations.filter((conv) => selectedIds.has(conv.id));
+
                onConfirm(selected);
        }
 
index d5665901a1a4d9a3ac5493ae35e5092744519a24..68cd1e0450e22212f89ee642e65e8c22fbf68992 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { ChevronLeft, ChevronRight } from '@lucide/svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import type { Snippet } from 'svelte';
 
        interface Props {
@@ -10,7 +10,7 @@
                onScrollableChange?: (isScrollable: boolean) => void;
        }
 
-       let { class: className = '', children, gapSize = '3', onScrollableChange }: Props = $props();
+       let { children, class: className = '', gapSize = '3', onScrollableChange }: Props = $props();
 
        let canScrollLeft = $state(false);
        let canScrollRight = $state(false);
@@ -22,7 +22,7 @@
 
                if (!scrollContainer) return;
 
-               scrollContainer.scrollBy({ left: scrollContainer.clientWidth * -0.67, behavior: 'smooth' });
+               scrollContainer.scrollBy({ behavior: 'smooth', left: scrollContainer.clientWidth * -0.67 });
        }
 
        function scrollRight(event?: MouseEvent) {
 
                if (!scrollContainer) return;
 
-               scrollContainer.scrollBy({ left: scrollContainer.clientWidth * 0.67, behavior: 'smooth' });
+               scrollContainer.scrollBy({ behavior: 'smooth', left: scrollContainer.clientWidth * 0.67 });
        }
 
        function updateScrollButtons() {
                if (!scrollContainer) return;
 
-               const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
+               const { clientWidth, scrollLeft, scrollWidth } = scrollContainer;
 
                canScrollLeft = scrollLeft > 0;
                canScrollRight = scrollLeft < scrollWidth - clientWidth - 1;
 
                const isScrollable = scrollWidth > clientWidth;
+
                onScrollableChange?.(isScrollable);
        }
 
@@ -59,6 +60,7 @@
                if (!scrollContainer) return;
 
                const observer = new ResizeObserver(() => updateScrollButtons());
+
                observer.observe(scrollContainer);
 
                return () => observer.disconnect();
index da55abda0234fa6ece0e600782e4dab962e5b17a..35d38d246e8632be1fc254c4b78f24943c51e9c0 100644 (file)
@@ -7,7 +7,7 @@
                class?: string;
        }
 
-       let { keys, variant = 'default', class: className = '' }: Props = $props();
+       let { class: className = '', keys, variant = 'default' }: Props = $props();
 
        let baseClasses =
                'px-1 pointer-events-none inline-flex select-none items-center gap-0.5 font-sans text-md font-medium opacity-0 transition-opacity -my-1';
index a6b7cb483e5be6a195c85df06fcf0dd6c31fd981..8e621d30108818c8779d2d012daaa40cb461ced8 100644 (file)
@@ -7,7 +7,7 @@
                showTooltip?: boolean;
        }
 
-       let { text, class: className = '', showTooltip = true }: Props = $props();
+       let { class: className = '', showTooltip = true, text }: Props = $props();
 
        let textElement: HTMLSpanElement | undefined = $state();
        let isTruncated = $state(false);
@@ -23,6 +23,7 @@
                        checkTruncation();
 
                        const observer = new ResizeObserver(checkTruncation);
+
                        observer.observe(textElement);
 
                        return () => observer.disconnect();
index b840687d4ef059b0c25abc96ebae32c23f2ccd79..969c21d5a4daf8a62d024d32d41c7104d08a37f4 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Package } from '@lucide/svelte';
-       import { BadgeInfo, ActionIconCopyToClipboard } from '$lib/components/app';
        import ModelId from './ModelId.svelte';
+       import { Package } from '@lucide/svelte';
+       import { ActionIconCopyToClipboard, BadgeInfo } from '$lib/components/app';
+       import * as Tooltip from '$lib/components/ui/tooltip';
        import { modelsStore } from '$lib/stores/models.svelte';
        import { serverStore } from '$lib/stores/server.svelte';
-       import * as Tooltip from '$lib/components/ui/tooltip';
 
        interface Props {
                class?: string;
index f566b55ee88b6db7941501fba336adefbfee1679..042d72106131c9052d6dbfa9f841e186144859be 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
+       import { TruncatedText } from '$lib/components/app';
        import { ModelsService } from '$lib/services/models.service';
        import { config } from '$lib/stores/settings.svelte';
-       import { TruncatedText } from '$lib/components/app';
 
        interface Props {
                modelId: string;
        }
 
        let {
-               modelId,
+               aliases,
+               class: className = '',
                hideOrgName = false,
-               showRaw = undefined,
                hideQuantization,
                hideTags,
-               aliases,
+               modelId,
+               showRaw = undefined,
                tags,
-               class: className = '',
                ...rest
        }: Props = $props();
 
index 415e5164d820135744ca8d87988e92fb6939c748..f1dfd60c154bc4cab9ac654f293105703f08b8e0 100644 (file)
@@ -1,12 +1,7 @@
 <script lang="ts">
+       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
+       import type { ModelItem } from './utils';
        import { ChevronDown, Loader2 } from '@lucide/svelte';
-       import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
-       import * as Tooltip from '$lib/components/ui/tooltip';
-       import { KeyboardKey, ServerModelStatus } from '$lib/enums';
-       import { MODEL_SELECTOR_ICON } from '$lib/constants';
-       import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
-       import { modelsStore, routerModels } from '$lib/stores/models.svelte';
-       import { modelLoadFraction } from '$lib/utils';
        import {
                DialogModelInformation,
                DropdownMenuSearchable,
@@ -14,8 +9,13 @@
                ModelsSelectorList,
                ModelsSelectorOption
        } from '$lib/components/app';
-       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
-       import type { ModelItem } from './utils';
+       import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
+       import * as Tooltip from '$lib/components/ui/tooltip';
+       import { MODEL_SELECTOR_ICON } from '$lib/constants';
+       import { KeyboardKey, ServerModelStatus } from '$lib/enums';
+       import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
+       import { modelsStore, routerModels } from '$lib/stores/models.svelte';
+       import { modelLoadFraction } from '$lib/utils';
 
        interface Props {
                class?: string;
 
        const ms = useModelsSelector({
                currentModel: () => currentModel,
-               useGlobalSelection: () => useGlobalSelection,
                onModelChange: () => onModelChange,
                onOpenChange: (open) => {
                        isOpen = open;
                        highlightedId = null;
-               }
+               },
+               useGlobalSelection: () => useGlobalSelection
        });
 
        $effect(() => {
 
        function moveHighlight(direction: 1 | -1) {
                const len = visualOrder.length;
+
                if (len === 0) {
                        highlightedId = null;
+
                        return;
                }
 
                let index = highlightedIndex;
+
                if (index === -1) {
                        index = direction === 1 ? 0 : len - 1;
                } else {
        async function handleModelKeyAction(modelId: string, unload: boolean) {
                if (!unload) {
                        void ms.handleSelect(modelId);
+
                        return;
                }
 
index 61a4cf0f662ece97c6af44bb197ff0f5936f58a9..c7ce50d29a482fd59a96dcbc154fce16e6998046 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import { modelsStore } from '$lib/stores/models.svelte';
-       import { ModelsSelectorOption } from '$lib/components/app';
        import type { GroupedModelOptions, ModelItem } from './utils';
+       import { ModelsSelectorOption } from '$lib/components/app';
+       import { modelsStore } from '$lib/stores/models.svelte';
 
        interface Props {
                groups: GroupedModelOptions;
        }
 
        let {
-               groups,
-               currentModel,
                activeId,
-               sectionHeaderClass = 'my-1 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none',
-               orgHeaderClass = 'px-2 py-2 text-[11px] font-semibold text-muted-foreground/50 select-none [&:not(:first-child)]:mt-1',
-               onSelect,
+               currentModel,
+               groups,
                onInfoClick,
-               renderOption
+               onSelect,
+               orgHeaderClass = 'px-2 py-2 text-[11px] font-semibold text-muted-foreground/50 select-none [&:not(:first-child)]:mt-1',
+               renderOption,
+               sectionHeaderClass = 'my-1 px-2 py-2 text-[13px] font-semibold text-muted-foreground/70 select-none'
        }: Props = $props();
        let render = $derived(renderOption ?? defaultOption);
 </script>
index a1b00e03e8875c5314d322481b1b494e3ca77fef..a7ce0310e2efe0e3333c6d43a96056a3ae2307bd 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
        import {
                CircleAlert,
                Heart,
                RotateCw
        } from '@lucide/svelte';
        import { ActionIcon, ModelId } from '$lib/components/app';
-       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
-       import type { ModelOption } from '$lib/types/models';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { ServerModelStatus } from '$lib/enums';
        import { modelsStore, routerModels } from '$lib/stores/models.svelte';
+       import type { ModelOption } from '$lib/types/models';
        import { modelLoadFraction, modelLoadProgressText } from '$lib/utils';
 
        interface Props {
        }
 
        let {
-               option,
-               isSelected,
-               isHighlighted,
-               isFav,
                hideOrgName = false,
-               onSelect,
-               onMouseEnter,
+               isFav,
+               isHighlighted,
+               isSelected,
+               onInfoClick,
                onKeyDown,
-               onInfoClick
+               onMouseEnter,
+               onSelect,
+               option
        }: Props = $props();
 
        let currentRouterModels = $derived(routerModels());
        let serverStatus = $derived.by(() => {
                const model = currentRouterModels.find((m) => m.id === option.model);
+
                return (model?.status?.value as ServerModelStatus) ?? null;
        });
        let isOperationInProgress = $derived(modelsStore.isModelOperationInProgress(option.model));
index a9e9ea1c8d5d52c8ff032b11b859675dbf89d5d4..db08313b2928372f4a9b2a17dda24f43aab8e20c 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
+       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
        import { ChevronDown, Loader2, Package } from '@lucide/svelte';
-       import * as Sheet from '$lib/components/ui/sheet';
-       import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
        import {
                DialogModelInformation,
                ModelId,
                ModelsSelectorList,
                SearchInput
        } from '$lib/components/app';
-       import ModelLoadHighlight from './ModelLoadHighlight.svelte';
+       import * as Sheet from '$lib/components/ui/sheet';
        import { ServerModelStatus } from '$lib/enums';
+       import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
        import { modelsStore, routerModels } from '$lib/stores/models.svelte';
        import { modelLoadFraction } from '$lib/utils';
 
@@ -27,9 +27,9 @@
        let {
                class: className = '',
                currentModel = null,
-               onModelChange,
                disabled = false,
                forceForegroundText = false,
+               onModelChange,
                useGlobalSelection = false
        }: Props = $props();
 
 
        const ms = useModelsSelector({
                currentModel: () => currentModel,
-               useGlobalSelection: () => useGlobalSelection,
                onModelChange: () => onModelChange,
                onOpenChange: (open) => {
                        sheetOpen = open;
-               }
+               },
+               useGlobalSelection: () => useGlobalSelection
        });
 
        export function open() {
index ae1f511e9f667c2ee09b666cd3574c5118126978..b78e7085b707fe9720f1a71b66ec6c188b12913c 100644 (file)
@@ -1,5 +1,5 @@
-import { SvelteMap } from 'svelte/reactivity';
 import type { ModelOption } from '$lib/types/models';
+import { SvelteMap } from 'svelte/reactivity';
 
 export interface ModelItem {
        option: ModelOption;
@@ -19,6 +19,7 @@ export interface GroupedModelOptions {
 
 export function filterModelOptions(options: ModelOption[], searchTerm: string): ModelOption[] {
        const term = searchTerm.trim().toLowerCase();
+
        if (!term) return options;
 
        return options.filter(
@@ -37,39 +38,45 @@ export function groupModelOptions(
 ): GroupedModelOptions {
        // Loaded models
        const loaded: ModelItem[] = [];
+
        for (let i = 0; i < filteredOptions.length; i++) {
                if (isModelLoaded(filteredOptions[i].model)) {
-                       loaded.push({ option: filteredOptions[i], flatIndex: i });
+                       loaded.push({ flatIndex: i, option: filteredOptions[i] });
                }
        }
 
        // Favorites (excluding loaded)
        const loadedModelIds = new Set(loaded.map((item) => item.option.model));
        const favorites: ModelItem[] = [];
+
        for (let i = 0; i < filteredOptions.length; i++) {
                if (
                        favoriteIds.has(filteredOptions[i].model) &&
                        !loadedModelIds.has(filteredOptions[i].model)
                ) {
-                       favorites.push({ option: filteredOptions[i], flatIndex: i });
+                       favorites.push({ flatIndex: i, option: filteredOptions[i] });
                }
        }
 
        // Available models grouped by org (excluding loaded and favorites)
        const available: OrgGroup[] = [];
        const orgGroups = new SvelteMap<string, ModelItem[]>();
+
        for (let i = 0; i < filteredOptions.length; i++) {
                const option = filteredOptions[i];
+
                if (loadedModelIds.has(option.model) || favoriteIds.has(option.model)) continue;
 
                const key = option.parsedId?.orgName ?? '';
+
                if (!orgGroups.has(key)) orgGroups.set(key, []);
-               orgGroups.get(key)!.push({ option, flatIndex: i });
+
+               orgGroups.get(key)!.push({ flatIndex: i, option });
        }
 
        for (const [orgName, items] of orgGroups) {
-               available.push({ orgName: orgName || null, items });
+               available.push({ items, orgName: orgName || null });
        }
 
-       return { loaded, favorites, available };
+       return { available, favorites, loaded };
 }
index 951831149fc9e1c087032b7d6eba07781c9b7962..1bf41b69a7c3897961a4c8846cdbbd5a05c70d39 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
+       import { KeyboardShortcutInfo } from '$lib/components/app';
        import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
        import * as Tooltip from '$lib/components/ui/tooltip';
-       import { KeyboardShortcutInfo } from '$lib/components/app';
        import type { Component } from 'svelte';
 
        interface ActionItem {
        }
 
        let {
-               triggerIcon,
-               triggerTooltip,
-               triggerClass = '',
                actions,
                align = 'end',
-               open = $bindable(false)
+               open = $bindable(false),
+               triggerClass = '',
+               triggerIcon,
+               triggerTooltip
        }: Props = $props();
 </script>
 
index 3bd68d3bd6e4c30b3166e8e86214b91484ab3c34..0e036c9a7af52b3c597e208a909e4a8c65f3f273 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import type { Snippet } from 'svelte';
-       import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
        import { SearchInput } from '$lib/components/app';
+       import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
+       import type { Snippet } from 'svelte';
 
        interface Props {
                placeholder?: string;
        }
 
        let {
-               placeholder = 'Search...',
-               searchValue = $bindable(''),
-               onSearchChange,
-               onSearchKeyDown,
+               children,
                emptyMessage = 'No items found',
+               footer,
                isEmpty = false,
-               children,
-               footer
+               onSearchChange,
+               onSearchKeyDown,
+               placeholder = 'Search...',
+               searchValue = $bindable('')
        }: Props = $props();
 </script>
 
index b5e4beeffd92016bad4179f90acd46df7cec437f..dfd3d9077a59ac102e1cc701459ac2a131ad8f6d 100644 (file)
@@ -1,32 +1,31 @@
 <script lang="ts">
+       import { PanelLeftClose, PanelLeftOpen, X } from '@lucide/svelte';
        import { goto } from '$app/navigation';
        import { page } from '$app/state';
-       import { PanelLeftClose, PanelLeftOpen, X } from '@lucide/svelte';
        import {
                ActionIcon,
                DialogConversationRename,
                Logo,
-               SidebarNavigationConversationList,
-               SidebarNavigationActions
+               SidebarNavigationActions,
+               SidebarNavigationConversationList
        } from '$lib/components/app';
        import { ROUTES } from '$lib/constants';
-       import { fade } from 'svelte/transition';
-       import { SvelteSet } from 'svelte/reactivity';
-       import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
-
+       import { TooltipSide } from '$lib/enums';
        import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
+       import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
+       import { RouterService } from '$lib/services/router.service';
+       import { chatStore } from '$lib/stores/chat.svelte';
        import {
                buildConversationTree,
-               conversationsStore,
-               conversations
+               conversations,
+               conversationsStore
        } from '$lib/stores/conversations.svelte';
-       import { chatStore } from '$lib/stores/chat.svelte';
+       import { device } from '$lib/stores/device.svelte';
        import { config } from '$lib/stores/settings.svelte';
-       import { RouterService } from '$lib/services/router.service';
        import { isMobile } from '$lib/stores/viewport.svelte';
-       import { TooltipSide } from '$lib/enums';
-       import { device } from '$lib/stores/device.svelte';
        import { circIn } from 'svelte/easing';
+       import { SvelteSet } from 'svelte/reactivity';
+       import { fade } from 'svelte/transition';
 
        interface Props {
                onSearchClick?: () => void;
@@ -55,6 +54,7 @@
 
        function toggleExpandedMode() {
                isExpandedMode = !isExpandedMode;
+
                if (!isExpandedMode) {
                        hoveredTooltip = null;
                }
@@ -64,7 +64,9 @@
                if (!isExpandedMode) {
                        isSearchModeActive = false;
                        searchQuery = '';
+
                        if (isSelectionMode) exitSelectionMode();
+
                        cancelMobileCollapse();
                }
        });
 
        const allSelectedArePinned = $derived.by(() => {
                if (selectedIds.size === 0) return false;
+
                const convs = conversations();
+
                for (const id of selectedIds) {
                        const c = convs.find((conv) => conv.id === id);
+
                        if (c && !c.pinned) return false;
                }
+
                return true;
        });
 
        const pinStateIsMixed = $derived.by(() => {
                if (selectedIds.size === 0) return false;
+
                const convs = conversations();
+
                let anyPinned = false;
                let anyUnpinned = false;
+
                for (const id of selectedIds) {
                        const c = convs.find((conv) => conv.id === id);
+
                        if (!c) continue;
+
                        if (c.pinned) anyPinned = true;
                        else anyUnpinned = true;
+
                        if (anyPinned && anyUnpinned) return true;
                }
+
                return false;
        });
 
        const visibleSelectionStats = $derived.by(() => {
                const visibleIds = filteredConversations.map((c) => c.id);
+
                let selectedVisible = 0;
+
                for (const id of visibleIds) {
                        if (selectedIds.has(id)) selectedVisible++;
                }
+
                return {
-                       visibleCount: visibleIds.length,
-                       selectedVisibleCount: selectedVisible
+                       selectedVisibleCount: selectedVisible,
+                       visibleCount: visibleIds.length
                };
        });
 
        function enterSelectionMode(id?: string) {
                isSelectionMode = true;
+
                if (id !== undefined) {
                        selectedIds.add(id);
                }
 
        async function handleBulkDelete() {
                const ids = Array.from(selectedIds);
+
                if (ids.length === 0) return;
+
                await conversationsStore.bulkDeleteConversations(ids);
                exitSelectionMode();
        }
 
        async function handleBulkPinToggle() {
                const ids = Array.from(selectedIds);
+
                if (ids.length === 0) return;
+
                await conversationsStore.bulkToggleConversationPin(ids);
        }
 
        async function handleBulkExport() {
                const ids = Array.from(selectedIds);
+
                if (ids.length === 0) return;
+
                await conversationsStore.bulkExportConversations(ids);
        }
 
        const marquee = useMarqueeSelection({
-               selectedIds: () => selectedIds,
+               enabled: () => isSelectionMode,
                orderedIds: () => renderedOrderIds,
-               enabled: () => isSelectionMode
+               selectedIds: () => selectedIds
        });
 
        function handleRowMouseDown(id: string, event: MouseEvent) {
                if (!isSelectionMode) return;
+
                marquee.rowMouseDown(id, event);
        }
 
        function handleSelectionClick(id: string, options: { shiftKey: boolean }): void {
                if (!isSelectionMode) return;
+
                marquee.rowClick(id, options.shiftKey);
        }
 
                if (isMobile.current) {
                        scheduleMobileCollapse();
                }
+
                await goto(RouterService.chat(id));
        }
 
        async function handleEditConversation(id: string) {
                const conversation = conversations().find((conv) => conv.id === id);
+
                if (!conversation) return;
 
                renameTargetConversationId = id;
 
        async function handleRenameConfirm() {
                const id = renameTargetConversationId;
+
                if (!id) return;
 
                const nextName = renameDraft.trim();
+
                if (!nextName || nextName === renameOriginalTitle.trim()) return;
 
                await conversationsStore.updateConversationName(id, nextName);
 
        async function handleDeleteConversation(id: string) {
                const conversation = conversations().find((conv) => conv.id === id);
+
                if (!conversation) return;
 
                const confirmed = window.confirm(
                        `Delete "${conversation.name}"? This action cannot be undone.`
                );
+
                if (!confirmed) return;
 
                await conversationsStore.deleteConversation(id, { deleteWithForks: false });
                if (pendingCollapse) {
                        clearTimeout(pendingCollapse);
                }
+
                pendingCollapse = setTimeout(() => {
                        isExpandedMode = false;
                        pendingCollapse = null;
                                        !isExpandedMode
                                                ? 'opacity-0 h-0!'
                                                : ''}"
-                                       in:fade={{ duration: 150, easing: circIn, delay: 50 }}
+                                       in:fade={{ delay: 50, duration: 150, easing: circIn }}
                                        out:fade={{ duration: 100 }}
                                >
                                        <ActionIcon
index 5cb805ce888dfcf45f4975c907a900c38d902d26..0aa7e7aef64648376d3043fd8a52c640484b0e97 100644 (file)
@@ -1,22 +1,22 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
+       import { Search } from '@lucide/svelte';
        import { goto } from '$app/navigation';
        import { page } from '$app/state';
-       import { Search } from '@lucide/svelte';
        import { ActionIcon, KeyboardShortcutInfo, SearchInput } from '$lib/components/app';
        import { Button } from '$lib/components/ui/button';
        import {
-               ICON_STRIP_TRANSITION_DURATION,
                ICON_STRIP_TRANSITION_DELAY_MULTIPLIER,
+               ICON_STRIP_TRANSITION_DURATION,
                ROUTES,
                SIDEBAR_ACTIONS_ITEMS
        } from '$lib/constants';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { TooltipSide } from '$lib/enums';
-       import { fade } from 'svelte/transition';
-       import { circIn } from 'svelte/easing';
-       import { onMount } from 'svelte';
+       import { isMobile } from '$lib/stores/viewport.svelte';
        import type { Component } from 'svelte';
+       import { onMount } from 'svelte';
+       import { circIn } from 'svelte/easing';
+       import { fade } from 'svelte/transition';
 
        interface Props {
                class: string;
                class: className,
                isExpandedMode = false,
                isSearchModeActive = $bindable(false),
-               searchQuery = $bindable(''),
-               onSearchDeactivated,
+               onNewChat,
                onSearchClick,
-               onNewChat
+               onSearchDeactivated,
+               searchQuery = $bindable('')
        }: Props = $props();
 
        let initialized = $state(false);
                                        ? undefined
                                        : onSearchClick}
                        {@const itemTransition = {
-                               duration: ICON_STRIP_TRANSITION_DURATION,
                                delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
+                               duration: ICON_STRIP_TRANSITION_DURATION,
                                easing: circIn
                        }}
 
                                        ? undefined
                                        : onSearchClick}
                        {@const itemTransition = {
-                               duration: ICON_STRIP_TRANSITION_DURATION,
                                delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0,
+                               duration: ICON_STRIP_TRANSITION_DURATION,
                                easing: circIn
                        }}
 
index 7204d7fec293566e34b08912cc452de84f81b29d..9a9fb8f52e239c697908b0ac07bcdc77c2c66817 100644 (file)
@@ -1,25 +1,25 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import {
-               Trash2,
-               Pencil,
-               MoreHorizontal,
                Download,
-               Loader2,
-               Square,
                GitBranch,
+               ListChecks,
+               Loader2,
+               MoreHorizontal,
+               Pencil,
                Pin,
                PinOff,
-               ListChecks
+               Square,
+               Trash2
        } from '@lucide/svelte';
        import { DropdownMenuActions } from '$lib/components/app';
-       import * as Tooltip from '$lib/components/ui/tooltip';
+       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 { RouterService } from '$lib/services/router.service';
        import { getAllLoadingChats } from '$lib/stores/chat.svelte';
        import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { TruncatedText } from '$lib/components/app';
        import { onMount } from 'svelte';
 
        interface Props {
 
        let {
                conversation,
+               depth = 0,
+               isActive = false,
+               isSelected = false,
+               isSelectionMode = false,
                onDelete,
                onEdit,
-               onSelect,
-               onStop,
-               onToggleSelect,
                onEnterSelectionMode,
-               onSelectionClick,
                onRowMouseDown,
-               isActive = false,
-               isSelectionMode = false,
-               isSelected = false,
-               depth = 0
+               onSelect,
+               onSelectionClick,
+               onStop,
+               onToggleSelect
        }: Props = $props();
 
        let renderActionsDropdown = $state(false);
@@ -99,6 +99,7 @@
 
        function handleMouseOver() {
                if (isSelectionMode) return;
+
                renderActionsDropdown = true;
        }
 
 
        function handleCheckboxClick(event: MouseEvent) {
                event.stopPropagation();
+
                if (isSelectionMode) {
                        onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
                } else {
 
        function handleCheckboxKeydown(event: KeyboardEvent) {
                if (event.key !== ' ' && event.key !== 'Enter') return;
+
                event.stopPropagation();
                event.preventDefault();
+
                if (isSelectionMode) {
                        onSelectionClick?.(conversation.id, { shiftKey: event.shiftKey });
                } else {
                                                icon: Trash2,
                                                label: 'Delete',
                                                onclick: handleDelete,
-                                               variant: 'destructive',
+                                               separator: true,
                                                shortcut: ['shift', 'cmd', 'd'],
-                                               separator: true
+                                               variant: 'destructive'
                                        }
                                ]}
                        />
index 1ea955319a0ca64e37473998241b65fb6a6b8810..fead08c1326d799100ef906e824249693f38b4e2 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { Pin } from '@lucide/svelte';
-       import { buildConversationTree } from '$lib/stores/conversations.svelte';
        import SidebarNavigationConversationItem from './SidebarNavigationConversationItem.svelte';
        import SidebarNavigationSearchResults from './SidebarNavigationSearchResults.svelte';
        import SidebarNavigationSelectionBar from './SidebarNavigationSelectionBar.svelte';
+       import { Pin } from '@lucide/svelte';
+       import { buildConversationTree } from '$lib/stores/conversations.svelte';
 
        interface Props {
                class: string;
        }
 
        let {
+               allSelectedArePinned,
+               allVisibleSelected,
                class: className,
-               filteredConversations,
                currentChatId,
+               filteredConversations,
                isSearchModeActive,
-               searchQuery,
                isSelectionMode = false,
-               selectedIds = new Set<string>(),
-               onSelect,
-               onEdit,
+               onBulkDelete,
+               onBulkExport,
+               onBulkPinToggle,
+               onCloseSelection,
                onDelete,
-               onStop,
-               onToggleSelect,
+               onEdit,
                onEnterSelectionMode,
-               onSelectionClick,
                onRowMouseDown,
-               visibleCount,
-               allVisibleSelected,
-               someVisibleSelected,
-               allSelectedArePinned,
-               pinStateIsMixed,
+               onSelect,
                onSelectAllToggle,
-               onBulkPinToggle,
-               onBulkExport,
-               onBulkDelete,
-               onCloseSelection
+               onSelectionClick,
+               onStop,
+               onToggleSelect,
+               pinStateIsMixed,
+               searchQuery,
+               selectedIds = new Set<string>(),
+               someVisibleSelected,
+               visibleCount
        }: Props = $props();
 
        let conversationTree = $derived(buildConversationTree(filteredConversations));
                                        <li class="group/item relative mb-1 p-0">
                                                <SidebarNavigationConversationItem
                                                        conversation={{
-                                                               id: conversation.id,
-                                                               name: conversation.name,
-                                                               lastModified: conversation.lastModified,
                                                                currNode: conversation.currNode,
                                                                forkedFromConversationId: conversation.forkedFromConversationId,
+                                                               id: conversation.id,
+                                                               lastModified: conversation.lastModified,
+                                                               name: conversation.name,
                                                                pinned: conversation.pinned
                                                        }}
                                                        {depth}
                                                <li class="group/item relative mb-1 p-0">
                                                        <SidebarNavigationConversationItem
                                                                conversation={{
-                                                                       id: conversation.id,
-                                                                       name: conversation.name,
-                                                                       lastModified: conversation.lastModified,
                                                                        currNode: conversation.currNode,
                                                                        forkedFromConversationId: conversation.forkedFromConversationId,
+                                                                       id: conversation.id,
+                                                                       lastModified: conversation.lastModified,
+                                                                       name: conversation.name,
                                                                        pinned: conversation.pinned
                                                                }}
                                                                {depth}
index 491e7c34798357269a8b25f37628f7c4faf94610..486d20eb8bd087d1cc45ab0c3ccd017d53503ff1 100644 (file)
@@ -9,10 +9,10 @@
        }
 
        let {
-               value = $bindable(''),
-               placeholder = 'Search conversations...',
+               class: className,
                onInput,
-               class: className
+               placeholder = 'Search conversations...',
+               value = $bindable('')
        }: Props = $props();
 </script>
 
index 68d6c214366b03d5f50522055c159d2cabe6f2da..af952899fc0e34e0a45d72e999ed4704a68c3fee 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { buildConversationTree } from '$lib/stores/conversations.svelte';
        import SidebarNavigationConversationItem from './SidebarNavigationConversationItem.svelte';
+       import { buildConversationTree } from '$lib/stores/conversations.svelte';
 
        interface Props {
                class?: string;
 
        let {
                class: className = '',
-               searchQuery,
-               filteredConversations,
                currentChatId,
+               filteredConversations,
                isSelectionMode = false,
-               selectedIds = new Set<string>(),
-               onSelect,
-               onEdit,
                onDelete,
-               onStop,
-               onToggleSelect,
+               onEdit,
                onEnterSelectionMode,
+               onRowMouseDown,
+               onSelect,
                onSelectionClick,
-               onRowMouseDown
+               onStop,
+               onToggleSelect,
+               searchQuery,
+               selectedIds = new Set<string>()
        }: Props = $props();
 
        let tree = $derived(buildConversationTree(filteredConversations));
                                <li class="group/item relative mb-1 p-0">
                                        <SidebarNavigationConversationItem
                                                conversation={{
-                                                       id: conversation.id,
-                                                       name: conversation.name,
-                                                       lastModified: conversation.lastModified,
                                                        currNode: conversation.currNode,
                                                        forkedFromConversationId: conversation.forkedFromConversationId,
+                                                       id: conversation.id,
+                                                       lastModified: conversation.lastModified,
+                                                       name: conversation.name,
                                                        pinned: conversation.pinned
                                                }}
                                                {depth}
index 15412e57b62e56f0913d72fdbe290bf9755721e4..60ae1e6709fa6ed9f0dcffb489495068a00b0db6 100644 (file)
        }
 
        let {
+               allVisibleSelected,
                class: className = '',
+               onBulkDelete,
+               onBulkExport,
+               onBulkPinToggle,
+               onClose,
+               onSelectAllToggle,
+               pinStateIsMixed,
                selectedCount,
-               visibleCount,
-               allVisibleSelected,
-               someVisibleSelected,
                someSelectedPinned,
-               pinStateIsMixed,
-               onSelectAllToggle,
-               onBulkPinToggle,
-               onBulkExport,
-               onBulkDelete,
-               onClose
+               someVisibleSelected,
+               visibleCount
        }: Props = $props();
 
        let showDeleteDialog = $state(false);
index d9c4386e4c109b687a817fef2725f64a359fc292..ffbfd9fdb6270c125d43b5e8aaa3f746267cf876 100644 (file)
@@ -1,17 +1,17 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { base } from '$app/paths';
-       import { AlertTriangle, RefreshCw, Key, CheckCircle, XCircle } from '@lucide/svelte';
+       import { AlertTriangle, CheckCircle, Key, RefreshCw, XCircle } from '@lucide/svelte';
        import { goto } from '$app/navigation';
+       import { base } from '$app/paths';
        import { Button } from '$lib/components/ui/button';
        import { Input } from '$lib/components/ui/input';
        import Label from '$lib/components/ui/label/label.svelte';
-       import { serverStore, serverLoading } from '$lib/stores/server.svelte';
-       import { config, settingsStore } from '$lib/stores/settings.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 { fade, fly, scale } from 'svelte/transition';
        import { KeyboardKey } from '$lib/enums';
+       import { serverLoading, serverStore } from '$lib/stores/server.svelte';
+       import { config, settingsStore } from '$lib/stores/settings.svelte';
+       import { fade, fly, scale } from 'svelte/transition';
 
        interface Props {
                class?: string;
@@ -55,6 +55,7 @@
                showApiKeyInput = true;
                // Pre-fill with current API key if it exists
                const currentConfig = config();
+
                apiKeyInput = currentConfig.apiKey?.toString() || '';
        }
 
@@ -71,8 +72,8 @@
                        // Test the API key by making a real request to the server
                        const response = await fetch(`${base}/props`, {
                                headers: {
-                                       'Content-Type': 'application/json',
-                                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKeyInput.trim()}`
+                                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKeyInput.trim()}`,
+                                       'Content-Type': 'application/json'
                                }
                        });
 
                </div>
 
                {#if isAccessDeniedError && !showApiKeyInput}
-                       <div in:fly={{ y: 10, duration: 300, delay: 200 }} class="mb-4">
+                       <div in:fly={{ delay: 200, duration: 300, y: 10 }} class="mb-4">
                                <Button onclick={handleShowApiKeyInput} variant="outline" class="w-full">
                                        <Key class={ICON_CLASS_DEFAULT} />
                                        Enter API Key
                {/if}
 
                {#if showApiKeyInput}
-                       <div in:fly={{ y: 10, duration: 300, delay: 200 }} class="mb-4 space-y-3 text-left">
+                       <div in:fly={{ delay: 200, duration: 300, y: 10 }} class="mb-4 space-y-3 text-left">
                                <div class="space-y-2">
                                        <Label for="api-key-input" class="text-sm font-medium">API Key</Label>
 
                                                {/if}
                                        </div>
                                        {#if apiKeyError}
-                                               <p class="text-sm text-destructive" in:fly={{ y: -10, duration: 200 }}>
+                                               <p class="text-sm text-destructive" in:fly={{ duration: 200, y: -10 }}>
                                                        {apiKeyError}
                                                </p>
                                        {/if}
                                        {#if apiKeyState === 'success'}
-                                               <p class="text-sm text-green-600" in:fly={{ y: -10, duration: 200 }}>
+                                               <p class="text-sm text-green-600" in:fly={{ duration: 200, y: -10 }}>
                                                        ✓ API key validated successfully! Connecting...
                                                </p>
                                        {/if}
                {/if}
 
                {#if showRetry}
-                       <div in:fly={{ y: 10, duration: 300, delay: 200 }}>
+                       <div in:fly={{ delay: 200, duration: 300, y: 10 }}>
                                <Button onclick={handleRetryConnection} disabled={isServerLoading} class="w-full">
                                        {#if isServerLoading}
                                                <RefreshCw class="{ICON_CLASS_DEFAULT} animate-spin" />
                {/if}
 
                {#if showTroubleshooting}
-                       <div class="mt-4 text-left" in:fly={{ y: 10, duration: 300, delay: 400 }}>
+                       <div class="mt-4 text-left" in:fly={{ delay: 400, duration: 300, y: 10 }}>
                                <details class="text-sm">
                                        <summary class="cursor-pointer text-muted-foreground hover:text-foreground">
                                                Troubleshooting
index ffdf4887c95129ad968d1bfba6fb5a7a6c3414c3..fb6327ed7dc1dda044bbe5a4fc5578ce93c6a2f0 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { AlertTriangle, Server } from '@lucide/svelte';
        import { Badge } from '$lib/components/ui/badge';
        import { Button } from '$lib/components/ui/button';
-       import { serverProps, serverLoading, serverError } from '$lib/stores/server.svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { singleModelName } from '$lib/stores/models.svelte';
+       import { serverError, serverLoading, serverProps } from '$lib/stores/server.svelte';
 
        interface Props {
                class?: string;
@@ -20,7 +20,9 @@
 
        function getStatusColor() {
                if (loading) return 'bg-yellow-500';
+
                if (error) return 'bg-red-500';
+
                if (serverData) return 'bg-green-500';
 
                return 'bg-gray-500';
@@ -28,7 +30,9 @@
 
        function getStatusText() {
                if (loading) return 'Connecting...';
+
                if (error) return 'Connection Error';
+
                if (serverData) return 'Connected';
 
                return 'Unknown';
index 8dfb04530cda3cba1eb135816a75d942e0be0d82..639e3e5a7562b8320a925fc17c8bb2c0841bf7f4 100644 (file)
@@ -1,4 +1,7 @@
 <script lang="ts">
+       import { RefreshCw } from '@lucide/svelte';
+       import { goto } from '$app/navigation';
+       import { page } from '$app/state';
        import {
                SettingsChatDesktopSidebar,
                SettingsChatFields,
                SettingsChatToolsTab,
                SettingsFooter
        } from '$lib/components/app/settings';
-       import { config, settingsStore } from '$lib/stores/settings.svelte';
+       import { Button } from '$lib/components/ui/button';
        import {
                NUMERIC_FIELDS,
                POSITIVE_INTEGER_FIELDS,
                SETTINGS_CHAT_SECTIONS,
                SETTINGS_SECTION_TITLES
        } from '$lib/constants';
-       import type { SettingsSection } from '$lib/types';
-       import { RouterService } from '$lib/services/router.service';
-       import { setMode } from 'mode-watcher';
-       import { ColorMode } from '$lib/enums/ui.enums';
-       import { fade } from 'svelte/transition';
-       import { goto } from '$app/navigation';
-       import { Button } from '$lib/components/ui/button';
-       import { RefreshCw } from '@lucide/svelte';
-       import { page } from '$app/state';
        import { setChatSettingsConfigContext } from '$lib/contexts';
-       import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
+       import { ColorMode } from '$lib/enums/ui.enums';
+       import { RouterService } from '$lib/services/router.service';
        import { modelsStore } from '$lib/stores/models.svelte';
        import { isRouterMode } from '$lib/stores/server.svelte';
+       import { config, settingsStore } from '$lib/stores/settings.svelte';
+       import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
+       import type { SettingsSection } from '$lib/types';
+       import { setMode } from 'mode-watcher';
+       import { fade } from 'svelte/transition';
        interface Props {
                initialSection?: string;
                getSectionHref?: (section: SettingsSection) => string;
        }
 
-       let { initialSection, getSectionHref }: Props = $props();
+       let { getSectionHref, initialSection }: Props = $props();
 
        let activeSlug = $derived(
                initialSection ?? (page.params as Record<string, string | undefined>).section ?? 'general'
@@ -87,6 +87,7 @@
                        } catch (error) {
                                alert('Invalid JSON in custom parameters. Please check the format and try again.');
                                console.error(error);
+
                                return;
                        }
                }
@@ -96,6 +97,7 @@
                for (const field of NUMERIC_FIELDS) {
                        if (processedConfig[field] !== undefined && processedConfig[field] !== '') {
                                const numValue = Number(processedConfig[field]);
+
                                if (!isNaN(numValue)) {
                                        if ((POSITIVE_INTEGER_FIELDS as readonly string[]).includes(field)) {
                                                const entryByMinMax = SETTINGS_CHAT_SECTIONS.flatMap(
                                                ).find((entry) => entry.key === field);
                                                const lo = entryByMinMax?.min ?? 1;
                                                const hi = entryByMinMax?.max ?? Number.POSITIVE_INFINITY;
+
                                                processedConfig[field] = Math.max(lo, Math.min(hi, Math.round(numValue)));
                                        } else {
                                                processedConfig[field] = numValue;
                                        }
                                } else {
                                        alert(`Invalid numeric value for ${field}. Please enter a valid number.`);
+
                                        return;
                                }
                        }
        }
 
        setChatSettingsConfigContext({
+               handleConfigChange,
+               handleThemeChange,
                get localConfig() {
                        return localConfig;
-               },
-               handleConfigChange,
-               handleThemeChange
+               }
        });
 </script>
 
index a54c61556ce71bdea9b9e1222a2fa193b3e13717..57da065df45d60b41e5be6925a1431329734b02d 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { RotateCcw, FlaskConical } from '@lucide/svelte';
+       import { FlaskConical, RotateCcw } from '@lucide/svelte';
+       import { SettingsChatParameterSourceIndicator } from '$lib/components/app/settings';
        import { Checkbox } from '$lib/components/ui/checkbox';
        import { Input } from '$lib/components/ui/input';
        import Label from '$lib/components/ui/label/label.svelte';
@@ -8,12 +8,12 @@
        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 { SettingsFieldType } from '$lib/enums/settings.enums';
-       import { settingsStore } from '$lib/stores/settings.svelte';
+       import { modelsStore, propsCacheVersion, selectedModelName } from '$lib/stores/models.svelte';
        import { serverStore } from '$lib/stores/server.svelte';
-       import { modelsStore, selectedModelName, propsCacheVersion } from '$lib/stores/models.svelte';
+       import { settingsStore } from '$lib/stores/settings.svelte';
        import { normalizeFloatingPoint } from '$lib/utils/precision';
-       import { SettingsChatParameterSourceIndicator } from '$lib/components/app/settings';
        import type { Component } from 'svelte';
 
        interface Props {
@@ -40,6 +40,7 @@
                                >;
                        }
                }
+
                return (serverStore.defaultParams ?? {}) as Record<string, unknown>;
        });
 </script>
@@ -52,6 +53,7 @@
                                {@const serverDefault = currentModelParams[field.key]}
                                {@const isCustomRealTime = (() => {
                                        if (serverDefault == null) return false;
+
                                        if (currentValue === '') return false;
 
                                        const numericInput = parseFloat(currentValue);
                                {@const serverDefault = currentModelParams[field.key]}
                                {@const isCustomRealTime = (() => {
                                        if (serverDefault == null) return false;
+
                                        if (currentValue === '' || currentValue === undefined) return false;
+
                                        return currentValue !== serverDefault;
                                })()}
 
index 39921d09802dc71b67c9183d53b818fbf7e867a0..d483840eb97c2532c7071a80bb4b2f2fd4fc01fe 100644 (file)
@@ -1,19 +1,19 @@
 <script lang="ts">
+       import { Button, type ButtonVariant } from '$lib/components/ui/button';
        import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import type { Component } from 'svelte';
-       import { Button, type ButtonVariant } from '$lib/components/ui/button';
 
        let {
-               title,
+               buttonClass,
+               buttonText,
+               buttonVariant,
                description,
                IconComponent,
-               buttonText,
                onclick,
+               summary,
+               title,
                titleClass,
-               buttonVariant,
-               buttonClass,
-               wrapperClass,
-               summary
+               wrapperClass
        }: {
                title: string;
                description: string;
index 57dbba30bc3932644885d33b200871f70223d153..42c7ee55aeb3a34efdf882f8996785750269d4eb 100644 (file)
@@ -1,18 +1,18 @@
 <script lang="ts">
-       import { Download, Upload, Trash2 } from '@lucide/svelte';
+       import SettingsChatImportExportSection from './SettingsChatImportExportSection.svelte';
+       import { Download, Trash2, Upload } from '@lucide/svelte';
        import {
-               DialogConversationSelection,
                DialogConfirmation,
+               DialogConversationSelection,
                DialogExportSettings
        } from '$lib/components/app';
-       import { createMessageCountMap } from '$lib/utils';
+       import SettingsGroup from '$lib/components/app/settings/SettingsGroup.svelte';
+       import { ConversationSelectionMode, FileExtensionText, HtmlInputType } from '$lib/enums';
+       import { conversations, conversationsStore } from '$lib/stores/conversations.svelte';
        import { settingsStore } from '$lib/stores/settings.svelte';
-       import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
-       import { toast } from 'svelte-sonner';
+       import { createMessageCountMap } from '$lib/utils';
        import { fade } from 'svelte/transition';
-       import { ConversationSelectionMode, HtmlInputType, FileExtensionText } from '$lib/enums';
-       import SettingsChatImportExportSection from './SettingsChatImportExportSection.svelte';
-       import SettingsGroup from '$lib/components/app/settings/SettingsGroup.svelte';
+       import { toast } from 'svelte-sonner';
 
        let exportedConversations = $state<DatabaseConversation[]>([]);
        let importedConversations = $state<DatabaseConversation[]>([]);
@@ -49,6 +49,7 @@
                        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
                        const url = URL.createObjectURL(blob);
                        const a = document.createElement('a');
+
                        a.href = url;
                        a.download = `llama_settings_${new Date().toISOString().split('T')[0]}.json`;
                        document.body.appendChild(a);
        function handleSettingsImport() {
                try {
                        const input = document.createElement('input');
+
                        input.type = HtmlInputType.FILE;
                        input.accept = FileExtensionText.JSON;
 
                        input.onchange = async (e) => {
                                const file = (e.target as HTMLInputElement)?.files?.[0];
+
                                if (!file) return;
 
                                try {
@@ -85,6 +88,7 @@
 
                                        if (!data || typeof data !== 'object' || !data.config) {
                                                toast.error('Invalid settings file: missing config');
+
                                                return;
                                        }
 
        async function handleExportClick() {
                try {
                        const allConversations = conversations();
+
                        if (allConversations.length === 0) {
                                toast.info('No conversations to export');
+
                                return;
                        }
 
                        const conversationsWithMessages = await Promise.all(
                                allConversations.map(async (conv: DatabaseConversation) => {
                                        const messages = await conversationsStore.getConversationMessages(conv.id);
+
                                        return { conv, messages };
                                })
                        );
                        const allData: ExportedConversation[] = await Promise.all(
                                selectedConversations.map(async (conv) => {
                                        const messages = await conversationsStore.getConversationMessages(conv.id);
+
                                        return { conv: $state.snapshot(conv), messages: $state.snapshot(messages) };
                                })
                        );
 
                        input.onchange = async (e) => {
                                const file = (e.target as HTMLInputElement)?.files?.[0];
+
                                if (!file) return;
 
                                try {
                        const selectedData = $state
                                .snapshot(fullImportData)
                                .filter((item) => selectedIds.has(item.conv.id));
-
                        const { imported, skipped } = await conversationsStore.importConversationsData(selectedData);
 
                        // A conversation already in the database is left untouched, so the summary
 
                        if (allConversations.length === 0) {
                                toast.info('No conversations to delete');
+
                                return;
                        }
 
                        IconComponent={Download}
                        buttonText="Export conversations"
                        onclick={handleExportClick}
-                       summary={{ show: showExportSummary, verb: 'Exported', items: exportedConversations }}
+                       summary={{ items: exportedConversations, show: showExportSummary, verb: 'Exported' }}
                />
 
                <SettingsChatImportExportSection
                        IconComponent={Upload}
                        buttonText="Import conversations"
                        onclick={handleImportClick}
-                       summary={{ show: showImportSummary, verb: 'Imported', items: importedConversations }}
+                       summary={{ items: importedConversations, show: showImportSummary, verb: 'Imported' }}
                />
 
                <SettingsChatImportExportSection
                        IconComponent={Download}
                        buttonText="Export settings"
                        onclick={handleSettingsExport}
-                       summary={{ show: showSettingsExportSummary, verb: 'Exported', items: [] }}
+                       summary={{ items: [], show: showSettingsExportSummary, verb: 'Exported' }}
                />
 
                <SettingsChatImportExportSection
                        IconComponent={Upload}
                        buttonText="Import settings"
                        onclick={handleSettingsImport}
-                       summary={{ show: showSettingsImportSummary, verb: 'Imported', items: [] }}
+                       summary={{ items: [], show: showSettingsImportSummary, verb: 'Imported' }}
                />
        </SettingsGroup>
 </div>
index 54679cfb6bf06cd8763a432c0fea57113a73806e..c3bf7bf5c50b2ce3594ff8bf5c5507ffdf41cbe7 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { ChevronDown, ChevronRight } from '@lucide/svelte';
+       import { McpServerIdentity, TruncatedText } from '$lib/components/app';
        import { Checkbox } from '$lib/components/ui/checkbox';
        import * as Collapsible from '$lib/components/ui/collapsible';
-       import { TruncatedText, McpServerIdentity } from '$lib/components/app';
-       import { toolsStore } from '$lib/stores/tools.svelte';
-       import { permissionsStore } from '$lib/stores/permissions.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
        import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        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 { SvelteSet } from 'svelte/reactivity';
 
        let expandedGroups = new SvelteSet<string>();
index d8e265dbd237c4f88816611c1d82f25e7ef154d7..aa197228a3107e8e441fa10053e2e76ba324dcbf 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import { Settings } from '@lucide/svelte';
+       import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
        import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
 
        interface Props {
@@ -10,7 +10,7 @@
                onSectionChange?: (section: SettingsSectionTitle) => void;
        }
 
-       let { sections, isActive, getHref, onSectionChange }: Props = $props();
+       let { getHref, isActive, onSectionChange, sections }: Props = $props();
 </script>
 
 <div class="sticky top-2 hidden w-64 flex-col self-start bg-background py-4 md:flex gap-6">
index 1383b65f18a199e9af2b808e3a3061e338464401..5572d118f78345b9f9f6bb966423107ebb61c2bf 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
+       import { ChevronLeft, ChevronRight, Settings } from '@lucide/svelte';
        import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
-       import { Settings, ChevronLeft, ChevronRight } from '@lucide/svelte';
-       import { onMount, tick } from 'svelte';
-       import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
        import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte';
+       import type { SettingsSection, SettingsSectionTitle } from '$lib/types';
+       import { onMount, tick } from 'svelte';
 
        interface Props {
                sections: SettingsSection[];
                onSectionChange?: (section: SettingsSectionTitle) => void;
        }
 
-       let { sections, isActive, getHref, onSectionChange }: Props = $props();
+       let { getHref, isActive, onSectionChange, sections }: Props = $props();
 
        const carousel = useScrollCarousel();
 
        onMount(async () => {
                await tick();
+
                if (carousel.scrollContainer) {
                        const activeTab = carousel.scrollContainer.querySelector('[data-active="true"]');
+
                        if (activeTab instanceof HTMLElement) {
                                carousel.scrollToCenter(activeTab);
                        }
index afc37377d0caac81e9df325dba0635d8719bd065..202335f0b08799b0ba6f35a84d60b1d890bb5179 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import { Button } from '$lib/components/ui/button';
+       import { RotateCcw } from '@lucide/svelte';
        import * as AlertDialog from '$lib/components/ui/alert-dialog';
+       import { Button } from '$lib/components/ui/button';
        import { settingsStore } from '$lib/stores/settings.svelte';
-       import { RotateCcw } from '@lucide/svelte';
 
        interface Props {
                onReset?: () => void;
index 113d32176b873712e0483685274adef0f5995a57..78dc19a13bf63a9a2c9254d596c24913cb1ecde8 100644 (file)
@@ -6,7 +6,7 @@
                children: Snippet;
        }
 
-       let { title, children }: Props = $props();
+       let { children, title }: Props = $props();
 </script>
 
 <div>
index 49fc36a586f5a4156e9faec4236b49504a59d7e0..89cecb9267c73ee83e8e1c818c8443b59f01e779 100644 (file)
@@ -1,20 +1,20 @@
 <script lang="ts">
-       import { X, Plus } from '@lucide/svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
-       import { Button } from '$lib/components/ui/button';
-       import * as Empty from '$lib/components/ui/empty';
+       import McpLogo from '../mcp/McpLogo.svelte';
+       import { Plus, X } from '@lucide/svelte';
+       import { browser } from '$app/environment';
+       import { goto, replaceState } from '$app/navigation';
+       import { page } from '$app/state';
        import { ActionIcon, McpServerCard, McpServerCardSkeleton } from '$lib/components/app';
        import { DialogMcpServerAddNew } from '$lib/components/app/dialogs';
-       import { HealthCheckStatus } from '$lib/enums';
+       import { Button } from '$lib/components/ui/button';
+       import * as Empty from '$lib/components/ui/empty';
        import { ROUTES } from '$lib/constants';
-       import { fade } from 'svelte/transition';
+       import { HealthCheckStatus } from '$lib/enums';
+       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { toolsStore } from '$lib/stores/tools.svelte';
        import { onMount } from 'svelte';
-       import McpLogo from '../mcp/McpLogo.svelte';
-       import { browser } from '$app/environment';
-       import { page } from '$app/state';
-       import { goto, replaceState } from '$app/navigation';
+       import { fade } from 'svelte/transition';
 
        interface Props {
                class?: string;
@@ -30,6 +30,7 @@
 
        $effect(() => {
                const currentId = page.route.id;
+
                return () => {
                        previousRouteId = currentId;
                };
@@ -37,6 +38,7 @@
 
        function handleClose() {
                const prevIsMcpServers = previousRouteId === '/mcp-servers';
+
                if (browser && window.history.length > 1 && !prevIsMcpServers) {
                        history.back();
                } else {
@@ -49,6 +51,7 @@
                        isAddingServer = true;
 
                        const newUrl = new URL(page.url);
+
                        newUrl.searchParams.delete('add');
 
                        replaceState(newUrl, {});
@@ -63,6 +66,7 @@
        // renders and keeps the enable toggle reachable.
        function isServerPending(serverId: string, enabled: boolean): boolean {
                const status = mcpStore.getHealthCheckState(serverId).status;
+
                return (
                        status === HealthCheckStatus.CONNECTING || (status === HealthCheckStatus.IDLE && enabled)
                );
                                                enabled={conversationsStore.isMcpServerEnabledForChat(server.id)}
                                                onToggle={async () => {
                                                        const wasEnabled = conversationsStore.isMcpServerEnabledForChat(server.id);
+
                                                        await conversationsStore.toggleMcpServerForChat(server.id);
+
                                                        if (!wasEnabled) {
                                                                // Promote the connection so tools/prompts/resources become
                                                                // available right away instead of waiting for the next chat-init.
index a3a406f26fafce40306802ca3c6e1d8a8be584f3..8189ad0e7242730226a1743753b07e1a5369abed 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { APPLE_META_TAGS, MEDIA_QUERIES, THEME_COLORS } from '$lib/constants/pwa';
        import { APP_NAME } from '$lib/constants';
+       import { APPLE_META_TAGS, MEDIA_QUERIES, THEME_COLORS } from '$lib/constants/pwa';
 
        let { appName = APP_NAME } = $props();
 </script>
index 500abdc135617a054f7cc1be57ef7cf6f149b2ed..0976e96464e77b9c2bd0974f40f6e593beedd6cb 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
-       import * as Card from '$lib/components/ui/card';
        import { Button } from '$lib/components/ui/button';
+       import * as Card from '$lib/components/ui/card';
 
-       let { needRefresh: needRefreshProp, updateServiceWorker, forceReload } = $props();
+       let { forceReload, needRefresh: needRefreshProp, updateServiceWorker } = $props();
        let needRefresh = $derived(needRefreshProp ?? false);
 </script>
 
index 162107eb1ef8650b46a28d7fa22d8148248c8e11..33f2fa584afab10083a8560eef4be0a5c123e66f 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import { buttonVariants } from '$lib/components/ui/button/index.js';
        import { cn } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: AlertDialogPrimitive.ActionProps = $props();
 </script>
index 6b3f354a91dcfc7a27bb4b6fad86c64d0951d53a..2a6ad68141ccbf56915dbb4d8af4d2621b979564 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import { buttonVariants } from '$lib/components/ui/button/index.js';
        import { cn } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: AlertDialogPrimitive.CancelProps = $props();
 </script>
index c0bb2a34e4f216f1ef9ffaf663753800fbec7fdd..b3b2908044d18787ddeb700674e2b6309104583f 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import AlertDialogOverlay from './alert-dialog-overlay.svelte';
        import { cn, type WithoutChild, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
                portalProps,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChild<AlertDialogPrimitive.ContentProps> & {
                portalProps?: WithoutChildrenOrChild<AlertDialogPrimitive.PortalProps>;
index 84735d870c669b27050f6496305689a29dca4e5b..9b0906a7567efaa883a8cc20cc030418573f08e7 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: AlertDialogPrimitive.DescriptionProps = $props();
 </script>
index da0f7be74b65a78018d4944120c5452556b5bbab..38894b03d36d612eb3c38616376f1710eff9d98d 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index fa6539db2902b32c307d09a4c228a2732063abd0..030bc15724348770f029ebd1fe1f5c649946764e 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index b047dcf6c450f07b506fb0d71c42f293fe3d9ec6..6a648b79715b55bbe8bd17e997f79d0710c83e7e 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: AlertDialogPrimitive.OverlayProps = $props();
 </script>
index 4c610aa602336970a6ff3a7e526d95bc42189245..c882a4aa288bc27f1598285e447627057b5ef3a1 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: AlertDialogPrimitive.TitleProps = $props();
 </script>
index a4439bc2e16ffc262cb7061c2f92b5d92f461ad0..8cf5899dfc47239f73cef27b82ff3e62bfc9853b 100644 (file)
@@ -1,13 +1,13 @@
-import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
-import Trigger from './alert-dialog-trigger.svelte';
-import Title from './alert-dialog-title.svelte';
 import Action from './alert-dialog-action.svelte';
 import Cancel from './alert-dialog-cancel.svelte';
+import Content from './alert-dialog-content.svelte';
+import Description from './alert-dialog-description.svelte';
 import Footer from './alert-dialog-footer.svelte';
 import Header from './alert-dialog-header.svelte';
 import Overlay from './alert-dialog-overlay.svelte';
-import Content from './alert-dialog-content.svelte';
-import Description from './alert-dialog-description.svelte';
+import Title from './alert-dialog-title.svelte';
+import Trigger from './alert-dialog-trigger.svelte';
+import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
 
 const Root = AlertDialogPrimitive.Root;
 const Portal = AlertDialogPrimitive.Portal;
index 440d0069d3b1446df4eb204565139607648353f0..8d87d2d8fe6315c5987dd308a5ed7214b4dc2f5f 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 0721aebf12a15fa36a51d6bbc8be57fad0bdb62a..1cdb7958ac9a6cdce6c9cbb219d75d399900ebdb 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 7d79e4bc0edf4962d1cb59eaba3382be6f72f8b8..c42000a54e4b5a8cd893ea91fe14b8d6cec48c8c 100644 (file)
@@ -1,17 +1,17 @@
 <script lang="ts" module>
-       import { type VariantProps, tv } from 'tailwind-variants';
+       import { tv, type VariantProps } from 'tailwind-variants';
 
        export const alertVariants = tv({
                base: 'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+               defaultVariants: {
+                       variant: 'default'
+               },
                variants: {
                        variant: {
                                default: 'bg-card text-card-foreground',
                                destructive:
                                        'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current'
                        }
-               },
-               defaultVariants: {
-                       variant: 'default'
                }
        });
 
 </script>
 
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
+               ref = $bindable(null),
                variant = 'default',
-               children,
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
                variant?: AlertVariant;
index 9fbf0b80a592b56bfabf90dc566797200612b8d7..0be4deae815113bc0f3b95adf463f2953ac2a6ca 100644 (file)
@@ -1,22 +1,22 @@
 <script lang="ts" module>
-       import { type VariantProps, tv } from 'tailwind-variants';
+       import { tv, type VariantProps } from 'tailwind-variants';
 
        export const badgeVariants = tv({
                base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3',
+               defaultVariants: {
+                       variant: 'default'
+               },
                variants: {
                        variant: {
                                default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent',
+                               destructive:
+                                       'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
+                               outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
                                secondary:
                                        'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent',
                                tertiary:
-                                       'bg-foreground/15 dark:bg-foreground/10 text-foreground [a&]:hover:bg-foreground/25 border-transparent',
-                               destructive:
-                                       'bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white',
-                               outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
+                                       'bg-foreground/15 dark:bg-foreground/10 text-foreground [a&]:hover:bg-foreground/25 border-transparent'
                        }
-               },
-               defaultVariants: {
-                       variant: 'default'
                }
        });
 
 </script>
 
 <script lang="ts">
-       import type { HTMLAnchorAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLAnchorAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               href,
+               children,
                class: className,
+               href,
+               ref = $bindable(null),
                variant = 'default',
-               children,
                ...restProps
        }: WithElementRef<HTMLAnchorAttributes> & {
                variant?: BadgeVariant;
index 4587ec38ec3f44a9ca47d9fd0a5a0d7bb1f1e914..89afc3a8ed5c987fc2767652b954b1d47f6aa24f 100644 (file)
@@ -7,7 +7,7 @@
                children: Snippet;
        }
 
-       let { class: className, children, ...restProps }: Props = $props();
+       let { children, class: className, ...restProps }: Props = $props();
 </script>
 
 <div
index 165b9fd6895335faddfa5eb60b30a8890cce00cd..3f39b651e8973a34d2423c42aafe30124aa3243b 100644 (file)
@@ -1,34 +1,34 @@
 <script lang="ts" module>
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
        import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
-       import { type VariantProps, tv } from 'tailwind-variants';
+       import { tv, type VariantProps } from 'tailwind-variants';
 
        export const buttonVariants = tv({
                base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+               defaultVariants: {
+                       size: 'default',
+                       variant: 'default'
+               },
                variants: {
+                       size: {
+                               default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+                               icon: 'size-9',
+                               'icon-lg': 'size-10',
+                               'icon-sm': 'size-5 rounded-sm',
+                               lg: 'h-10 rounded-lg px-6 has-[>svg]:px-4',
+                               sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5'
+                       },
                        variant: {
                                default: 'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
                                destructive:
                                        'bg-destructive shadow-sm hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white!',
+                               ghost: 'hover:text-accent-foreground hover:bg-muted-foreground/10 backdrop-blur-sm',
+                               link: 'text-primary underline-offset-4 hover:underline',
                                outline:
                                        'shadow-sm hover:text-accent-foreground hover:bg-muted-foreground/10 backdrop-blur-sm dark:border-input border',
                                secondary:
-                                       'bg-background dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm text-foreground hover:bg-muted-foreground/20 dark:hover:bg-muted-foreground/25',
-                               ghost: 'hover:text-accent-foreground hover:bg-muted-foreground/10 backdrop-blur-sm',
-                               link: 'text-primary underline-offset-4 hover:underline'
-                       },
-                       size: {
-                               default: 'h-9 px-4 py-2 has-[>svg]:px-3',
-                               sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
-                               lg: 'h-10 rounded-lg px-6 has-[>svg]:px-4',
-                               'icon-lg': 'size-10',
-                               icon: 'size-9',
-                               'icon-sm': 'size-5 rounded-sm'
+                                       'bg-background dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm text-foreground hover:bg-muted-foreground/20 dark:hover:bg-muted-foreground/25'
                        }
-               },
-               defaultVariants: {
-                       variant: 'default',
-                       size: 'default'
                }
        });
 
 
 <script lang="ts">
        let {
+               children,
                class: className,
-               variant = 'default',
-               size = 'default',
-               ref = $bindable(null),
+               disabled,
                href = undefined,
+               ref = $bindable(null),
+               size = 'default',
                type = 'button',
-               disabled,
-               children,
+               variant = 'default',
                ...restProps
        }: ButtonProps = $props();
 </script>
@@ -60,7 +60,7 @@
        <a
                bind:this={ref}
                data-slot="button"
-               class={cn(buttonVariants({ variant, size }), className)}
+               class={cn(buttonVariants({ size, variant }), className)}
                href={disabled ? undefined : href}
                aria-disabled={disabled}
                role={disabled ? 'link' : undefined}
@@ -73,7 +73,7 @@
        <button
                bind:this={ref}
                data-slot="button"
-               class={cn(buttonVariants({ variant, size }), className)}
+               class={cn(buttonVariants({ size, variant }), className)}
                {type}
                {disabled}
                {...restProps}
index 0d4e965a67b3d649d34e0d18227bde04532f7c74..19baee33d26ae7377da272f5938cd723ce512f17 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index c68f6136078b11defbce249dc981e3b648f93cac..230d6decdacde0b256440cdf0d1e123d1c2b1bb3 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 81578dfdf8b8ca1e6e734f35b0d5632b80bbc4e1..d4c588875f84ff9a32309839e749ee173fd91291 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
 </script>
index 0366459f8e6b1991f54090c61bb96cedd127c138..53151b384925021aa63f039634706f8974811bac 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 74ab1639bdff163aa0a554907350de0119b9fa7a..0c9db040dc6a0797f617bd7304d5d0850e514e67 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 8dfc062dc38d27014265ea3b07bb191e685dea5f..a60b4f936be168979a701a9b782aec7c38aeaf2f 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index d0a57d0c9704da401a68810c11473160b116ac53..1df9f113bea7e99dd279d73823930eb3e9e4cca6 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
        import { BOX_BORDER } from '$lib/constants';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 77d36747788e7a492ecbb236cc6e5c1fa8fe6428..87d089b32a13192824c410bc63f4ce7d25696c10 100644 (file)
@@ -1,10 +1,10 @@
 import Root from './card.svelte';
+import Action from './card-action.svelte';
 import Content from './card-content.svelte';
 import Description from './card-description.svelte';
 import Footer from './card-footer.svelte';
 import Header from './card-header.svelte';
 import Title from './card-title.svelte';
-import Action from './card-action.svelte';
 
 export {
        Root,
index 0eded6810ecb405b4b25079a4d60c6585bc218a6..8e2a4b5e7e935f5a9eff13bb3843f3a524632dc9 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
-       import { Checkbox as CheckboxPrimitive } from 'bits-ui';
        import CheckIcon from '@lucide/svelte/icons/check';
        import MinusIcon from '@lucide/svelte/icons/minus';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { Checkbox as CheckboxPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                checked = $bindable(false),
-               indeterminate = $bindable(false),
                class: className,
+               indeterminate = $bindable(false),
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
 </script>
index 7a8c5da46811a97fc2ba0ac9f4dd3c04859723f2..68f56dfc5420bb6941746e7898d1e86cfad16d95 100644 (file)
@@ -2,8 +2,8 @@
        import { Collapsible as CollapsiblePrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                open = $bindable(false),
+               ref = $bindable(null),
                ...restProps
        }: CollapsiblePrimitive.RootProps = $props();
 </script>
index 8181f6448d98ddfbade523e410f71d37bdc17f44..35d83fb08fc18362ba340c1cd48642a5ae376990 100644 (file)
@@ -1,6 +1,6 @@
 import Root from './collapsible.svelte';
-import Trigger from './collapsible-trigger.svelte';
 import Content from './collapsible-content.svelte';
+import Trigger from './collapsible-trigger.svelte';
 
 export {
        Root,
index 0e1b07c40ed203a75dff262acda532f049e76af7..3063795ce7d89364c75c5fc3ece36e929de03321 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { Dialog as DialogPrimitive } from 'bits-ui';
-       import XIcon from '@lucide/svelte/icons/x';
-       import type { Snippet } from 'svelte';
        import * as Dialog from './index.js';
+       import XIcon from '@lucide/svelte/icons/x';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils';
+       import { Dialog as DialogPrimitive } from 'bits-ui';
+       import type { Snippet } from 'svelte';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
                portalProps,
-               children,
+               ref = $bindable(null),
                showCloseButton = true,
                ...restProps
        }: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
index 6c0c192316266ac5db18eae7910ddcb01c09b5cd..97873cfee30a651572a330b45cbb03f1ec97ce1c 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as DialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils';
+       import { Dialog as DialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: DialogPrimitive.DescriptionProps = $props();
 </script>
index abf948fc8ea9c970d6552aef3d4413e8c40a401f..6e1926b93dcfbe6120baceecbfdbd0c97ca5fd35 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 7ba9ba17b0dccd12120bc74255f91db3251f49ab..5c6a1aee74adc942b52ea1d50a382322913ba8f5 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index a7803f9036802cdca0b54d7094c6e3e5822081c5..bd6fb3fc208a0bb874d046c3943fcf1b09e860e9 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as DialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils';
+       import { Dialog as DialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: DialogPrimitive.OverlayProps = $props();
 </script>
index e8c99c5d95080ec45a368242405f95637cadf8c2..68d6522e4fb258ecc5423b21f93be1ef6a60a779 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as DialogPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils';
+       import { Dialog as DialogPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: DialogPrimitive.TitleProps = $props();
 </script>
index d9e5fb86efe22c051478774f1b179ba6da4dfdda..b9e86c320087be7f7785f93adc31b703fbdde852 100644 (file)
@@ -1,13 +1,12 @@
-import { Dialog as DialogPrimitive } from 'bits-ui';
-
-import Title from './dialog-title.svelte';
+import Close from './dialog-close.svelte';
+import Content from './dialog-content.svelte';
+import Description from './dialog-description.svelte';
 import Footer from './dialog-footer.svelte';
 import Header from './dialog-header.svelte';
 import Overlay from './dialog-overlay.svelte';
-import Content from './dialog-content.svelte';
-import Description from './dialog-description.svelte';
+import Title from './dialog-title.svelte';
 import Trigger from './dialog-trigger.svelte';
-import Close from './dialog-close.svelte';
+import { Dialog as DialogPrimitive } from 'bits-ui';
 
 const Root = DialogPrimitive.Root;
 const Portal = DialogPrimitive.Portal;
index e71acefab673b66dcc63b8ac6004b0b58a1c190f..142a5e1a62d4a82dbf040548663056a95d50dfd8 100644 (file)
@@ -1,16 +1,16 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import CheckIcon from '@lucide/svelte/icons/check';
        import MinusIcon from '@lucide/svelte/icons/minus';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import type { Snippet } from 'svelte';
 
        let {
-               ref = $bindable(null),
                checked = $bindable(false),
-               indeterminate = $bindable(false),
-               class: className,
                children: childrenProp,
+               class: className,
+               indeterminate = $bindable(false),
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
                children?: Snippet;
index 0ca0d3964acf789ca1781115d4914d681bc6941c..af34ad6aa572066f0a977789a8b9721fecc052bc 100644 (file)
@@ -3,10 +3,10 @@
        import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
+               class: className,
+               portalProps,
                ref = $bindable(null),
                sideOffset = 4,
-               portalProps,
-               class: className,
                ...restProps
        }: DropdownMenuPrimitive.ContentProps & {
                portalProps?: DropdownMenuPrimitive.PortalProps;
index f2179668b5612a1c3601a34b9a9722c2e767f41b..3b98c11fe7c8c227ecb2729d2f75ed7c01f8c0c3 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import type { ComponentProps } from 'svelte';
 
        let {
-               ref = $bindable(null),
                class: className,
                inset,
+               ref = $bindable(null),
                ...restProps
        }: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
                inset?: boolean;
index 1ac561595d98c38738f5b8c6c8ebe4f6b8df0c74..82e19def57cdf575bdd8b188f3be087ee24a623a 100644 (file)
@@ -3,9 +3,9 @@
        import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
                inset,
+               ref = $bindable(null),
                variant = 'default',
                ...restProps
        }: DropdownMenuPrimitive.ItemProps & {
index 15b546ea57c31cabe16fa4c1a17301628c1edde3..36166cd79ca329fb6003f92cffc543fc3e1a7a40 100644 (file)
@@ -3,10 +3,10 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
                inset,
-               children,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
                inset?: boolean;
index 97ba772838739b9873f6facbb8138adf9a8bb768..077bd0941303b54b1c36c05d77eb6d0be2b365d7 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import CircleIcon from '@lucide/svelte/icons/circle';
        import { cn, type WithoutChild } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children: childrenProp,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
 </script>
index 17b64ac9c2477ff9e278b7599b33b6f1e7082f0a..b2da42ae7694756ae24686b4e5aa5dc784a710e7 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: DropdownMenuPrimitive.SeparatorProps = $props();
 </script>
index c3ccc21920cb5d5082b86803cb7e05730cb365de..72ff18fb3103a46aa68a1fea153c86d867969dd1 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
 </script>
index e26c51cdc23c7f350b22ebc328f396cf39293adc..6760a84927e330f68ae16935d651b043ae6decc5 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: DropdownMenuPrimitive.SubContentProps = $props();
 </script>
index 550a789ce852d220f3d1076d01300e8544b08e4a..8e07ce1fb5e0f3ab4d90ab97e03c784c9cee475e 100644 (file)
@@ -1,13 +1,13 @@
 <script lang="ts">
-       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
        import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
        import { cn } from '$lib/components/ui/utils.js';
+       import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
                inset,
-               children,
+               ref = $bindable(null),
                ...restProps
        }: DropdownMenuPrimitive.SubTriggerProps & {
                inset?: boolean;
index aeb398e0611336e9f6317169ea66f953c4ae84ae..cf03db7f127dc10e4a0d449138433167c82847ea 100644 (file)
@@ -1,17 +1,17 @@
-import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
 import Content from './dropdown-menu-content.svelte';
 import Group from './dropdown-menu-group.svelte';
+import GroupHeading from './dropdown-menu-group-heading.svelte';
 import Item from './dropdown-menu-item.svelte';
 import Label from './dropdown-menu-label.svelte';
 import RadioGroup from './dropdown-menu-radio-group.svelte';
 import RadioItem from './dropdown-menu-radio-item.svelte';
 import Separator from './dropdown-menu-separator.svelte';
 import Shortcut from './dropdown-menu-shortcut.svelte';
-import Trigger from './dropdown-menu-trigger.svelte';
 import SubContent from './dropdown-menu-sub-content.svelte';
 import SubTrigger from './dropdown-menu-sub-trigger.svelte';
-import GroupHeading from './dropdown-menu-group-heading.svelte';
+import Trigger from './dropdown-menu-trigger.svelte';
+import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
 const Sub = DropdownMenuPrimitive.Sub;
 const Root = DropdownMenuPrimitive.Root;
 
index cbae3ab041b95e179d8fdcd6f237b79e4b727955..d763e85dba0d84ef92c396633083f2d19c00d4c4 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 4d0fd7d534dfc55cc0d47a3a7a9f434c220768fa..42c0cdf3d163af549dd4f56e9773daa631aea9af 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 87014feaffa199ea3561c6aa394468ff83fe7319..1636467f09576b5ff4253e0079f6dd010fa971de 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 13e15918c191daad703841c0c8c3a9d578756dae..8ca69f0608e9818f51083639bd05de91ac26b216 100644 (file)
@@ -3,14 +3,14 @@
 
        export const emptyMediaVariants = tv({
                base: 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
+               defaultVariants: {
+                       variant: 'default'
+               },
                variants: {
                        variant: {
                                default: 'bg-transparent',
                                icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-4"
                        }
-               },
-               defaultVariants: {
-                       variant: 'default'
                }
        });
 
@@ -22,9 +22,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                variant = 'default',
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant } = $props();
index 83c9810eb98360836813f67edb83e7eeb33e930b..37d2701b1b7d4c391c225db5fda86bac5ef3f230 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 6c38c10a9b6ce21d0e505295ac32e9d0c1620e22..35855885043b18e4707392e639b13b99974ae707 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index cae5ff9148df9a716db1af78dc1488890170c655..42cb71e1d69ec716dff809fe2a672303f9de3a9b 100644 (file)
@@ -1,9 +1,9 @@
 import Root from './empty.svelte';
+import Content from './empty-content.svelte';
+import Description from './empty-description.svelte';
 import Header from './empty-header.svelte';
 import Media from './empty-media.svelte';
 import Title from './empty-title.svelte';
-import Description from './empty-description.svelte';
-import Content from './empty-content.svelte';
 
 export {
        Root,
index db1406a7b5f9788655dd92d1317355292551549c..1355ae401a7b17f49c8b4ba7cfef9e1765757789 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
-       import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
        import HoverCardPortal from './hover-card-portal.svelte';
+       import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
        import type { ComponentProps } from 'svelte';
 
        let {
-               ref = $bindable(null),
-               class: className,
                align = 'center',
-               sideOffset = 4,
+               class: className,
                portalProps,
+               ref = $bindable(null),
+               sideOffset = 4,
                ...restProps
        }: HoverCardPrimitive.ContentProps & {
                portalProps?: WithoutChildrenOrChild<ComponentProps<typeof HoverCardPortal>>;
index 098f69176d0eff2c15cbdefdd14f08937a87c221..5490fcda4c90614e9ec5644e39977004b6f826cd 100644 (file)
@@ -1,7 +1,7 @@
 import Root from './hover-card.svelte';
 import Content from './hover-card-content.svelte';
-import Trigger from './hover-card-trigger.svelte';
 import Portal from './hover-card-portal.svelte';
+import Trigger from './hover-card-trigger.svelte';
 
 export {
        Root,
index 2b6279b6420de0b5efe201d9566fc8ef0267abb1..87e71b196b88772676d0e52ae1c86ae1b0c2ed4f 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils';
+       import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
 
        type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
 
        >;
 
        let {
+               class: className,
+               files = $bindable(),
                ref = $bindable(null),
-               value = $bindable(),
                type,
-               files = $bindable(),
-               class: className,
+               value = $bindable(),
                ...restProps
        }: Props = $props();
 </script>
index 9da4ae369dfbbb638dc5743b7b5c3333eb28331a..aab94043e7785ebc2412eae519d91586c27e363a 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Label as LabelPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Label as LabelPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: LabelPrimitive.RootProps = $props();
 </script>
index c5937fb3a04309294233fa1655a3ffd44dadcf65..cff469d5b5cc7e22bc665bb554ca99c7b6ea5577 100644 (file)
@@ -1,8 +1,8 @@
 import Root from './popover.svelte';
 import Close from './popover-close.svelte';
 import Content from './popover-content.svelte';
-import Trigger from './popover-trigger.svelte';
 import Portal from './popover-portal.svelte';
+import Trigger from './popover-trigger.svelte';
 
 export {
        Root,
index b46e928b1b90c1501644ed27530b5814cacbfab1..6ee27dde24879a4def0fe51279d41c11dcdfdcf3 100644 (file)
@@ -1,18 +1,18 @@
 <script lang="ts">
-       import { Popover as PopoverPrimitive } from 'bits-ui';
        import PopoverPortal from './popover-portal.svelte';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { Popover as PopoverPrimitive } from 'bits-ui';
        import type { ComponentProps } from 'svelte';
 
        let {
-               ref = $bindable(null),
-               class: className,
-               sideOffset = 4,
-               side,
                align = 'center',
-               collisionPadding = 8,
                avoidCollisions = true,
+               class: className,
+               collisionPadding = 8,
                portalProps,
+               ref = $bindable(null),
+               side,
+               sideOffset = 4,
                ...restProps
        }: PopoverPrimitive.ContentProps & {
                portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
index 5ef3d0e932464348a90816fa0e12e3224e2792da..13624b56c9beb362141e34a86d0da431ecd81549 100644 (file)
@@ -3,8 +3,8 @@
        import { Popover as PopoverPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: PopoverPrimitive.TriggerProps = $props();
 </script>
index af3ab615289b67fa35fe0ef03b265d58c4c965fd..a3d9ebdf0c5fce53e12a209f5065626b7e35e5fa 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
        import CircleIcon from '@lucide/svelte/icons/circle';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
 </script>
index 083a0d4957acf4e447e05ef9c5d4cd3ec7cd0d14..77504486c4d2224e64bfd5a1f222d58ee3ba31b7 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { RadioGroup as RadioGroupPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                value = $bindable(''),
                ...restProps
        }: RadioGroupPrimitive.RootProps = $props();
index d5468067dee8acc4ee158d9201fb462fb9929f78..c2ba9a915cde8705cfd99c95de0aa96a4e60cf91 100644 (file)
@@ -1,5 +1,5 @@
-import Scrollbar from './scroll-area-scrollbar.svelte';
 import Root from './scroll-area.svelte';
+import Scrollbar from './scroll-area-scrollbar.svelte';
 
 export {
        Root,
index 3f0d00d5ebf87a2de9fbd3e42dc62c88c2bbce83..f0085804f6436557f97afb51f25c2e52c524965a 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
        import { cn, type WithoutChild } from '$lib/components/ui/utils';
+       import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
                orientation = 'vertical',
-               children,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChild<ScrollAreaPrimitive.ScrollbarProps> = $props();
 </script>
index ba6f8382e5679ca3b044bcc7f5d8381b5d6d5bcd..d881913c2e7e3ab2e8d804fb92ba24dbd0be3358 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
        import { Scrollbar } from './index.js';
        import { cn, type WithoutChild } from '$lib/components/ui/utils';
+       import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
                orientation = 'vertical',
+               ref = $bindable(null),
                scrollbarXClasses = '',
                scrollbarYClasses = '',
-               children,
                ...restProps
        }: WithoutChild<ScrollAreaPrimitive.RootProps> & {
                orientation?: 'vertical' | 'horizontal' | 'both' | undefined;
index bfa73d90ebd3e1c750682062831233bfbd88b575..35e552cfbc4c2e57bb9367fc808103f017b90d30 100644 (file)
@@ -1,14 +1,13 @@
-import { Select as SelectPrimitive } from 'bits-ui';
-
+import Content from './select-content.svelte';
 import Group from './select-group.svelte';
-import Label from './select-label.svelte';
+import GroupHeading from './select-group-heading.svelte';
 import Item from './select-item.svelte';
-import Content from './select-content.svelte';
-import Trigger from './select-trigger.svelte';
-import Separator from './select-separator.svelte';
+import Label from './select-label.svelte';
 import ScrollDownButton from './select-scroll-down-button.svelte';
 import ScrollUpButton from './select-scroll-up-button.svelte';
-import GroupHeading from './select-group-heading.svelte';
+import Separator from './select-separator.svelte';
+import Trigger from './select-trigger.svelte';
+import { Select as SelectPrimitive } from 'bits-ui';
 
 const Root = SelectPrimitive.Root;
 
index b54bc60c2274ce8f3e12c79830934d089a9640ef..cd64675972a01d0a147dc7a49b9e4c402a6eb489 100644 (file)
@@ -1,16 +1,16 @@
 <script lang="ts">
-       import { onDestroy, onMount } from 'svelte';
-       import { Select as SelectPrimitive } from 'bits-ui';
-       import SelectScrollUpButton from './select-scroll-up-button.svelte';
        import SelectScrollDownButton from './select-scroll-down-button.svelte';
+       import SelectScrollUpButton from './select-scroll-up-button.svelte';
        import { cn, type WithoutChild } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
+       import { onDestroy, onMount } from 'svelte';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
-               sideOffset = 4,
                portalProps,
-               children,
+               ref = $bindable(null),
+               sideOffset = 4,
                ...restProps
        }: WithoutChild<SelectPrimitive.ContentProps> & {
                portalProps?: SelectPrimitive.PortalProps;
@@ -20,7 +20,6 @@
 
        onMount(() => {
                const listenerOptions: AddEventListenerOptions = { passive: false };
-
                const blockOutsideWheel = (event: WheelEvent) => {
                        if (!ref) {
                                return;
@@ -33,7 +32,6 @@
                                event.stopPropagation();
                        }
                };
-
                const blockOutsideTouchMove = (event: TouchEvent) => {
                        if (!ref) {
                                return;
@@ -68,7 +66,6 @@
                const stopWheelPropagation = (event: WheelEvent) => {
                        event.stopPropagation();
                };
-
                const stopTouchPropagation = (event: TouchEvent) => {
                        event.stopPropagation();
                };
index 77c2042c8cd7e7397095f479615c83e3dc5e2b9d..fc7dcd6d6ec90df51f7e79bd019f63d7f5d9cea1 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { Select as SelectPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
        import type { ComponentProps } from 'svelte';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
 </script>
index 02543c1fc314a62bade65cd330ba9f5e3450430e..72e112d78f53d2f5ae05fc5999c21eb6c0a3f8b2 100644 (file)
@@ -1,14 +1,14 @@
 <script lang="ts">
        import CheckIcon from '@lucide/svelte/icons/check';
-       import { Select as SelectPrimitive } from 'bits-ui';
        import { cn, type WithoutChild } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
+               children: childrenProp,
                class: className,
-               value,
                label,
-               children: childrenProp,
+               ref = $bindable(null),
+               value,
                ...restProps
        }: WithoutChild<SelectPrimitive.ItemProps> = $props();
 </script>
        )}
        {...restProps}
 >
-       {#snippet children({ selected, highlighted })}
+       {#snippet children({ highlighted, selected })}
                <span class="absolute right-2 flex size-3.5 items-center justify-center">
                        {#if selected}
                                <CheckIcon class="size-4" />
                        {/if}
                </span>
                {#if childrenProp}
-                       {@render childrenProp({ selected, highlighted })}
+                       {@render childrenProp({ highlighted, selected })}
                {:else}
                        {label || value}
                {/if}
index e2b830cf11b4ac5906ecd37e3014808b74fce891..2eebf413629b7c5d921431fa4f12315ec5a0ae3a 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
 </script>
index 9256dd8b5934ee4ccb5e18ad3fb8cac83a7e04dc..c4d629b0a5cea50cd08e9cc8cb530aa48097fa25 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
        import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
-       import { Select as SelectPrimitive } from 'bits-ui';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
 </script>
index 552e52728de8dc7a1625350220ead5bea041e59e..f267bf612e6d1cf111993932ca7d4b5040adbd45 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
        import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
-       import { Select as SelectPrimitive } from 'bits-ui';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
 </script>
index 7daaa8d09f0de940db196804d5be2af5b024a1e8..d627076038d3162bdd405f7727336ba130d9121a 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { Separator as SeparatorPrimitive } from 'bits-ui';
        import { Separator } from '$lib/components/ui/separator/index.js';
        import { cn } from '$lib/components/ui/utils.js';
+       import type { Separator as SeparatorPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: SeparatorPrimitive.RootProps = $props();
 </script>
index 5bc28eeb47bf3d72218b0d40e984db8b366f2fe8..6bd8e52d9b9790bf59ac9a6b4ce635d8dca7a353 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
-       import { Select as SelectPrimitive } from 'bits-ui';
        import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
        import { cn, type WithoutChild } from '$lib/components/ui/utils.js';
+       import { Select as SelectPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                size = 'default',
                variant = 'default',
                ...restProps
index 00307fdcae720fef6ca3c8a20e5521280e2d2563..343770f5be814a9d60edc355f40fbf11dad2222b 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Separator as SeparatorPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Separator as SeparatorPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: SeparatorPrimitive.RootProps = $props();
 </script>
index 139e2d253429f93414c7678ae3cc2f42d676fa98..cfb0178d9fa1e288e20ff41ff4cc2947399990ef 100644 (file)
@@ -1,12 +1,12 @@
-import { Dialog as SheetPrimitive } from 'bits-ui';
-import Trigger from './sheet-trigger.svelte';
 import Close from './sheet-close.svelte';
-import Overlay from './sheet-overlay.svelte';
 import Content from './sheet-content.svelte';
-import Header from './sheet-header.svelte';
+import Description from './sheet-description.svelte';
 import Footer from './sheet-footer.svelte';
+import Header from './sheet-header.svelte';
+import Overlay from './sheet-overlay.svelte';
 import Title from './sheet-title.svelte';
-import Description from './sheet-description.svelte';
+import Trigger from './sheet-trigger.svelte';
+import { Dialog as SheetPrimitive } from 'bits-ui';
 
 const Root = SheetPrimitive.Root;
 const Portal = SheetPrimitive.Portal;
index b616c469a9188123ee8add5889800338f0d2d407..292fe0e21019033742f220b53af3def8f6470f52 100644 (file)
@@ -2,18 +2,18 @@
        import { tv, type VariantProps } from 'tailwind-variants';
        export const sheetVariants = tv({
                base: `border-border/30 dark:border-border/20 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fill-mode-forwards fixed z-50 flex flex-col gap-4 shadow-sm transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 ${PANEL_CLASSES}`,
+               defaultVariants: {
+                       side: 'right'
+               },
                variants: {
                        side: {
-                               top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
                                bottom:
                                        'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
                                left: 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
                                right:
-                                       'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm'
+                                       'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
+                               top: 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b'
                        }
-               },
-               defaultVariants: {
-                       side: 'right'
                }
        });
 
 </script>
 
 <script lang="ts">
-       import { Dialog as SheetPrimitive } from 'bits-ui';
-       import XIcon from '@lucide/svelte/icons/x';
-       import type { Snippet } from 'svelte';
        import SheetOverlay from './sheet-overlay.svelte';
+       import XIcon from '@lucide/svelte/icons/x';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
        import { PANEL_CLASSES } from '$lib/constants';
+       import { Dialog as SheetPrimitive } from 'bits-ui';
+       import type { Snippet } from 'svelte';
 
        let {
-               ref = $bindable(null),
+               children,
                class: className,
-               side = 'right',
                portalProps,
-               children,
+               ref = $bindable(null),
+               side = 'right',
                ...restProps
        }: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
                portalProps?: SheetPrimitive.PortalProps;
index ef4d58f227f31c6bda2c468deff9400175b00b29..a39ea3d0455ac5e9202ae45aa36271c2eef988f1 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as SheetPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Dialog as SheetPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: SheetPrimitive.DescriptionProps = $props();
 </script>
index 4e1b927a5c21f065b2397a806c50a1bb02b8b0fd..420a8fdd7dace1db1a15c0321bc09fc1cb855445 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index 6c6c1ec9d4ab3d57d0cb0d007195c6dc71ef56b0..3f3dd3c2c1da8c19ef658680c2b26f360a97e99b 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
 </script>
index f402d81aa6f7f0f1f068d19e7d8ec4b750167133..ddfae7592f1f6472c0f0924071ab0db242d703dc 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as SheetPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Dialog as SheetPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: SheetPrimitive.OverlayProps = $props();
 </script>
index 0efcc7a4fd71c44a43797d3699c2acaaa3519f89..38feaad8ffc13f3d21a254d739654ee7c8a8bf5a 100644 (file)
@@ -1,10 +1,10 @@
 <script lang="ts">
-       import { Dialog as SheetPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Dialog as SheetPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: SheetPrimitive.TitleProps = $props();
 </script>
index 62b6f80dfa71bf43e78032dd9984e49ac7985272..46d38a3d54500b46e5345c3e04f2553fcbb826e0 100644 (file)
@@ -3,8 +3,8 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
                class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
 </script>
index e0848790d3c15cd04f8ccecdd53c96a4c74b2431..9d9698f7d95a2d6d6fd808413d48ba0197741300 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import { Switch as SwitchPrimitive } from 'bits-ui';
        import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
+       import { Switch as SwitchPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
-               class: className,
                checked = $bindable(false),
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
 </script>
index f8df65cf689b3f501951cf66f00e27276963c1b4..7b16bc7a0a6a84e2c82ba70cb6e546dcb0f7da50 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
 </script>
index 0fdcc6439c1b32a66090b82801dc2dccc586a9d5..f10282f9b16c5772f395898bb53f159f5805c6a0 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
 </script>
index 4506fdfc5bc3ce62d259d935604ef349f2722a05..5c90e223ba4644a550ef8eadd9468f3938966524 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLTdAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLTdAttributes> = $props();
 </script>
index 77e4a64c08b233f680f608d7b957f2774469de4d..880a5297a71e091830605dc0bde5064c8c7084b9 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
 </script>
index c1c57ad44349534f482cae72ac0b808aade7a8c8..1740812360067f585c6ff79f1234ff8de1669415 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLThAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLThAttributes> = $props();
 </script>
index eb366739b39e3543d920512ed6f5a172e4e6046c..3ffbb563eb235ad48bf3b25febd64b784ce4c83a 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
 </script>
index 4131d3660a4c78f72728f3f428bcde2144fc1a11..51b24b8e3fb318afcb7f347e0f55941a6912d950 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
 </script>
index c11a6a6c4ba62e7996758eb6bf7db82e3f64f43e..19f2644985ab1732392cbca214fda1ce072e047c 100644 (file)
@@ -1,11 +1,11 @@
 <script lang="ts">
-       import type { HTMLTableAttributes } from 'svelte/elements';
        import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
+       import type { HTMLTableAttributes } from 'svelte/elements';
 
        let {
-               ref = $bindable(null),
-               class: className,
                children,
+               class: className,
+               ref = $bindable(null),
                ...restProps
        }: WithElementRef<HTMLTableAttributes> = $props();
 </script>
index bf838829c02d6fb5c2167ad06d821f1d97e433c3..346153ee5088d4c23e425f6140cca02cc103b12c 100644 (file)
@@ -3,9 +3,9 @@
        import type { HTMLTextareaAttributes } from 'svelte/elements';
 
        let {
+               class: className,
                ref = $bindable(null),
                value = $bindable(),
-               class: className,
                ...restProps
        }: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
 </script>
index 273d831e6e8ffc152005d356d7886dcd179b9ead..48177b6affc35fa75f1417ea9bb79f3bc454b236 100644 (file)
@@ -1,6 +1,6 @@
-import { Tooltip as TooltipPrimitive } from 'bits-ui';
-import Trigger from './tooltip-trigger.svelte';
 import Content from './tooltip-content.svelte';
+import Trigger from './tooltip-trigger.svelte';
+import { Tooltip as TooltipPrimitive } from 'bits-ui';
 
 const Root = TooltipPrimitive.Root;
 const Provider = TooltipPrimitive.Provider;
index 0b173ee7c15a40f2c128b0798f651d79f3248cca..19d90d637441911bebc70c06af4a5ee995c2f933 100644 (file)
@@ -1,15 +1,15 @@
 <script lang="ts">
-       import { Tooltip as TooltipPrimitive } from 'bits-ui';
        import { cn } from '$lib/components/ui/utils.js';
+       import { Tooltip as TooltipPrimitive } from 'bits-ui';
 
        let {
-               ref = $bindable(null),
-               class: className,
-               sideOffset = 4,
-               side = 'top',
-               children,
                arrowClasses,
+               children,
+               class: className,
                noPortal = false,
+               ref = $bindable(null),
+               side = 'top',
+               sideOffset = 4,
                ...restProps
        }: TooltipPrimitive.ContentProps & {
                arrowClasses?: string;
index f92bfcbb3f6aaa73e14c2ab1c2648769876f0927..97525cc4290f825902bdace33634d03b581015ad 100644 (file)
@@ -1,4 +1,4 @@
-import { clsx, type ClassValue } from 'clsx';
+import { type ClassValue, clsx } from 'clsx';
 import { twMerge } from 'tailwind-merge';
 
 export function cn(...inputs: ClassValue[]) {
index e63d5c259a3726be7b3bfdce931472d59fe7c61c..582572a29afca1af4e4fdeefecdbd67ac2e0817b 100644 (file)
@@ -34,8 +34,8 @@ export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
 } as const;
 
 export const REASONING_TAGS = {
-       START: '<think>',
-       END: '</think>'
+       END: '</think>',
+       START: '<think>'
 } as const;
 
 /**
@@ -43,12 +43,12 @@ export const REASONING_TAGS = {
  * New messages use structured fields (reasoningContent, toolCalls, toolCallId).
  */
 export const LEGACY_AGENTIC_TAGS = {
-       TOOL_CALL_START: '<<<AGENTIC_TOOL_CALL_START>>>',
-       TOOL_CALL_END: '<<<AGENTIC_TOOL_CALL_END>>>',
-       TOOL_NAME_PREFIX: '<<<TOOL_NAME:',
-       TOOL_ARGS_START: '<<<TOOL_ARGS_START>>>',
+       TAG_SUFFIX: '>>>',
        TOOL_ARGS_END: '<<<TOOL_ARGS_END>>>',
-       TAG_SUFFIX: '>>>'
+       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;
 
 /**
@@ -56,20 +56,20 @@ export const LEGACY_AGENTIC_TAGS = {
  * New messages use the dedicated reasoningContent field.
  */
 export const LEGACY_REASONING_TAGS = {
-       START: '<<<reasoning_content_start>>>',
-       END: '<<<reasoning_content_end>>>'
+       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]*$/,
-       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]*$/,
-       HAS_LEGACY_MARKERS: /<<<(?:AGENTIC_TOOL_CALL_START|reasoning_content_start)>>>/
+       REASONING_OPEN: /<<<reasoning_content_start>>>[\s\S]*$/
 } as const;
index ab35708a46da64b0ddb0abf6218f2a94bf07500a..74f1c7302c92c5c9b023e768871823e68c36a83c 100644 (file)
@@ -1,8 +1,8 @@
 export const API_MODELS = {
        LIST: '/v1/models',
        LOAD: '/models/load',
-       UNLOAD: '/models/unload',
-       SSE: '/models/sse'
+       SSE: '/models/sse',
+       UNLOAD: '/models/unload'
 };
 
 // chat completion routes, the control route drives realtime inference (e.g. end reasoning)
@@ -17,8 +17,8 @@ export const API_SLOTS = {
 };
 
 export const API_TOOLS = {
-       LIST: '/tools',
-       EXECUTE: '/tools'
+       EXECUTE: '/tools',
+       LIST: '/tools'
 };
 
 // resumable stream routes, the conv::model identity travels as the conv_id query param
index 3d7381812e5d04bce2c799949350f0f8aca068c3..1cd7f9ba5ae09ad270cb731b0cb59e2c07beb775 100644 (file)
@@ -1,5 +1,4 @@
-import type { Component } from 'svelte';
-import { MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
+import { FolderOpen, MessageSquare, Zap } from '@lucide/svelte';
 import { FILE_TYPE_ICONS } from '$lib/constants/icons';
 import {
        AttachmentAction,
@@ -7,6 +6,7 @@ import {
        AttachmentItemVisibleWhen,
        AttachmentMenuItemId
 } from '$lib/enums';
+import type { Component } from 'svelte';
 
 export interface AttachmentMenuItem {
        /** Unique identifier for the item */
@@ -35,47 +35,47 @@ export interface AttachmentMenuItem {
  */
 export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [
        {
-               id: AttachmentMenuItemId.IMAGES,
-               label: 'Images',
-               icon: FILE_TYPE_ICONS.image,
+               action: AttachmentAction.FILE_UPLOAD,
                class: 'images-button',
-               enabledWhen: AttachmentItemEnabledWhen.HAS_VISION_MODALITY,
                disabledTooltip: 'Image processing requires a vision model',
-               action: AttachmentAction.FILE_UPLOAD
+               enabledWhen: AttachmentItemEnabledWhen.HAS_VISION_MODALITY,
+               icon: FILE_TYPE_ICONS.image,
+               id: AttachmentMenuItemId.IMAGES,
+               label: 'Images'
        },
        {
-               id: AttachmentMenuItemId.AUDIO,
-               label: 'Audio Files',
-               icon: FILE_TYPE_ICONS.audio,
+               action: AttachmentAction.FILE_UPLOAD,
                class: 'audio-button',
-               enabledWhen: AttachmentItemEnabledWhen.HAS_AUDIO_MODALITY,
                disabledTooltip: 'Audio files processing requires an audio model',
-               action: AttachmentAction.FILE_UPLOAD
+               enabledWhen: AttachmentItemEnabledWhen.HAS_AUDIO_MODALITY,
+               icon: FILE_TYPE_ICONS.audio,
+               id: AttachmentMenuItemId.AUDIO,
+               label: 'Audio Files'
        },
        {
-               id: AttachmentMenuItemId.VIDEO,
-               label: 'Video Files',
-               icon: FILE_TYPE_ICONS.video,
+               action: AttachmentAction.FILE_UPLOAD,
                class: 'video-button',
-               enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
                disabledTooltip: 'Video files processing requires a video model',
-               action: AttachmentAction.FILE_UPLOAD
+               enabledWhen: AttachmentItemEnabledWhen.HAS_VIDEO_MODALITY,
+               icon: FILE_TYPE_ICONS.video,
+               id: AttachmentMenuItemId.VIDEO,
+               label: 'Video Files'
        },
        {
-               id: AttachmentMenuItemId.TEXT,
-               label: 'Text Files',
-               icon: FILE_TYPE_ICONS.text,
+               action: AttachmentAction.FILE_UPLOAD,
                enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
-               action: AttachmentAction.FILE_UPLOAD
+               icon: FILE_TYPE_ICONS.text,
+               id: AttachmentMenuItemId.TEXT,
+               label: 'Text Files'
        },
        {
-               id: AttachmentMenuItemId.PDF,
-               label: 'PDF Files',
-               icon: FILE_TYPE_ICONS.pdf,
-               enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
+               action: AttachmentAction.FILE_UPLOAD,
                disabledTooltip: 'PDFs will be converted to text. Image-based PDFs may not work properly.',
+               enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
                hasEnabledTooltip: true,
-               action: AttachmentAction.FILE_UPLOAD
+               icon: FILE_TYPE_ICONS.pdf,
+               id: AttachmentMenuItemId.PDF,
+               label: 'PDF Files'
        }
 ];
 
@@ -83,30 +83,30 @@ export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = [];
 
 export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [
        {
-               id: AttachmentMenuItemId.SYSTEM_MESSAGE,
-               label: 'System Message',
-               icon: MessageSquare,
+               action: AttachmentAction.SYSTEM_PROMPT_CLICK,
                enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
                hasEnabledTooltip: true,
-               action: AttachmentAction.SYSTEM_PROMPT_CLICK
+               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',
-               icon: Zap,
-               enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
-               action: AttachmentAction.MCP_PROMPT_CLICK,
                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',
-               icon: FolderOpen,
-               enabledWhen: AttachmentItemEnabledWhen.ALWAYS,
-               action: AttachmentAction.MCP_RESOURCES_CLICK,
                visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_RESOURCES_SUPPORT
        }
 ];
index 21a95cc88371759785c856564226d4ce0daf32f8..69bd4d48e3711e59498094cb8e85ad6c0f302fa2 100644 (file)
@@ -1,7 +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
-       maxAbsoluteNullBytes: 2
+       suspiciousCharThresholdRatio: 0.15 // Allow up to 15% suspicious chars
 };
index a6c4981d0480f405bff2f13e1d55ae9d38fb4025..89bcbecb8ae45bab63d11e04724f84b83b79c97a 100644 (file)
@@ -7,7 +7,6 @@
 // (see ChatMessageToolCallBlockGetDatetime and
 // ChatMessageToolCallBlockSearchResults for prior art).
 
-import type { Component } from 'svelte';
 import {
        Braces,
        Clock,
@@ -20,6 +19,7 @@ import {
        Terminal
 } from '@lucide/svelte';
 import { BuiltInTool, ToolSource } from '$lib/enums';
+import type { Component } from 'svelte';
 
 export interface BuiltinToolUiEntry {
        icon: Component;
@@ -28,34 +28,35 @@ export interface BuiltinToolUiEntry {
 }
 
 export const BUILTIN_TOOL_UI: Readonly<Record<BuiltInTool, BuiltinToolUiEntry>> = {
-       [BuiltInTool.READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.BUILTIN },
        [BuiltInTool.EDIT_FILE]: { icon: FilePen, label: 'Edit file', source: ToolSource.BUILTIN },
-       [BuiltInTool.WRITE_FILE]: { icon: FilePlus, label: 'Write 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.GET_DATETIME]: { icon: Clock, label: 'Current time', source: ToolSource.BUILTIN },
-       [BuiltInTool.GET_INFO]: { icon: Info, label: 'Runtime info', source: ToolSource.BUILTIN },
-       [BuiltInTool.EXEC_SHELL_COMMAND]: {
-               icon: Terminal,
-               label: 'Run command',
-               source: ToolSource.BUILTIN
-       },
+       [BuiltInTool.READ_FILE]: { icon: FileText, label: 'Read file', source: ToolSource.BUILTIN },
        [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;
 }
index 3cc09fe6d73a2f1c97a1a67c37fee7099180b8c4..c34c635fd735d99192fad2621130035d5eb13574 100644 (file)
@@ -22,23 +22,23 @@ interface ChatCommandsOptions {
 export function getChatCommands(options: ChatCommandsOptions): ChatFormCommand[] {
        return [
                {
-                       name: 'prompt',
-                       description: 'Insert an MCP prompt',
                        action: ChatFormCommandAction.PROMPT,
-                       disabled: !options.hasPrompts()
+                       description: 'Insert an MCP prompt',
+                       disabled: !options.hasPrompts(),
+                       name: 'prompt'
                },
                {
-                       name: 'cwd',
+                       action: ChatFormCommandAction.CWD,
                        description: SET_WORKING_DIRECTORY_LABEL,
+                       disabled: !options.hasCwdTools(),
                        keywords: ['current working directory'],
-                       action: ChatFormCommandAction.CWD,
-                       disabled: !options.hasCwdTools()
+                       name: 'cwd'
                },
                {
-                       name: 'model',
-                       description: 'Select model',
                        action: ChatFormCommandAction.MODEL,
-                       disabled: !options.showModelSelector
+                       description: 'Select model',
+                       disabled: !options.showModelSelector,
+                       name: 'model'
                }
        ];
 }
index 4339bd25d55e2455c12c1e4248314c96d4b2b4bd..17527fc1ea5b01fa7f799f2d3eb84504f13ef948 100644 (file)
@@ -1,17 +1,17 @@
 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'
-       },
-       HTTP: {
-               GENERIC: 'Request failed',
-               ACCESS_DENIED: 'Access denied',
-               INTERNAL_ERROR: 'Server error - check server logs',
-               NOT_FOUND: 'Not found',
-               TEMPORARILY_UNAVAILABLE: 'Server temporarily unavailable'
        }
 };
 
index 6ef02c4cb7cfd707f80ceddadce91d8d3b3eb623..5563740509796f07b7d0cb563b81bb24306cc253 100644 (file)
@@ -4,35 +4,35 @@
  */
 
 import {
+       Eye as VisionIcon,
        File as FileIcon,
        FileText as FileTextIcon,
        Image as ImageIcon,
-       Eye as VisionIcon,
        Mic as AudioIcon,
        Video as VideoIcon
 } from '@lucide/svelte';
 import { FileTypeCategory, ModelModality } from '$lib/enums';
 
 export const FILE_TYPE_ICONS = {
-       [FileTypeCategory.IMAGE]: ImageIcon,
        [FileTypeCategory.AUDIO]: AudioIcon,
-       [FileTypeCategory.VIDEO]: VideoIcon,
+       [FileTypeCategory.IMAGE]: ImageIcon,
+       [FileTypeCategory.PDF]: FileIcon,
        [FileTypeCategory.TEXT]: FileTextIcon,
-       [FileTypeCategory.PDF]: FileIcon
+       [FileTypeCategory.VIDEO]: VideoIcon
 } as const;
 
 export const DEFAULT_FILE_ICON = FileIcon;
 
 export const MODALITY_ICONS = {
-       [ModelModality.VISION]: VisionIcon,
        [ModelModality.AUDIO]: AudioIcon,
-       [ModelModality.VIDEO]: VideoIcon
+       [ModelModality.VIDEO]: VideoIcon,
+       [ModelModality.VISION]: VisionIcon
 } as const;
 
 export const MODALITY_LABELS = {
-       [ModelModality.VISION]: 'Vision',
        [ModelModality.AUDIO]: 'Audio',
-       [ModelModality.VIDEO]: 'Video'
+       [ModelModality.VIDEO]: 'Video',
+       [ModelModality.VISION]: 'Vision'
 } as const;
 
 // Shared SVG icon strings for copy and preview buttons
index 44419012d16fa2ae67745edcc1c8caf580c54e70..0ef8d9ec13b6ed2528009d882af75b7294727f8a 100644 (file)
@@ -47,9 +47,9 @@ export const BINARY_CONTENT_LABEL = 'Binary content';
  * 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.GIF]: 'gif',
        [MimeTypeImage.WEBP]: 'webp'
 } as const;
index f4979564d2510a34fdebee0e8ce0780263fd169d..854cbf9f76aec25ad1e37a816fe3475dff301ba5 100644 (file)
@@ -1,8 +1,8 @@
-import { Zap, Globe, Radio } from '@lucide/svelte';
+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';
-import { MimeTypeImage } from '$lib/enums/files.enums';
 
 export const DEFAULT_CLIENT_VERSION = '1.0.0';
 export const MCP_CLIENT_NAME = 'llama-ui-mcp';
@@ -27,11 +27,11 @@ export const MCP_ALLOWED_ICON_MIME_TYPES = new Set([
 export const MCP_PROTOCOL_VERSION = '2025-06-18';
 
 export const DEFAULT_MCP_CONFIG = {
-       protocolVersion: MCP_PROTOCOL_VERSION,
        capabilities: { tools: { listChanged: true } } as ClientCapabilities,
        clientInfo: { name: MCP_CLIENT_NAME, version: DEFAULT_CLIENT_VERSION } as Implementation,
-       requestTimeoutSeconds: 300, // 5 minutes for long-running tools
-       connectionTimeoutMs: 10_000 // 10 seconds for connection establishment
+       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';
@@ -85,16 +85,16 @@ export const REDACTED_HEADERS = new Set([
 
 /** Human-readable labels for MCP transport types */
 export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
-       [MCPTransportType.WEBSOCKET]: 'WebSocket',
+       [MCPTransportType.SSE]: 'SSE',
        [MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
-       [MCPTransportType.SSE]: 'SSE'
+       [MCPTransportType.WEBSOCKET]: 'WebSocket'
 };
 
 /** Icon components for MCP transport types */
 export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
-       [MCPTransportType.WEBSOCKET]: Zap,
+       [MCPTransportType.SSE]: Radio,
        [MCPTransportType.STREAMABLE_HTTP]: Globe,
-       [MCPTransportType.SSE]: Radio
+       [MCPTransportType.WEBSOCKET]: Zap
 };
 
 /** Standard SSE endpoint path indicators */
index d60727ba25a38fd42d7551dc5168312bd138a832..5de8270140a7d774ecc5a258019477d70c1de00b 100644 (file)
@@ -17,14 +17,14 @@ export const MENTION_BADGE_ICON_CLASSNAME = 'h-3 w-3 shrink-0';
  * valid for both.
  */
 export const MENTION_BADGE_SVG_ATTRIBUTES: Readonly<Record<string, string>> = {
-       xmlns: 'http://www.w3.org/2000/svg',
-       viewBox: '0 0 24 24',
+       'aria-hidden': 'true',
        fill: 'none',
        stroke: 'currentColor',
-       'stroke-width': '2',
        'stroke-linecap': 'round',
        'stroke-linejoin': 'round',
-       'aria-hidden': 'true'
+       'stroke-width': '2',
+       viewBox: '0 0 24 24',
+       xmlns: 'http://www.w3.org/2000/svg'
 };
 
 /**
index a55ba708b1cd9a3c3ed5737bee5f7867071fb872..0d0ca3263294cb9d3193baca32dfb2ff5cf67c76 100644 (file)
@@ -2,9 +2,9 @@
  * Labels shown while a model loads, keyed by the stage reported on /models/sse.
  */
 export const MODEL_LOAD_STAGE_LABELS: Record<ApiModelLoadStage, string> = {
-       text_model: 'Loading weights',
+       mmproj_model: 'Loading projector',
        spec_model: 'Loading draft',
-       mmproj_model: 'Loading projector'
+       text_model: 'Loading weights'
 };
 
 /**
index 343bcaf3cd414840f5e77a0ed1804d7335c24b8c..4da37c06bcfeb272ec6c0ee38070679115679d36 100644 (file)
@@ -6,40 +6,40 @@
 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)',
-       DISPLAY_MODE_STANDALONE: '(display-mode: standalone)'
+       PREFERS_LIGHT: '(prefers-color-scheme: light)'
 } as const;
 
 export const THEME_COLORS = {
-       LIGHT: '#ffffff',
-       DARK: '#0d0d0d',
        ACCENT_BLUE: '#2563eb',
        ACCENT_BLUE_HOVER: '#1d4ed8',
-       BACKGROUND_LIGHT: 'white',
        BACKGROUND_DARK: '#111111',
+       BACKGROUND_LIGHT: 'white',
+       DARK: '#0d0d0d',
+       LIGHT: '#ffffff',
        TITLE_UPDATE_ALERT: {
-               BORDER_LIGHT: 'zinc-200',
-               BORDER_DARK: 'zinc-700',
-               BG_LIGHT: 'white',
                BG_DARK: 'zinc-800',
-               TEXT_LIGHT: 'zinc-500',
-               TEXT_DARK: 'zinc-400'
+               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_LIGHT: 'favicon.ico',
        ICO_DARK: 'favicon-dark.ico',
-       SVG_LIGHT: 'favicon.svg',
-       SVG_DARK: 'favicon-dark.svg'
+       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 = {
-       LIGHT: '#111111',
-       DARK: '#fafafa'
+       DARK: '#fafafa',
+       LIGHT: '#111111'
 } as const;
 
 export const FAVICON_SELECTORS = {
@@ -52,50 +52,50 @@ export const APPLE_ASSETS = {
 } as const;
 
 export const PWA_MANIFEST = {
-       name: APP_NAME,
-       short_name: APP_NAME,
+       background_color: THEME_COLORS.BACKGROUND_LIGHT,
        description: 'Local AI chat interface powered by llama.cpp',
-       start_url: './',
        display: 'standalone' as const,
-       background_color: THEME_COLORS.BACKGROUND_LIGHT,
-       theme_color: THEME_COLORS.BACKGROUND_LIGHT,
        icons: [
-               { src: 'pwa-64x64.png', sizes: '64x64', type: 'image/png' },
-               { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
-               { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' as const },
+               { 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' },
                {
-                       src: 'maskable-icon-512x512.png',
+                       purpose: 'maskable' as const,
                        sizes: '512x512',
-                       type: 'image/png',
-                       purpose: 'maskable' as const
+                       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',
-       MASKABLE_512: '/maskable-icon-512x512.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': { width: 390, height: 844, dpr: 3 }, // iPhone 13, 15
-       '1179x2556': { width: 393, height: 852, dpr: 3 }, // iPhone 14, 15 Pro, 16
-       '1206x2622': { width: 402, height: 874, dpr: 3 }, // iPhone 16 Plus, 16e
-       '1284x2778': { width: 428, height: 926, dpr: 3 }, // iPhone 15 Plus
-       '1290x2796': { width: 430, height: 932, dpr: 3 }, // iPhone 15 Pro Max, 16 Pro
-       '1320x2868': { width: 440, height: 956, dpr: 3 }, // iPhone 16 Pro Max
-       '750x1334': { width: 375, height: 667, dpr: 2 }, // iPhone 6/7/8, 14
-       '640x1136': { width: 320, height: 568, dpr: 2 }, // iPhone 6/7/8 Plus
+       '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': { width: 834, height: 1194, dpr: 2 }, // iPad Air 11", iPad 11"
-       '2048x2732': { width: 1024, height: 1366, dpr: 2 }, // iPad Pro 12.9"
-       '1640x2360': { width: 820, height: 1180, dpr: 2 }, // iPad Air 10.9"
-       '1032x1376': { width: 1032, height: 1376, dpr: 2 }, // iPad Air 13"
-       '744x1133': { width: 376, height: 573, dpr: 2 } // iPad mini 8.3"
+       '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;
@@ -183,7 +183,6 @@ export const PUBLIC_ENDPOINTS = [
        '/workbox-<hash>.js'
 ] as const;
 export const BUILD_CONFIG = {
-       OUTPUT_DIR: './dist',
        GUIDE_COMMENT: `
 <!--
   This is a static build of the frontend.
@@ -191,12 +190,13 @@ export const BUILD_CONFIG = {
   Do not edit this file directly.
   To make changes, refer to the "Web UI" section in the README.
 -->
-`.trim()
+`.trim(),
+       OUTPUT_DIR: './dist'
 } as const;
 
 export const REGEX_PATTERNS = {
-       SPLASH_FILE: /^apple-splash-(portrait|landscape)-(dark-)?(\d+)x(\d+)\.png$/,
-       HEAD_CLOSE: /\t*<\/head>/
+       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.
@@ -235,22 +235,22 @@ export const PWA_GENERATOR_DEVICES = [
 // 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 = {
-       LINK_PRESET: '2023',
-       FAVICON_PADDING: 0.04,
-       SPLASH_PADDING: 0.75,
-       FIT_MODE: 'contain',
        ADD_MEDIA_SCREEN: true,
        BASE_PATH: './',
-       XHTML: false,
+       DARK_PREFIX: 'dark-',
+       FAVICON_PADDING: 0.04,
+       FIT_MODE: 'contain',
+       LINK_PRESET: '2023',
        PNG_COMPRESSION_LEVEL: 9,
        PNG_QUALITY: 60,
-       DARK_PREFIX: 'dark-'
+       SPLASH_PADDING: 0.75,
+       XHTML: false
 } as const;
 
 export const CACHE_SETTINGS = {
-       IMMUTABLE_MAX_AGE_SECONDS: 31536000,
        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;
 
@@ -271,35 +271,46 @@ export const SW_CONFIG = {
 
 // Runtime caching configuration for Workbox
 export const RUNTIME_CACHING = {
-       HANDLER: 'NetworkFirst',
-       CACHE_NAME: 'api-cache'
+       CACHE_NAME: 'api-cache',
+       HANDLER: 'NetworkFirst'
 } as const;
 
 // Workbox runtime caching patterns
 export const API_CACHING_PATTERNS = {
-       V1_API: /^\/v1\/.*/,
-       STATIC_API: /^\/(health|props|models|tools|slots|cors-proxy).*/
+       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: { name: 'apple-mobile-web-app-capable', content: 'yes' },
-       STATUS_BAR_STYLE: { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
-       MOBILE_WEB_APP_TITLE: { name: 'apple-mobile-web-app-title' }
+       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 = {
-       HTML: '<link rel="apple-touch-startup-image"',
-       DARK_MEDIA_SUFFIX: ' and (prefers-color-scheme: dark)'
+       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
@@ -324,38 +335,27 @@ export const SVELTEKIT_PWA_OPTIONS: SvelteKitPWAOptions = {
                // Runtime caching for API calls - use NetworkFirst so APIs are always fresh
                runtimeCaching: [
                        {
-                               urlPattern: API_CACHING_PATTERNS.V1_API,
                                handler: RUNTIME_CACHING.HANDLER,
                                options: {
                                        cacheName: RUNTIME_CACHING.CACHE_NAME,
                                        expiration: {
-                                               maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES,
-                                               maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS
+                                               maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
+                                               maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
                                        }
-                               }
+                               },
+                               urlPattern: API_CACHING_PATTERNS.V1_API
                        },
                        {
-                               urlPattern: API_CACHING_PATTERNS.STATIC_API,
                                handler: RUNTIME_CACHING.HANDLER,
                                options: {
                                        cacheName: RUNTIME_CACHING.CACHE_NAME,
                                        expiration: {
-                                               maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES,
-                                               maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS
+                                               maxAgeSeconds: CACHE_SETTINGS.API_CACHE_MAX_AGE_SECONDS,
+                                               maxEntries: CACHE_SETTINGS.API_CACHE_MAX_ENTRIES
                                        }
-                               }
+                               },
+                               urlPattern: API_CACHING_PATTERNS.STATIC_API
                        }
                ]
-       },
-
-       devOptions: {
-               enabled: true,
-               suppressWarnings: true
-       },
-
-       // SvelteKit-specific options
-       kit: {
-               // Include version file for proper cache invalidation
-               includeVersionFile: true
        }
 };
index 059af71dea008fe1510bfe46dc45c54c764ca719..a0c244e5e9d3b405f552a03309f24eed702e1f54 100644 (file)
@@ -5,8 +5,8 @@ import { ReasoningEffort } from '$lib/enums';
  * Maps the ReasoningEffort enum values to concrete token counts for the server.
  */
 export const REASONING_EFFORT_TOKENS: Record<string, number> = {
-       [ReasoningEffort.LOW]: 512,
-       [ReasoningEffort.MEDIUM]: 2048,
        [ReasoningEffort.HIGH]: 8192,
-       [ReasoningEffort.MAX]: -1 // unlimited
+       [ReasoningEffort.LOW]: 512,
+       [ReasoningEffort.MAX]: -1, // unlimited
+       [ReasoningEffort.MEDIUM]: 2048
 };
index f21ea588ad78d967d0aceb3d6f7ca3a35dbad31a..4cbb7388ba19ccfb85d5fea141e8adece36b5f7b 100644 (file)
@@ -7,18 +7,18 @@ import type { ReasoningEffortLevel } from '$lib/types';
  */
 export const REASONING_EFFORT_LABELS: Record<string, string> = {
        [ReasoningEffort.DEFAULT]: 'Default',
-       [ReasoningEffort.OFF]: 'Off',
+       [ReasoningEffort.HIGH]: 'High',
        [ReasoningEffort.LOW]: 'Low',
+       [ReasoningEffort.MAX]: 'Max',
        [ReasoningEffort.MEDIUM]: 'Medium',
-       [ReasoningEffort.HIGH]: 'High',
-       [ReasoningEffort.MAX]: 'Max'
+       [ReasoningEffort.OFF]: 'Off'
 };
 
 export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [
-       { value: ReasoningEffort.DEFAULT, label: 'Default' },
-       { value: ReasoningEffort.OFF, label: 'Off' },
-       { value: ReasoningEffort.LOW, label: 'Low' },
-       { value: ReasoningEffort.MEDIUM, label: 'Medium' },
-       { value: ReasoningEffort.HIGH, label: 'High' },
-       { value: ReasoningEffort.MAX, label: 'Max', hasInfo: true }
+       { 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 }
 ];
index f8ca18cfe0c11ca7f94e987f94a98227b679e85c..6a550ee9693064711fe18cdfc21efd44e9e27289 100644 (file)
@@ -6,33 +6,33 @@ import type { RecommendedMCPServer } from '$lib/types';
 // 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',
-               description: 'Search the web and fetch full page content as clean markdown.',
-               url: 'https://mcp.exa.ai/mcp',
-               iconUrl: '/recommended-mcp/exa.ico'
+               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',
-               description: 'Search and browse AI models, datasets, spaces, and docs on the Hugging Face Hub.',
-               url: 'https://huggingface.co/mcp',
-               iconUrl: '/recommended-mcp/huggingface.ico'
+               url: 'https://huggingface.co/mcp'
        },
        {
-               id: 'github',
-               name: 'GitHub',
                description: 'Search repositories, issues, pull requests and interact with code on GitHub.',
-               url: 'https://api.githubcopilot.com/mcp',
-               iconUrlLight: '/recommended-mcp/github-light.png',
                iconUrlDark: '/recommended-mcp/github-dark.png',
-               needsAuthorization: true
+               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',
-               description: 'Browse up-to-date documentation and code examples for libraries and frameworks.',
-               url: 'https://mcp.context7.com/mcp',
-               iconUrl: '/recommended-mcp/context7.png'
+               url: 'https://mcp.context7.com/mcp'
        }
 ];
index 26c7d9a293a648ba5b189f953d9f5a40ec0255dc..84b0f5300bbd8b7c38c0ae4353b6254e95a7b0ad 100644 (file)
@@ -1,38 +1,38 @@
 /** Query params the chat routes read from the URL. */
 export const URL_PARAMS = {
-       /** Prompt to send on arrival. */
-       QUERY: 'q',
-       /** Model to select. */
-       MODEL: 'model',
        /** 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'
+       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 = {
-       GENERAL: 'general',
-       DISPLAY: 'display',
-       SAMPLING: 'sampling',
-       PENALTIES: 'penalties',
        AGENTIC: 'agentic',
        DEVELOPER: 'developer',
-       TOOLS: 'tools',
-       IMPORT_EXPORT: 'import-export'
+       DISPLAY: 'display',
+       GENERAL: 'general',
+       IMPORT_EXPORT: 'import-export',
+       PENALTIES: 'penalties',
+       SAMPLING: 'sampling',
+       TOOLS: 'tools'
 } as const;
 
 export const ROUTES = {
-       /** Root — start of the app. */
-       START: '#/',
-       /** New chat — root with new chat query param. */
-       NEW_CHAT: `?${URL_PARAMS.NEW_CHAT}=true#/`,
        /** 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',
-       /** Search — mobile-only full-page conversation search. */
-       SEARCH: '#/search'
+       /** Root — start of the app. */
+       START: '#/'
 } as const;
index 381621de647e27a317a97154fb1544a82039649c..11e5fcf8fca513669fe7ef1321f532465bdf440d 100644 (file)
@@ -31,27 +31,27 @@ IMPORTANT:Identifier 'nerdamer' has already been declared, use it directly`;
  */
 export function buildSandboxToolDefinition(includeSymbolicMath: boolean): OpenAIToolDefinition {
        return {
-               type: ToolCallType.FUNCTION,
                function: {
-                       name: SANDBOX_TOOL_NAME,
                        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: {
-                               type: JsonSchemaType.OBJECT,
                                properties: {
                                        code: {
-                                               type: JsonSchemaType.STRING,
-                                               description: 'JavaScript source to execute'
+                                               description: 'JavaScript source to execute',
+                                               type: JsonSchemaType.STRING
                                        },
                                        timeout_ms: {
-                                               type: JsonSchemaType.NUMBER,
-                                               description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}`
+                                               description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}`,
+                                               type: JsonSchemaType.NUMBER
                                        }
                                },
-                               required: ['code']
+                               required: ['code'],
+                               type: JsonSchemaType.OBJECT
                        }
-               }
+               },
+               type: ToolCallType.FUNCTION
        };
 }
 
index c0b5b002ccb3e87cf63685df6b005343b00c70ad..8388f2b759e05e47e3a2d27e09a374d4b3bcc5c0 100644 (file)
@@ -5,71 +5,71 @@
  * in settings field configurations to ensure consistency.
  */
 export const SETTINGS_KEYS = {
-       // General
-       THEME: 'theme',
+       AGENTIC_MAX_TURNS: 'agenticMaxTurns',
+       ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
+       ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
        API_KEY: 'apiKey',
-       SYSTEM_MESSAGE: 'systemMessage',
-       PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen',
+       AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
+       BACKEND_SAMPLING: 'backend_sampling',
        COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT: 'copyTextAttachmentsAsPlainText',
-       SEND_ON_ENTER: 'sendOnEnter',
+       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',
-       PDF_AS_IMAGE: 'pdfAsImage',
-       TITLE_GENERATION_USE_FIRST_LINE: 'titleGenerationUseFirstLine',
-       TITLE_GENERATION_USE_LLM: 'titleGenerationUseLLM',
-       TITLE_GENERATION_PROMPT: 'titleGenerationPrompt',
+       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_CONTENT_AS_RAW_TEXT: 'renderContentAsRawText',
+       // 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_AGENTIC_TURN_STATS: 'showAgenticTurnStats',
-       SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
-       AUTO_MIC_ON_EMPTY: 'autoMicOnEmpty',
-       RENDER_CONTENT_AS_RAW_TEXT: 'renderContentAsRawText',
-       DISABLE_AUTO_SCROLL: 'disableAutoScroll',
-       ALWAYS_SHOW_SIDEBAR_ON_DESKTOP: 'alwaysShowSidebarOnDesktop',
-       FULL_HEIGHT_CODE_BLOCKS: 'fullHeightCodeBlocks',
-       SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
        SHOW_MODEL_QUANTIZATION: 'showModelQuantization',
        SHOW_MODEL_TAGS: 'showModelTags',
-       SHOW_BUILD_VERSION: 'showBuildVersion',
-       SHOW_FULL_PATH_IN_MENTIONS: 'showFullPathInMentions',
+       SHOW_RAW_MODEL_NAMES: 'showRawModelNames',
+       SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
        SHOW_SYSTEM_MESSAGE: 'showSystemMessage',
-       MENTION_SEARCH_MAX_DEPTH: 'mentionSearchMaxDepth',
+       SHOW_THOUGHT_IN_PROGRESS: 'showThoughtInProgress',
+       SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
+       SYSTEM_MESSAGE: 'systemMessage',
        // Sampling
        TEMPERATURE: 'temperature',
-       DYNATEMP_RANGE: 'dynatemp_range',
-       DYNATEMP_EXPONENT: 'dynatemp_exponent',
+       // 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',
-       MIN_P: 'min_p',
-       XTC_PROBABILITY: 'xtc_probability',
-       XTC_THRESHOLD: 'xtc_threshold',
        TYP_P: 'typ_p',
-       MAX_TOKENS: 'max_tokens',
-       SAMPLERS: 'samplers',
-       BACKEND_SAMPLING: 'backend_sampling',
-       // Penalties
-       REPEAT_LAST_N: 'repeat_last_n',
-       REPEAT_PENALTY: 'repeat_penalty',
-       PRESENCE_PENALTY: 'presence_penalty',
-       FREQUENCY_PENALTY: 'frequency_penalty',
-       DRY_MULTIPLIER: 'dry_multiplier',
-       DRY_BASE: 'dry_base',
-       DRY_ALLOWED_LENGTH: 'dry_allowed_length',
-       DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
-       // MCP
-       MCP_SERVERS: 'mcpServers',
-       MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
-       AGENTIC_MAX_TURNS: 'agenticMaxTurns',
-       ALWAYS_SHOW_TOOL_CALL_CONTENT: 'alwaysShowToolCallContent',
-       // Performance
-       PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
-       // Developer
-       DISABLE_REASONING_PARSING: 'disableReasoningParsing',
-       EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext',
-       SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
-       JS_SANDBOX_ENABLED: 'jsSandboxEnabled',
-       SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled',
-       // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled',
-       CUSTOM_JSON: 'customJson',
-       CUSTOM_CSS: 'customCss'
+       XTC_PROBABILITY: 'xtc_probability',
+       XTC_THRESHOLD: 'xtc_threshold'
 } as const;
index adb2dfb78716aefd8c573d7e433c58b00e67bcd2..37f7c8f310ac58cf491ff4f90d94e1a1eac3d0be 100644 (file)
@@ -1,64 +1,62 @@
-import { ColorMode } from '$lib/enums/ui.enums';
-import { SettingsFieldType } from '$lib/enums/settings.enums';
-import { SyncableParameterType } from '$lib/enums';
+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 {
-       Funnel,
        AlertTriangle,
        Code,
-       Monitor,
-       ListRestart,
-       Sliders,
-       PencilRuler,
        Database,
+       Funnel,
+       ListRestart,
+       Monitor,
        Monitor as MonitorIcon,
-       Sun,
-       Moon
+       Moon,
+       PencilRuler,
+       Sliders,
+       Sun
 } from '@lucide/svelte';
-import type { Component } from 'svelte';
+import { SyncableParameterType } from '$lib/enums';
+import { SettingsFieldType } from '$lib/enums/settings.enums';
+import { ColorMode } from '$lib/enums/ui.enums';
 import type {
        SettingsConfigValue,
-       SyncableParameter,
        SettingsEntry,
-       SettingsSectionTitle,
+       SettingsSection,
        SettingsSectionEntry,
-       SettingsSection
+       SettingsSectionTitle,
+       SyncableParameter
 } from '$lib/types';
-import { CLI_FLAGS } from './cli-flags';
-import { DEFAULT_MCP_CONFIG } from './mcp';
-import {
-       FILE_GLOB_SEARCH_PICKERS_MAX_SEARCH_DEPTH,
-       FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH
-} from './working-directory';
-import { SETTINGS_KEYS } from './settings-keys';
-import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes';
-import { TITLE_GENERATION } from './title-generation';
+import type { Component } from 'svelte';
 
 export const SETTINGS_SECTION_TITLES = {
-       GENERAL: 'General',
-       DISPLAY: 'Display',
-       SAMPLING: 'Sampling',
-       PENALTIES: 'Penalties',
        AGENTIC: 'Agentic',
-       TOOLS: 'Tools',
+       DEVELOPER: 'Developer',
+       DISPLAY: 'Display',
+       GENERAL: 'General',
        IMPORT_EXPORT: 'Import/Export',
-       DEVELOPER: 'Developer'
+       PENALTIES: 'Penalties',
+       SAMPLING: 'Sampling',
+       TOOLS: 'Tools'
 } as const;
 
 const STANDALONE_SECTIONS: { title: SettingsSectionTitle; slug: string; icon: Component }[] = [
-       { title: SETTINGS_SECTION_TITLES.TOOLS, slug: SETTINGS_SECTION_SLUGS.TOOLS, icon: PencilRuler },
+       { icon: PencilRuler, slug: SETTINGS_SECTION_SLUGS.TOOLS, title: SETTINGS_SECTION_TITLES.TOOLS },
        {
-               title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT,
+               icon: Database,
                slug: SETTINGS_SECTION_SLUGS.IMPORT_EXPORT,
-               icon: Database
+               title: SETTINGS_SECTION_TITLES.IMPORT_EXPORT
        }
 ];
-
 const COLOR_MODE_OPTIONS: Array<{ value: string; label: string; icon: Component }> = [
-       { value: ColorMode.SYSTEM, label: 'System', icon: MonitorIcon },
-       { value: ColorMode.LIGHT, label: 'Light', icon: Sun },
-       { value: ColorMode.DARK, label: 'Dark', icon: Moon }
+       { 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<{
@@ -68,607 +66,604 @@ const TITLE_GENERATION_RADIO_OPTIONS: Array<{
        isExperimental?: boolean;
 }> = [
        {
-               value: 'firstLine',
+               key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
                label: 'Use first non-empty line for the conversation title',
-               key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE
+               value: 'firstLine'
        },
        {
-               value: 'llm',
-               label: 'Generate title with LLM',
+               isExperimental: true,
                key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM,
-               isExperimental: true
+               label: 'Generate title with LLM',
+               value: 'llm'
        }
 ];
-
 // Common shape for the conversation title radio entry.
 const TITLE_GENERATION_BASE = {
-       type: SettingsFieldType.RADIO,
+       radioOptions: TITLE_GENERATION_RADIO_OPTIONS,
        section: SETTINGS_SECTION_SLUGS.GENERAL,
-       radioOptions: TITLE_GENERATION_RADIO_OPTIONS
+       type: SettingsFieldType.RADIO
 } as const;
-
 const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
-       [SETTINGS_SECTION_SLUGS.GENERAL]: {
-               title: SETTINGS_SECTION_TITLES.GENERAL,
-               slug: SETTINGS_SECTION_SLUGS.GENERAL,
-               icon: Sliders,
+       [SETTINGS_SECTION_SLUGS.AGENTIC]: {
+               icon: ListRestart,
                settings: [
                        {
-                               key: SETTINGS_KEYS.THEME,
-                               label: 'Theme',
-                               help: 'Choose the color theme for the interface. You can choose between System (follows your device settings), Light, or Dark.',
-                               defaultValue: ColorMode.SYSTEM,
-                               type: SettingsFieldType.SELECT,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL,
-                               options: COLOR_MODE_OPTIONS
-                       },
-                       {
-                               key: SETTINGS_KEYS.API_KEY,
-                               label: 'API Key',
-                               help: `Set the API Key if you are using <code> ${CLI_FLAGS.API_KEY} </code> option for the server.`,
-                               defaultValue: '',
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.SYSTEM_MESSAGE,
-                               label: 'System Message',
-                               help: 'The starting message that defines how model should behave.',
-                               defaultValue: '',
-                               type: SettingsFieldType.TEXTAREA,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN,
-                               label: 'Paste long text to file length',
-                               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.',
-                               defaultValue: 2500,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
-                       },
+                               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: [
                        {
-                               key: SETTINGS_KEYS.SEND_ON_ENTER,
-                               label: 'Send message on Enter',
-                               help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.',
-                               defaultValue: true,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY,
-                               label: 'Show microphone on empty input',
-                               help: 'Automatically show microphone button instead of send button when textarea is empty for models with audio modality support.',
                                defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL,
-                               isExperimental: true
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION,
-                               label: 'Enable "Continue" button',
-                               help: 'Enable "Continue" button for assistant messages, including reasoning models.',
                                defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL,
-                               isExperimental: true
+                               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
                        },
                        {
-                               ...TITLE_GENERATION_BASE,
-                               key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE,
-                               label: 'Conversation title',
-                               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.',
-                               defaultValue: true
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT,
-                               label: 'LLM title generation prompt',
-                               help: 'Optional template for the title generation prompt. Use {{USER}} for the user message and {{ASSISTANT}} for the assistant message.',
-                               defaultValue: TITLE_GENERATION.DEFAULT_PROMPT,
-                               type: SettingsFieldType.TEXTAREA,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL,
-                               dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT,
-                               label: 'Copy text attachments as plain text',
-                               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.',
                                defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.PDF_AS_IMAGE,
-                               label: 'Parse PDF as image',
-                               help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION,
-                               label: 'Maximum image resolution (megapixels)',
-                               help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.',
-                               defaultValue: 0,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.GENERAL
+                               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]: {
-               title: SETTINGS_SECTION_TITLES.DISPLAY,
-               slug: 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',
-                               help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
-                               defaultValue: true,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display per-turn statistics (tokens, duration) under each turn in agentic responses. Shown only when "Show message generation statistics" is enabled.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
                                section: SETTINGS_SECTION_SLUGS.DISPLAY,
-                               dependsOn: SETTINGS_KEYS.SHOW_MESSAGE_STATS
+                               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',
-                               help: 'Expand thought process by default when generating messages.',
-                               defaultValue: true,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Automatically expand tool call details while executing and keep them expanded after completion.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               section: SETTINGS_SECTION_SLUGS.DISPLAY,
+                               type: SettingsFieldType.CHECKBOX
                        },
                        {
+                               defaultValue: false,
+                               help: 'Display user, system and thinking content as plain text instead of formatted Markdown. Markdown is the default so that @-mention badges render in sent messages.',
                                key: SETTINGS_KEYS.RENDER_CONTENT_AS_RAW_TEXT,
                                label: 'Render content as raw text',
-                               help: 'Display user, system and thinking content as plain text instead of formatted Markdown. Markdown is the default so that @-mention badges render in sent messages.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Always display code blocks at their full natural height, overriding any height limits.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.',
-                               defaultValue: true,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.',
-                               defaultValue: true,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display the current build version in the bottom-right corner of the interface.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               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',
-                               help: 'Display the full file system path inside file and folder @-mention badges instead of just the file or folder name.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DISPLAY
+                               section: SETTINGS_SECTION_SLUGS.DISPLAY,
+                               type: SettingsFieldType.CHECKBOX
                        }
-               ]
+               ],
+               slug: SETTINGS_SECTION_SLUGS.DISPLAY,
+               title: SETTINGS_SECTION_TITLES.DISPLAY
        },
-       [SETTINGS_SECTION_SLUGS.SAMPLING]: {
-               title: SETTINGS_SECTION_TITLES.SAMPLING,
-               slug: SETTINGS_SECTION_SLUGS.SAMPLING,
-               icon: Funnel,
+       [SETTINGS_SECTION_SLUGS.GENERAL]: {
+               icon: Sliders,
                settings: [
                        {
-                               key: SETTINGS_KEYS.TEMPERATURE,
-                               label: 'Temperature',
-                               help: 'Controls the randomness of the generated text by affecting the probability distribution of the output tokens. Higher = more random, lower = more focused.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.TEMPERATURE,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
-                       },
-                       {
-                               key: SETTINGS_KEYS.DYNATEMP_RANGE,
-                               label: 'Dynamic temperature range',
-                               help: 'Addon for the temperature sampler. The added value to the range of dynamic temperature, which adjusts probabilities by entropy of tokens.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.DYNATEMP_RANGE,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.DYNATEMP_EXPONENT,
-                               label: 'Dynamic temperature exponent',
-                               help: 'Addon for the temperature sampler. Smoothes out the probability redistribution based on the most probable token.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.DYNATEMP_EXPONENT,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.TOP_K,
-                               label: 'Top K',
-                               help: 'Keeps only k top tokens.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: { serverKey: SETTINGS_KEYS.TOP_K, paramType: SyncableParameterType.NUMBER }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.TOP_P,
-                               label: 'Top P',
-                               help: 'Limits tokens to those that together have a cumulative probability of at least p',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: { serverKey: SETTINGS_KEYS.TOP_P, paramType: SyncableParameterType.NUMBER }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.MIN_P,
-                               label: 'Min P',
-                               help: 'Limits tokens based on the minimum probability for a token to be considered, relative to the probability of the most likely token.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: { serverKey: SETTINGS_KEYS.MIN_P, paramType: SyncableParameterType.NUMBER }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.XTC_PROBABILITY,
-                               label: 'XTC probability',
-                               help: 'XTC sampler cuts out top tokens; this parameter controls the chance of cutting tokens at all. 0 disables XTC.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.XTC_PROBABILITY,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.XTC_THRESHOLD,
-                               label: 'XTC threshold',
-                               help: 'XTC sampler cuts out top tokens; this parameter controls the token probability that is required to cut that token.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.XTC_THRESHOLD,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.TYP_P,
-                               label: 'Typical P',
-                               help: 'Sorts and limits tokens based on the difference between log-probability and entropy.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: { serverKey: SETTINGS_KEYS.TYP_P, paramType: SyncableParameterType.NUMBER }
+                               ...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'
                        },
                        {
-                               key: SETTINGS_KEYS.MAX_TOKENS,
-                               label: 'Max tokens',
-                               help: 'The maximum number of token per output. Use -1 for infinite (no limit).',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: {
-                                       serverKey: SETTINGS_KEYS.MAX_TOKENS,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.SAMPLERS,
-                               label: 'Samplers',
-                               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',
-                               defaultValue: '',
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING,
-                               sync: { serverKey: SETTINGS_KEYS.SAMPLERS, paramType: SyncableParameterType.STRING }
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.BACKEND_SAMPLING,
-                               label: 'Backend sampling',
-                               help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.',
                                defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.SAMPLING
+                               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]: {
-               title: SETTINGS_SECTION_TITLES.PENALTIES,
-               slug: 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',
-                               help: 'Last n tokens to consider for penalizing repetition',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.REPEAT_LAST_N,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'Controls the repetition of token sequences in the generated text',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.REPEAT_PENALTY,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'Limits tokens based on whether they appear in the output or not.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.PRESENCE_PENALTY,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'Limits tokens based on how often they appear in the output.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.FREQUENCY_PENALTY,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling multiplier.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.DRY_MULTIPLIER,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the DRY sampling base value.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
-                               sync: { serverKey: SETTINGS_KEYS.DRY_BASE, paramType: SyncableParameterType.NUMBER }
+                               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',
-                               help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets the allowed length for DRY sampling.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.DRY_ALLOWED_LENGTH,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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',
-                               help: 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.',
-                               defaultValue: undefined,
-                               type: SettingsFieldType.INPUT,
                                section: SETTINGS_SECTION_SLUGS.PENALTIES,
                                sync: {
-                                       serverKey: SETTINGS_KEYS.DRY_PENALTY_LAST_N,
-                                       paramType: SyncableParameterType.NUMBER
-                               }
+                                       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.AGENTIC]: {
-               title: SETTINGS_SECTION_TITLES.AGENTIC,
-               slug: SETTINGS_SECTION_SLUGS.AGENTIC,
-               icon: ListRestart,
+       [SETTINGS_SECTION_SLUGS.SAMPLING]: {
+               icon: Funnel,
                settings: [
                        {
-                               key: SETTINGS_KEYS.AGENTIC_MAX_TURNS,
-                               label: 'Agentic turns',
-                               help: 'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
-                               defaultValue: 10,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.AGENTIC,
-                               isPositiveInteger: true
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
-                               label: 'MCP request timeout (seconds)',
-                               help: 'Timeout for individual MCP tool calls.',
-                               defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.AGENTIC,
-                               isPositiveInteger: true
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.MENTION_SEARCH_MAX_DEPTH,
-                               label: 'Mention 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.',
-                               defaultValue: FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
-                               placeholder: `${FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH}`,
-                               min: 1,
-                               max: FILE_GLOB_SEARCH_PICKERS_MAX_SEARCH_DEPTH,
-                               type: SettingsFieldType.INPUT,
-                               section: SETTINGS_SECTION_SLUGS.AGENTIC,
-                               isPositiveInteger: true
-                       }
-               ]
-       },
-       [SETTINGS_SECTION_SLUGS.DEVELOPER]: {
-               title: SETTINGS_SECTION_TITLES.DEVELOPER,
-               slug: SETTINGS_SECTION_SLUGS.DEVELOPER,
-               icon: Code,
-               settings: [
+                               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
+                       },
                        {
-                               key: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION,
-                               label: 'Pre-fill KV cache after response',
-                               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.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.DISABLE_REASONING_PARSING,
-                               label: 'Disable reasoning content parsing',
-                               help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT,
-                               label: 'Exclude reasoning from context',
-                               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.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH,
-                               label: 'Enable raw output toggle',
-                               help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.JS_SANDBOX_ENABLED,
-                               label: 'JavaScript sandbox tool',
-                               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.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED,
-                               label: 'Symbolic math (nerdamer)',
-                               help: 'Pre-load nerdamer in the sandbox for symbolic computation: simplify, diff, integrate, solve, and more. Requires "JavaScript sandbox tool" to be enabled.',
-                               defaultValue: false,
-                               type: SettingsFieldType.CHECKBOX,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER,
-                               dependsOn: SETTINGS_KEYS.JS_SANDBOX_ENABLED
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.CUSTOM_JSON,
-                               label: 'Custom JSON',
-                               help: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
-                               defaultValue: '',
-                               type: SettingsFieldType.TEXTAREA,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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
                        },
                        {
-                               key: SETTINGS_KEYS.CUSTOM_CSS,
-                               label: 'Custom CSS',
-                               help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.',
                                defaultValue: '',
-                               type: SettingsFieldType.TEXTAREA,
-                               section: SETTINGS_SECTION_SLUGS.DEVELOPER
+                               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',
-               help: 'Display the system message at the top of each conversation.',
-               defaultValue: true,
                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',
-               help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
-               defaultValue: '[]',
                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',
-               help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.',
-               defaultValue: false,
                type: SettingsFieldType.CHECKBOX
        }
        // {
@@ -684,10 +679,12 @@ const NON_UI_SETTINGS: SettingsEntry[] = [
 
 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;
 }
 
@@ -707,23 +704,23 @@ 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) => ({
-               title: section.title,
-               slug: section.slug,
-               icon: section.icon,
                fields: section.settings.map((s) => ({
-                       key: s.key,
-                       label: s.label,
-                       type: s.type,
+                       dependsOn: s.dependsOn,
+                       help: s.help,
                        isExperimental: s.isExperimental,
                        isPositiveInteger: s.isPositiveInteger,
-                       placeholder: s.placeholder,
-                       min: s.min,
+                       key: s.key,
+                       label: s.label,
                        max: s.max,
-                       dependsOn: s.dependsOn,
-                       help: s.help,
+                       min: s.min,
                        options: s.options,
-                       radioOptions: s.radioOptions
-               }))
+                       placeholder: s.placeholder,
+                       radioOptions: s.radioOptions,
+                       type: s.type
+               })),
+               icon: section.icon,
+               slug: section.slug,
+               title: section.title
        })),
        ...STANDALONE_SECTIONS
 ];
@@ -742,10 +739,10 @@ export const POSITIVE_INTEGER_FIELDS = getAllSettings()
 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,
-               canSync: true
+               type: s.sync!.paramType
        }));
 
 export const SETTINGS_FALLBACK_EXIT_ROUTE = ROUTES.START;
index cbe780aa57e7e623aff9076cb83c3cf0c849f3a3..a6bcefaa157ed7f3cac4d217838781dd67f7cc92 100644 (file)
@@ -12,11 +12,11 @@ import {
        FileTypeImage,
        FileTypePdf,
        FileTypeText,
+       MimeTypeApplication,
        MimeTypeAudio,
-       MimeTypeVideo,
        MimeTypeImage,
-       MimeTypeApplication,
-       MimeTypeText
+       MimeTypeText,
+       MimeTypeVideo
 } from '$lib/enums';
 import { FileExtensionVideo, FileTypeVideo } from '$lib/enums/files.enums';
 
@@ -44,6 +44,14 @@ export const VIDEO_FILE_TYPES = {
 } 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]
@@ -52,21 +60,13 @@ export const IMAGE_FILE_TYPES = {
                extensions: [FileExtensionImage.PNG],
                mimeTypes: [MimeTypeImage.PNG]
        },
-       [FileTypeImage.GIF]: {
-               extensions: [FileExtensionImage.GIF],
-               mimeTypes: [MimeTypeImage.GIF]
-       },
-       [FileTypeImage.WEBP]: {
-               extensions: [FileExtensionImage.WEBP],
-               mimeTypes: [MimeTypeImage.WEBP]
-       },
        [FileTypeImage.SVG]: {
                extensions: [FileExtensionImage.SVG],
                mimeTypes: [MimeTypeImage.SVG]
        },
-       [FileTypeImage.HEIC]: {
-               extensions: [FileExtensionImage.HEIC, FileExtensionImage.HEIF],
-               mimeTypes: [MimeTypeImage.HEIC, MimeTypeImage.HEIF]
+       [FileTypeImage.WEBP]: {
+               extensions: [FileExtensionImage.WEBP],
+               mimeTypes: [MimeTypeImage.WEBP]
        }
 } as const;
 
@@ -78,95 +78,119 @@ export const PDF_FILE_TYPES = {
 } as const;
 
 export const TEXT_FILE_TYPES = {
-       [FileTypeText.PLAIN_TEXT]: {
-               extensions: [FileExtensionText.TXT],
-               mimeTypes: [MimeTypeText.PLAIN]
-       },
-       [FileTypeText.MARKDOWN]: {
-               extensions: [FileExtensionText.MD],
-               mimeTypes: [MimeTypeText.MARKDOWN]
-       },
        [FileTypeText.ASCIIDOC]: {
                extensions: [FileExtensionText.ADOC],
                mimeTypes: [MimeTypeText.ASCIIDOC]
        },
-       [FileTypeText.JAVASCRIPT]: {
-               extensions: [FileExtensionText.JS],
-               mimeTypes: [MimeTypeText.JAVASCRIPT, MimeTypeText.JAVASCRIPT_APP]
-       },
-       [FileTypeText.TYPESCRIPT]: {
-               extensions: [FileExtensionText.TS],
-               mimeTypes: [MimeTypeText.TYPESCRIPT]
+       [FileTypeText.BIBTEX]: {
+               extensions: [FileExtensionText.BIB],
+               mimeTypes: [MimeTypeText.BIBTEX]
        },
-       [FileTypeText.JSX]: {
-               extensions: [FileExtensionText.JSX],
-               mimeTypes: [MimeTypeText.JSX]
+       [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.TSX]: {
-               extensions: [FileExtensionText.TSX],
-               mimeTypes: [MimeTypeText.TSX]
+       [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.XML]: {
-               extensions: [FileExtensionText.XML],
-               mimeTypes: [MimeTypeText.XML_TEXT, MimeTypeText.XML_APP]
+       [FileTypeText.JSX]: {
+               extensions: [FileExtensionText.JSX],
+               mimeTypes: [MimeTypeText.JSX]
        },
-       [FileTypeText.YAML]: {
-               extensions: [FileExtensionText.YAML, FileExtensionText.YML],
-               mimeTypes: [MimeTypeText.YAML_TEXT, MimeTypeText.YAML_APP]
+       [FileTypeText.KOTLIN]: {
+               extensions: [FileExtensionText.KT],
+               mimeTypes: [MimeTypeText.KOTLIN]
        },
-       [FileTypeText.CSV]: {
-               extensions: [FileExtensionText.CSV],
-               mimeTypes: [MimeTypeText.CSV]
+       [FileTypeText.LATEX]: {
+               extensions: [FileExtensionText.TEX],
+               mimeTypes: [MimeTypeText.LATEX, MimeTypeText.TEX, MimeTypeText.TEX_APP]
        },
        [FileTypeText.LOG]: {
                extensions: [FileExtensionText.LOG],
                mimeTypes: [MimeTypeText.PLAIN]
        },
-       [FileTypeText.PYTHON]: {
-               extensions: [FileExtensionText.PY],
-               mimeTypes: [MimeTypeText.PYTHON]
-       },
-       [FileTypeText.JAVA]: {
-               extensions: [FileExtensionText.JAVA],
-               mimeTypes: [MimeTypeText.JAVA]
-       },
-       [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.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.GO]: {
-               extensions: [FileExtensionText.GO],
-               mimeTypes: [MimeTypeText.GO]
-       },
        [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]
@@ -175,60 +199,36 @@ export const TEXT_FILE_TYPES = {
                extensions: [FileExtensionText.SQL],
                mimeTypes: [MimeTypeText.SQL]
        },
-       [FileTypeText.R]: {
-               extensions: [FileExtensionText.R],
-               mimeTypes: [MimeTypeText.R]
-       },
-       [FileTypeText.SCALA]: {
-               extensions: [FileExtensionText.SCALA],
-               mimeTypes: [MimeTypeText.SCALA]
-       },
-       [FileTypeText.KOTLIN]: {
-               extensions: [FileExtensionText.KT],
-               mimeTypes: [MimeTypeText.KOTLIN]
+       [FileTypeText.SVELTE]: {
+               extensions: [FileExtensionText.SVELTE],
+               mimeTypes: [MimeTypeText.SVELTE]
        },
        [FileTypeText.SWIFT]: {
                extensions: [FileExtensionText.SWIFT],
                mimeTypes: [MimeTypeText.SWIFT]
        },
-       [FileTypeText.DART]: {
-               extensions: [FileExtensionText.DART],
-               mimeTypes: [MimeTypeText.DART]
+       [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.SVELTE]: {
-               extensions: [FileExtensionText.SVELTE],
-               mimeTypes: [MimeTypeText.SVELTE]
-       },
-       [FileTypeText.LATEX]: {
-               extensions: [FileExtensionText.TEX],
-               mimeTypes: [MimeTypeText.LATEX, MimeTypeText.TEX, MimeTypeText.TEX_APP]
-       },
-       [FileTypeText.BIBTEX]: {
-               extensions: [FileExtensionText.BIB],
-               mimeTypes: [MimeTypeText.BIBTEX]
-       },
-       [FileTypeText.CUDA]: {
-               extensions: [FileExtensionText.CU, FileExtensionText.CUH],
-               mimeTypes: [MimeTypeText.CUDA]
-       },
        [FileTypeText.VULKAN]: {
                extensions: [FileExtensionText.COMP],
                mimeTypes: [MimeTypeText.PLAIN]
        },
-       [FileTypeText.HASKELL]: {
-               extensions: [FileExtensionText.HS],
-               mimeTypes: [MimeTypeText.HASKELL]
-       },
-       [FileTypeText.CSHARP]: {
-               extensions: [FileExtensionText.CS],
-               mimeTypes: [MimeTypeText.CSHARP]
+       [FileTypeText.XML]: {
+               extensions: [FileExtensionText.XML],
+               mimeTypes: [MimeTypeText.XML_TEXT, MimeTypeText.XML_APP]
        },
-       [FileTypeText.PROPERTIES]: {
-               extensions: [FileExtensionText.PROPERTIES],
-               mimeTypes: [MimeTypeText.PROPERTIES]
+       [FileTypeText.YAML]: {
+               extensions: [FileExtensionText.YAML, FileExtensionText.YML],
+               mimeTypes: [MimeTypeText.YAML_TEXT, MimeTypeText.YAML_APP]
        }
 } as const;
index ccca9376c68f52bade2e22825322b40c47c63535..359e0692f2d2668726df7d645790ace1767b65c0 100644 (file)
@@ -30,8 +30,8 @@ export const SVG_MAX_BYTES = 256 * 1024;
  * so an author <style> stays scoped to that root and can not reach the page.
  */
 export const SVG_SANITIZE_CONFIG = {
-       USE_PROFILES: { svg: true, svgFilters: true },
-       FORBID_TAGS: ['foreignObject', 'script']
+       FORBID_TAGS: ['foreignObject', 'script'],
+       USE_PROFILES: { svg: true, svgFilters: true }
 };
 
 /**
index 48ca2217a27d5f35a918c4c4c2e815abe27e2db1..0496daafe2c821c41a6a846d32fee30573be5086 100644 (file)
@@ -1,9 +1,9 @@
 /* Title generation constants */
 export const TITLE_GENERATION = {
-       MIN_LENGTH: 3,
-       FALLBACK: 'New Chat',
        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;
index 2c18cc518b9caa1352dfd1808f8edbe6071bbe99..9489a3f268c374db5128132eaaaad573cf449df4 100644 (file)
@@ -1,7 +1,7 @@
+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';
-import { ROUTES } from './routes';
 
 export const FORK_TREE_DEPTH_PADDING = 8;
 export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
@@ -26,18 +26,18 @@ export interface DesktopIconStripItem {
 }
 
 export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [
-       { icon: SquarePen, tooltip: 'New chat', route: ROUTES.NEW_CHAT, keys: ['shift', 'cmd', 'o'] },
-       { icon: Search, tooltip: 'Search', keys: ['cmd', 'k'] },
+       { 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,
-               tooltip: 'MCP Servers',
                route: ROUTES.MCP_SERVERS,
-               activeRouteId: '/mcp-servers'
+               tooltip: 'MCP Servers'
        },
        {
+               activeUrlIncludes: '#/settings',
                icon: Settings,
-               tooltip: 'Settings',
                route: `${ROUTES.SETTINGS}/general`,
-               activeUrlIncludes: '#/settings'
+               tooltip: 'Settings'
        }
 ];
index 9b44d6ed375615b5af4525a369d803ced91f6740..3ae25551b9bebb13ce20a31ab8ea2cb5dce55d86 100644 (file)
@@ -10,38 +10,38 @@ export const TEMPLATE_EXPRESSION_REGEX = /\{([+#./;?&]?)([^}]+)\}/g;
 
 /** RFC 6570 URI template operators */
 export const URI_TEMPLATE_OPERATORS = {
-       /** Simple string expansion (default) */
-       SIMPLE: '',
-       /** Reserved expansion */
-       RESERVED: '+',
+       /** Form-style query continuation */
+       FORM_CONTINUATION: '&',
+       /** Form-style query */
+       FORM_QUERY: '?',
        /** Fragment expansion */
        FRAGMENT: '#',
-       /** Path segment expansion */
-       PATH_SEGMENT: '/',
        /** Label expansion */
        LABEL: '.',
        /** Path-style parameters */
        PATH_PARAM: ';',
-       /** Form-style query */
-       FORM_QUERY: '?',
-       /** Form-style query continuation */
-       FORM_CONTINUATION: '&'
+       /** 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: ',',
-       /** Slash separator for path segments */
-       SLASH: '/',
        /** Period separator for label expansion */
        PERIOD: '.',
-       /** Semicolon separator for path parameters */
-       SEMICOLON: ';',
+       /** Ampersand prefix for query continuation */
+       QUERY_CONTINUATION: '&',
        /** Question mark prefix for query string */
        QUERY_PREFIX: '?',
-       /** Ampersand prefix for query continuation */
-       QUERY_CONTINUATION: '&'
+       /** Semicolon separator for path parameters */
+       SEMICOLON: ';',
+       /** Slash separator for path segments */
+       SLASH: '/'
 } as const;
 
 /** Maximum number of leading slashes to strip during URI normalization */
index dd0962d1851c6f41f51a780a4d8a8aa0db4686a3..214c8afbac9fd59b9768b6f1b85e67e9b2389452 100644 (file)
@@ -1,34 +1,12 @@
 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,
-       cn: STD_MIL,
-       eg: STD,
-       gr: STD,
-       hk: STD,
-       hr: STD,
-       lk: STD,
-       mx: STD_MIL,
-       my: STD_MIL,
-       ng: STD,
-       ph: STD,
-       pk: STD,
-       pl: STD,
-       ro: STD,
-       ru: STD,
-       sa: STD,
-       si: STD,
-       tr: STD,
-       tw: STD,
-       ua: STD,
-       ve: STD,
-
-       au: [...STD_MIL, 'id', 'asn', 'csiro'],
        br: [
                ...STD_MIL,
                'art',
@@ -84,9 +62,22 @@ const ccTLD_PREFIXES: Record<string, readonly string[]> = {
                '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',
@@ -100,19 +91,25 @@ const ccTLD_PREFIXES: Record<string, readonly string[]> = {
                'iwi',
                'parliament'
        ],
-       sg: [...STD, 'per'],
-       th: ['co', 'go', 'or', 'in', 'ac', 'mi', 'net'],
 
-       ae: ['co', 'net', 'org', 'gov', 'ac', 'sch'],
-       hu: ['co', 'net', 'org', 'gov', 'edu'],
-       il: ['co', 'net', 'org', 'gov', 'ac', 'muni'],
-       jp: ['ac', 'ad', 'co', 'ed', 'go', 'gr', 'lg', 'ne', 'or'],
-       ke: ['co', 'or', 'ne', 'go', 'ac', 'sc'],
+       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: [
index e9050fa27fb3ddd236ffa5815279cd13e1cbc156..dffb5f3a36d923412a56f52ad38ef2c674bc72e4 100644 (file)
@@ -1,5 +1,5 @@
-import { getContext, setContext } from 'svelte';
 import { CONTEXT_KEY_CHAT_ACTIONS } from '$lib/constants';
+import { getContext, setContext } from 'svelte';
 
 export interface ChatActionsContext {
        copy: (message: DatabaseMessage) => void;
index 35941e09bd5cd82773993c4e023c87cac6a4f764..a90f709b49f80b5b0620cd148df07d69407aad63 100644 (file)
@@ -1,5 +1,5 @@
-import { getContext, setContext } from 'svelte';
 import { CONTEXT_KEY_CHAT_SETTINGS_CONFIG } from '$lib/constants';
+import { getContext, setContext } from 'svelte';
 
 export interface ChatSettingsConfigContext {
        readonly localConfig: SettingsConfigType;
index b6231f940e9b1fe3a925fd8ba8833460f83cc441..80f3e6eee28ce1b0ec7146065811ed94a516fc4b 100644 (file)
@@ -1,6 +1,6 @@
-import { getContext, setContext } from 'svelte';
 import { CONTEXT_KEY_MESSAGE_EDIT } from '$lib/constants';
 import { MessageRole } from '$lib/enums';
+import { getContext, setContext } from 'svelte';
 
 export interface MessageEditState {
        readonly isEditing: boolean;
index 0940ce7b6b1880c23ae8eda09f460b637365eb0e..98ecc9ace0a9fe9ea6d00630472d43909ecc63fd 100644 (file)
@@ -40,28 +40,30 @@ export function useAttachmentMenu(
        close: () => void
 ): UseAttachmentMenuReturn {
        const modalityFlags = $derived(getFlags());
-
        const callbacks = $derived.by(() => {
                const cbs = getCallbacks();
                const wrap = (fn?: () => void) => () => {
                        close();
                        fn?.();
                };
+
                return {
                        [AttachmentAction.FILE_UPLOAD]: wrap(cbs.onFileUpload),
-                       [AttachmentAction.SYSTEM_PROMPT_CLICK]: wrap(cbs.onSystemPromptClick),
                        [AttachmentAction.MCP_PROMPT_CLICK]: wrap(cbs.onMcpPromptClick),
-                       [AttachmentAction.MCP_RESOURCES_CLICK]: wrap(cbs.onMcpResourcesClick)
+                       [AttachmentAction.MCP_RESOURCES_CLICK]: wrap(cbs.onMcpResourcesClick),
+                       [AttachmentAction.SYSTEM_PROMPT_CLICK]: wrap(cbs.onSystemPromptClick)
                };
        });
 
        function isItemEnabled(enabledWhen: string | undefined): boolean {
                if (!enabledWhen || enabledWhen === 'always') return true;
+
                return !!modalityFlags[enabledWhen as keyof AttachmentModalityFlags];
        }
 
        function isItemVisible(visibleWhen: string | undefined): boolean {
                if (!visibleWhen) return true;
+
                return !!modalityFlags[visibleWhen as keyof AttachmentModalityFlags];
        }
 
@@ -75,8 +77,8 @@ export function useAttachmentMenu(
                get callbacks() {
                        return callbacks;
                },
+               getSystemMessageTooltip,
                isItemEnabled,
-               isItemVisible,
-               getSystemMessageTooltip
+               isItemVisible
        };
 }
index f2ad50dff799d510730a6807166379fc6e52465b..6ebce15dad03729532e7d0cb91170280ace81453 100644 (file)
@@ -51,7 +51,9 @@ export class AutoScrollController {
         */
        setDisabled(disabled: boolean): void {
                if (this._disabled === disabled) return;
+
                this._disabled = disabled;
+
                if (disabled) {
                        this._autoScrollEnabled = false;
                        this.stopInterval();
@@ -67,7 +69,7 @@ export class AutoScrollController {
        handleScroll(): void {
                if (this._disabled || !this._container) return;
 
-               const { scrollTop, scrollHeight, clientHeight } = this._container;
+               const { clientHeight, scrollHeight, scrollTop } = this._container;
                const distanceFromBottom = scrollHeight - clientHeight - scrollTop;
                const isScrollingUp = scrollTop < this._lastScrollTop;
                const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
@@ -88,6 +90,7 @@ export class AutoScrollController {
         */
        scrollToBottom(): void {
                if (this._disabled || !this._container) return;
+
                this._container.scrollTop = this._container.scrollHeight;
        }
 
@@ -96,6 +99,7 @@ export class AutoScrollController {
         */
        enable(): void {
                if (this._disabled) return;
+
                this._userScrolledUp = false;
                this._autoScrollEnabled = true;
        }
@@ -106,6 +110,7 @@ export class AutoScrollController {
        resetScrollState(): void {
                this._userScrolledUp = false;
                this._autoScrollEnabled = !this._disabled;
+
                if (this._container) {
                        this._lastScrollTop = this._container.scrollTop;
                }
@@ -139,6 +144,7 @@ export class AutoScrollController {
        updateInterval(isStreaming: boolean): void {
                if (this._disabled) {
                        this.stopInterval();
+
                        return;
                }
 
@@ -184,9 +190,11 @@ export class AutoScrollController {
 
                this._mutationObserver = new MutationObserver(() => {
                        if (!this._autoScrollEnabled || this._rafPending) return;
+
                        this._rafPending = true;
                        requestAnimationFrame(() => {
                                this._rafPending = false;
+
                                if (this._autoScrollEnabled && this._container) {
                                        this._container.scrollTop = this._container.scrollHeight;
                                }
@@ -194,9 +202,9 @@ export class AutoScrollController {
                });
 
                this._mutationObserver.observe(this._container, {
+                       characterData: true,
                        childList: true,
-                       subtree: true,
-                       characterData: true
+                       subtree: true
                });
        }
 
@@ -205,6 +213,7 @@ export class AutoScrollController {
                        this._mutationObserver.disconnect();
                        this._mutationObserver = null;
                }
+
                this._rafPending = false;
        }
 }
index 4860f16fb2af0eca5c973aa8c43b877dadf2f74f..712189746b6d37b7d4a8dc0e506aa61af8061236 100644 (file)
@@ -2,12 +2,12 @@ import { getChatCommands, PROMPT_TRIGGER_PREFIX } from '$lib/constants';
 import { ChatFormCommandAction, KeyboardKey } from '$lib/enums';
 import type { ChatFormCommand } from '$lib/types';
 import {
+       type CommandDismissSnapshot,
        findCommandToken,
        findMentionToken,
+       type MentionDismissSnapshot,
        takeCommandDismissSnapshot,
-       takeMentionDismissSnapshot,
-       type CommandDismissSnapshot,
-       type MentionDismissSnapshot
+       takeMentionDismissSnapshot
 } from '$lib/utils';
 
 /** Dependencies injected as getters so the hook stays free of store circular imports. */
@@ -47,23 +47,20 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
        let mentionQuery = $state('');
        let isWorkingDirectoryPickerOpen = $state(false);
        let workingDirectoryQuery = $state('');
-
        // Last dismissed `@`-mention token; while intact, the picker does not
        // reopen, so an escaped `@<query>` stays literal until edited.
        let mentionDismissedSnapshot: MentionDismissSnapshot | null = null;
-
        // Same dismissal contract for the `/`-command token.
        let commandDismissedSnapshot: CommandDismissSnapshot | null = null;
 
        // Fall back to the server home so the picker still finds matches
        // before a cwd is set.
        const mentionScopePath = $derived(opts.getCwd() ?? opts.getServerHome() ?? null);
-
        const availableCommands = $derived(
                getChatCommands({
-                       showModelSelector: opts.getShowModelSelector(),
+                       hasCwdTools: opts.hasCwdTools,
                        hasPrompts: opts.hasPrompts,
-                       hasCwdTools: opts.hasCwdTools
+                       showModelSelector: opts.getShowModelSelector()
                })
        );
 
@@ -81,24 +78,29 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                                opts.setValue('');
                                isPromptPickerOpen = true;
                                promptSearchQuery = args.trim();
+
                                break;
                        case ChatFormCommandAction.CWD: {
                                // Keep `/cwd <args>` in the input so the search field and the
                                // token stay two-way bound; normalize partial tokens (`/cw foo`).
                                const trimmed = args.trim();
                                const newValue = `/cwd ${trimmed}`;
+
                                if (opts.getValue() !== newValue) {
                                        opts.setValue(newValue);
                                        queueMicrotask(() => opts.setCaretOffset(newValue.length));
                                }
+
                                workingDirectoryQuery = trimmed;
                                isWorkingDirectoryPickerOpen = true;
+
                                break;
                        }
                        case ChatFormCommandAction.MODEL:
                                isWorkingDirectoryPickerOpen = false;
                                opts.setValue('');
                                opts.openModelSelector();
+
                                break;
                }
        }
@@ -114,9 +116,11 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                        promptSearchQuery = '';
 
                        const token = findCommandToken(value);
+
                        if (!token) {
                                isCommandPickerOpen = false;
                                commandQuery = '';
+
                                return;
                        }
 
@@ -125,12 +129,14 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                        if (isWorkingDirectoryPickerOpen) {
                                isCommandPickerOpen = false;
                                commandQuery = '';
+
                                if (token.name === 'cwd') {
                                        workingDirectoryQuery = token.args.trim();
                                } else {
                                        isWorkingDirectoryPickerOpen = false;
                                        workingDirectoryQuery = '';
                                }
+
                                return;
                        }
 
@@ -143,6 +149,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                        if (isDismissedSticky) {
                                isCommandPickerOpen = false;
                                commandQuery = '';
+
                                return;
                        }
 
@@ -156,14 +163,17 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                                isCommandPickerOpen = false;
                                commandQuery = '';
                        }
+
                        return;
                }
 
                isCommandPickerOpen = false;
                commandQuery = '';
+
                if (commandDismissedSnapshot !== null) {
                        commandDismissedSnapshot = null;
                }
+
                if (isWorkingDirectoryPickerOpen) {
                        isWorkingDirectoryPickerOpen = false;
                }
@@ -186,6 +196,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                                        mentionQuery = token.query;
                                        isPromptPickerOpen = false;
                                        promptSearchQuery = '';
+
                                        return;
                                }
                        }
@@ -210,6 +221,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                if (event.key === KeyboardKey.ESCAPE && isPromptPickerOpen) {
                        isPromptPickerOpen = false;
                        promptSearchQuery = '';
+
                        return true;
                }
 
@@ -219,6 +231,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
        function handleCommandSelect(command: ChatFormCommand) {
                // Dispatch on the live token so typed args seed the target picker.
                const token = findCommandToken(opts.getValue());
+
                dispatchCommand(command, token?.args ?? '');
        }
 
@@ -228,8 +241,10 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                if (isCommandPickerOpen) {
                        commandDismissedSnapshot = takeCommandDismissSnapshot(opts.getValue());
                }
+
                isCommandPickerOpen = false;
                commandQuery = '';
+
                // Target picker manages its own focus: don't yank it back to the input.
                if (!isPromptPickerOpen && !isMentionPickerOpen && !isWorkingDirectoryPickerOpen) {
                        opts.focusInput();
@@ -240,8 +255,10 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
        function handleMentionPickerClose() {
                if (isMentionPickerOpen) {
                        const cursor = opts.getCaretOffset() ?? opts.getValue().length;
+
                        mentionDismissedSnapshot = takeMentionDismissSnapshot(opts.getValue(), cursor);
                }
+
                isMentionPickerOpen = false;
                mentionQuery = '';
                opts.focusInput();
@@ -268,21 +285,27 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
        // reverse direction is handled by handleInput.
        $effect(() => {
                if (!isWorkingDirectoryPickerOpen) return;
+
                const value = opts.getValue();
                const token = findCommandToken(value);
+
                if (!token || token.name !== 'cwd') return;
+
                const newValue = `/cwd ${workingDirectoryQuery}`;
+
                if (newValue === value) return;
+
                opts.setValue(newValue);
                queueMicrotask(() => opts.setCaretOffset(newValue.length));
        });
 
        return {
-               get isCommandPickerOpen() {
-                       return isCommandPickerOpen;
+               get availableCommands() {
+                       return availableCommands;
                },
-               set isCommandPickerOpen(v: boolean) {
-                       isCommandPickerOpen = v;
+               closePromptPicker() {
+                       isPromptPickerOpen = false;
+                       promptSearchQuery = '';
                },
                get commandQuery() {
                        return commandQuery;
@@ -290,17 +313,21 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                set commandQuery(v: string) {
                        commandQuery = v;
                },
-               get isPromptPickerOpen() {
-                       return isPromptPickerOpen;
-               },
-               set isPromptPickerOpen(v: boolean) {
-                       isPromptPickerOpen = v;
-               },
-               get promptSearchQuery() {
-                       return promptSearchQuery;
+               dispatchCommand,
+               handleCommandPickerClose,
+               handleCommandSelect,
+               handleInput,
+               // True when a picker consumed the event, so the form skips submit.
+               handleKeydown,
+               handleMentionPickerClose,
+               handlePromptPickerClose,
+               handleWorkingDirectoryClose,
+               handleWorkingDirectoryOpen,
+               get isCommandPickerOpen() {
+                       return isCommandPickerOpen;
                },
-               set promptSearchQuery(v: string) {
-                       promptSearchQuery = v;
+               set isCommandPickerOpen(v: boolean) {
+                       isCommandPickerOpen = v;
                },
                get isMentionPickerOpen() {
                        return isMentionPickerOpen;
@@ -308,11 +335,11 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                set isMentionPickerOpen(v: boolean) {
                        isMentionPickerOpen = v;
                },
-               get mentionQuery() {
-                       return mentionQuery;
+               get isPromptPickerOpen() {
+                       return isPromptPickerOpen;
                },
-               set mentionQuery(v: string) {
-                       mentionQuery = v;
+               set isPromptPickerOpen(v: boolean) {
+                       isPromptPickerOpen = v;
                },
                get isWorkingDirectoryPickerOpen() {
                        return isWorkingDirectoryPickerOpen;
@@ -320,34 +347,29 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                set isWorkingDirectoryPickerOpen(v: boolean) {
                        isWorkingDirectoryPickerOpen = v;
                },
-               get workingDirectoryQuery() {
-                       return workingDirectoryQuery;
-               },
-               set workingDirectoryQuery(v: string) {
-                       workingDirectoryQuery = v;
+               get mentionQuery() {
+                       return mentionQuery;
                },
-               get availableCommands() {
-                       return availableCommands;
+               set mentionQuery(v: string) {
+                       mentionQuery = v;
                },
                get mentionScopePath() {
                        return mentionScopePath;
                },
-               handleInput,
-               // True when a picker consumed the event, so the form skips submit.
-               handleKeydown,
-               dispatchCommand,
-               handleCommandSelect,
-               handleCommandPickerClose,
-               handleMentionPickerClose,
-               handlePromptPickerClose,
-               handleWorkingDirectoryOpen,
-               handleWorkingDirectoryClose,
                openPromptPicker() {
                        isPromptPickerOpen = true;
                },
-               closePromptPicker() {
-                       isPromptPickerOpen = false;
-                       promptSearchQuery = '';
+               get promptSearchQuery() {
+                       return promptSearchQuery;
+               },
+               set promptSearchQuery(v: string) {
+                       promptSearchQuery = v;
+               },
+               get workingDirectoryQuery() {
+                       return workingDirectoryQuery;
+               },
+               set workingDirectoryQuery(v: string) {
+                       workingDirectoryQuery = v;
                }
        };
 }
index de75e9fefcaea4c1e79856949e24573bdbb11910..0f91f822f049da9725f294d850b5dbcc37989343 100644 (file)
@@ -8,17 +8,16 @@
  * demand if they aren't cached yet.
  */
 
-import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
-import { isRouterMode } from '$lib/stores/server.svelte';
 import { chatStore } from '$lib/stores/chat.svelte';
 import { activeMessages } from '$lib/stores/conversations.svelte';
+import { modelOptions, modelsStore, selectedModelId } from '$lib/stores/models.svelte';
+import { isRouterMode } from '$lib/stores/server.svelte';
 
 export function useChatScreenActiveModel() {
        const isRouter = $derived(isRouterMode());
        const conversationModel = $derived(
                chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
        );
-
        const activeModelId = $derived.by(() => {
                const options = modelOptions();
 
@@ -27,13 +26,16 @@ export function useChatScreenActiveModel() {
                }
 
                const selectedId = selectedModelId();
+
                if (selectedId) {
                        const model = options.find((m) => m.id === selectedId);
+
                        if (model) return model.model;
                }
 
                if (conversationModel) {
                        const model = options.find((m) => m.model === conversationModel);
+
                        if (model) return model.model;
                }
 
@@ -45,6 +47,7 @@ export function useChatScreenActiveModel() {
        $effect(() => {
                if (activeModelId) {
                        const cached = modelsStore.getModelProps(activeModelId);
+
                        if (!cached) {
                                modelsStore.fetchModelProps(activeModelId).then(() => {
                                        modelPropsVersion++;
@@ -56,37 +59,38 @@ export function useChatScreenActiveModel() {
        const hasAudioModality = $derived.by(() => {
                if (activeModelId) {
                        void modelPropsVersion;
+
                        return modelsStore.modelSupportsAudio(activeModelId);
                }
+
                return false;
        });
-
        const hasVideoModality = $derived.by(() => {
                if (activeModelId) {
                        void modelPropsVersion;
+
                        return modelsStore.modelSupportsVideo(activeModelId);
                }
+
                return false;
        });
-
        const hasVisionModality = $derived.by(() => {
                if (activeModelId) {
                        void modelPropsVersion;
+
                        return modelsStore.modelSupportsVision(activeModelId);
                }
+
                return false;
        });
 
        return {
-               get isRouter() {
-                       return isRouter;
+               get activeModelId() {
+                       return activeModelId;
                },
                get conversationModel() {
                        return conversationModel;
                },
-               get activeModelId() {
-                       return activeModelId;
-               },
                get hasAudioModality() {
                        return hasAudioModality;
                },
@@ -95,6 +99,9 @@ export function useChatScreenActiveModel() {
                },
                get hasVisionModality() {
                        return hasVisionModality;
+               },
+               get isRouter() {
+                       return isRouter;
                }
        };
 }
index 3f292b4e1d955fffe7186fd56eb42991ff6c62fa..e3324f91519180d23419d5c1b8aac4934e3533a2 100644 (file)
@@ -21,6 +21,7 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
        function handleDragEnter(event: DragEvent) {
                event.preventDefault();
                dragCounter++;
+
                if (event.dataTransfer?.types.includes('Files')) {
                        isDragOver = true;
                }
@@ -29,6 +30,7 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
        function handleDragLeave(event: DragEvent) {
                event.preventDefault();
                dragCounter--;
+
                if (dragCounter === 0) {
                        isDragOver = false;
                }
@@ -49,8 +51,10 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
 
                if (isEditing()) {
                        const handler = getAddFilesHandler();
+
                        if (handler) {
                                handler(files);
+
                                return;
                        }
                }
@@ -59,14 +63,14 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
        }
 
        return {
-               get isDragOver() {
-                       return isDragOver;
-               },
                dragHandlers: {
                        dragenter: handleDragEnter,
                        dragleave: handleDragLeave,
                        dragover: handleDragOver,
                        drop: handleDrop
+               },
+               get isDragOver() {
+                       return isDragOver;
                }
        };
 }
index f1ab9472184e99892925061fd2488d39bcff8aa7..5bb1ea33f8b4814d0f368ca8fbd7c3acb7769b74 100644 (file)
@@ -7,8 +7,8 @@
  * as reactive getters so validation tracks the model in real time.
  */
 
+import { filterFilesByModalities, isFileTypeSupported } from '$lib/utils';
 import { processFilesToChatUploaded } from '$lib/utils/browser-only';
-import { isFileTypeSupported, filterFilesByModalities } from '$lib/utils';
 
 interface UseChatScreenFileUploadOptions {
        capabilities: () => { hasVision: boolean; hasAudio: boolean; hasVideo: boolean };
@@ -27,8 +27,8 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
        let showFileErrorDialog = $state(false);
        let fileErrorData = $state<FileErrorData>({
                generallyUnsupported: [],
-               modalityUnsupported: [],
                modalityReasons: {},
+               modalityUnsupported: [],
                supportedTypes: []
        });
 
@@ -44,24 +44,26 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
                        }
                }
 
-               const { supportedFiles, unsupportedFiles, modalityReasons } = filterFilesByModalities(
+               const { modalityReasons, supportedFiles, unsupportedFiles } = filterFilesByModalities(
                        generallySupported,
                        options.capabilities()
                );
-
                const allUnsupportedFiles = [...generallyUnsupported, ...unsupportedFiles];
 
                if (allUnsupportedFiles.length > 0) {
                        const supportedTypes: string[] = ['text files', 'PDFs'];
                        const caps = options.capabilities();
+
                        if (caps.hasVision) supportedTypes.push('images');
+
                        if (caps.hasAudio) supportedTypes.push('audio files');
+
                        if (caps.hasVideo) supportedTypes.push('video files');
 
                        fileErrorData = {
                                generallyUnsupported,
-                               modalityUnsupported: unsupportedFiles,
                                modalityReasons,
+                               modalityUnsupported: unsupportedFiles,
                                supportedTypes
                        };
                        showFileErrorDialog = true;
@@ -72,6 +74,7 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
                                supportedFiles,
                                options.activeModelId() ?? undefined
                        );
+
                        uploadedFiles = [...uploadedFiles, ...processed];
                }
        }
@@ -85,20 +88,20 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
        }
 
        return {
-               get uploadedFiles() {
-                       return uploadedFiles;
-               },
-               set uploadedFiles(value) {
-                       uploadedFiles = value;
-               },
+               fileErrorData,
+               handleFileRemove,
+               handleFileUpload,
                get showFileErrorDialog() {
                        return showFileErrorDialog;
                },
                set showFileErrorDialog(value) {
                        showFileErrorDialog = value;
                },
-               fileErrorData,
-               handleFileUpload,
-               handleFileRemove
+               get uploadedFiles() {
+                       return uploadedFiles;
+               },
+               set uploadedFiles(value) {
+                       uploadedFiles = value;
+               }
        };
 }
index cecf208bdec6f4088451f0470be2d03b5720a0a0..004db9bcc06d17dac44e1f2b55a8f7d72f775832 100644 (file)
@@ -7,8 +7,8 @@
  * scroll handler seeing spurious events from layout shifts.
  */
 
-import { afterNavigate, beforeNavigate } from '$app/navigation';
 import type { AutoScrollController } from './use-auto-scroll.svelte';
+import { afterNavigate, beforeNavigate } from '$app/navigation';
 
 export function useChatScreenScroll(autoScroll: AutoScrollController) {
        let chatScrollContainer: HTMLElement | undefined = $state();
@@ -18,6 +18,7 @@ export function useChatScreenScroll(autoScroll: AutoScrollController) {
                // Ignore scroll events caused by navigation layout changes or by our own
                // programmatic scrolls so they don't accidentally disable auto-scroll.
                if (isNavigating || !event.isTrusted) return;
+
                autoScroll.handleScroll();
        }
 
index e11e2f7ab1a58a96c405dc22c2951c9dcaa130bb..76dcd356ec296011ae3895435c0af4197ec82930 100644 (file)
@@ -4,23 +4,23 @@
  * read / fresh / cache / output and cumulative token counts.
  */
 
+import { useProcessingState } from './use-processing-state.svelte';
+import {
+       type ColorLevel,
+       colorLevelFromPercent
+} from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge';
+import { STATS_UNITS } from '$lib/constants';
+import { MessageRole } from '$lib/enums';
+import { chatStore } from '$lib/stores/chat.svelte';
+import { activeMessages } from '$lib/stores/conversations.svelte';
 import {
-       modelsStore,
        modelOptions,
+       modelsStore,
        selectedModelId,
        singleModelName
 } from '$lib/stores/models.svelte';
-import { chatStore } from '$lib/stores/chat.svelte';
-import { activeMessages } from '$lib/stores/conversations.svelte';
 import { isRouterMode } from '$lib/stores/server.svelte';
-import { MessageRole } from '$lib/enums';
-import { STATS_UNITS } from '$lib/constants';
 import type { ChatMessageTimings, DatabaseMessage } from '$lib/types';
-import { useProcessingState } from './use-processing-state.svelte';
-import {
-       colorLevelFromPercent,
-       type ColorLevel
-} from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge';
 
 interface LiveStats {
        freshTokens: number;
@@ -55,8 +55,10 @@ export interface UseContextGaugeReturn {
 function lastAssistantTimings(messages: DatabaseMessage[]): ChatMessageTimings | undefined {
        for (let i = messages.length - 1; i >= 0; i--) {
                const m = messages[i];
+
                if (m.role === MessageRole.ASSISTANT && m.timings) return m.timings;
        }
+
        return undefined;
 }
 
@@ -66,13 +68,15 @@ function deriveLiveStats(
        if (!state || (state.status !== 'preparing' && state.status !== 'generating')) {
                return null;
        }
+
        const promptTokens = state.promptTokens ?? 0;
        const cacheTokens = state.cacheTokens ?? 0;
+
        return {
-               freshTokens: promptTokens,
-               promptTokens: promptTokens + cacheTokens,
                cacheTokens,
-               outputTokens: state.outputTokensUsed ?? 0
+               freshTokens: promptTokens,
+               outputTokens: state.outputTokensUsed ?? 0,
+               promptTokens: promptTokens + cacheTokens
        };
 }
 
@@ -83,13 +87,13 @@ function filterTransientDetails(raw: string[]): string[] {
                if (TRANSIENT_DETAILS_EXCLUDED_PREFIXES.some((prefix) => detail.startsWith(prefix))) {
                        return false;
                }
+
                return !detail.includes(STATS_UNITS.TOKENS_PER_SECOND);
        });
 }
 
 export function useContextGauge(): UseContextGaugeReturn {
        const processingState = useProcessingState();
-
        // Resolve the model the gauge reports context for: explicit selection >
        // last assistant model > single-model mode (mirrors useChatScreenActiveModel).
        const activeModelId = $derived.by(() => {
@@ -98,18 +102,18 @@ export function useContextGauge(): UseContextGaugeReturn {
                }
 
                const selectedId = selectedModelId();
+
                if (selectedId) {
                        const model = modelOptions().find((m) => m.id === selectedId);
+
                        if (model) return model.model;
                }
 
                return chatStore.getConversationModel(activeMessages() as DatabaseMessage[]);
        });
-
        const isActiveModelLoaded = $derived(
                activeModelId !== null && modelsStore.isModelLoaded(activeModelId)
        );
-
        const isActiveModelLoading = $derived(
                activeModelId !== null && modelsStore.isModelOperationInProgress(activeModelId)
        );
@@ -118,6 +122,7 @@ export function useContextGauge(): UseContextGaugeReturn {
        $effect(() => {
                if (activeModelId && isActiveModelLoaded) {
                        const cached = modelsStore.getModelProps(activeModelId);
+
                        if (!cached) {
                                void modelsStore.fetchModelProps(activeModelId);
                        }
@@ -126,52 +131,54 @@ export function useContextGauge(): UseContextGaugeReturn {
 
        const contextTotal = $derived.by(() => {
                void modelsStore.propsCacheVersion;
+
                return activeModelId ? modelsStore.getModelContextSize(activeModelId) : null;
        });
-
        const liveStats = $derived(deriveLiveStats(processingState.processingState));
-
        const currentRead = $derived.by(() => {
                const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
+
                let read = 0;
+
                if (timings) {
                        read = (timings.prompt_n ?? 0) + (timings.cache_n ?? 0);
                }
+
                // live.promptTokens is already the combined reading (prompt + cache),
                // so do not also add live.cacheTokens.
                if (liveStats && liveStats.promptTokens > 0) {
                        read = Math.max(read, liveStats.promptTokens);
                }
+
                return read;
        });
-
        const currentFresh = $derived.by(() => {
                const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
                const fresh = timings?.prompt_n ?? 0;
+
                return Math.max(fresh, liveStats?.freshTokens ?? 0);
        });
-
        const currentCache = $derived.by(() => {
                const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
                const cached = timings?.cache_n ?? 0;
+
                if (liveStats && liveStats.promptTokens > 0) {
                        return Math.max(cached, liveStats.cacheTokens);
                }
+
                return cached;
        });
-
        const currentOutput = $derived.by(() => {
                if (liveStats && liveStats.outputTokens > 0) return liveStats.outputTokens;
+
                const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
+
                return timings?.predicted_n ?? 0;
        });
-
        const kvTotal = $derived(currentRead + currentOutput);
        const contextUsed = $derived(currentRead + currentOutput);
-
        const cumulative = $derived.by(() => {
                const messages = activeMessages() as DatabaseMessage[];
-
                // Agentic sessions stamp the same agentic.llm totals onto every
                // assistant message; cache_n is never per-turn so cache_total stays 0.
                const agenticMessages = messages.filter(
@@ -183,11 +190,12 @@ export function useContextGauge(): UseContextGaugeReturn {
                        const output = llm.predicted_n ?? 0;
                        const outputMs = llm.predicted_ms ?? 0;
                        const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
+
                        return {
-                               read: llm.prompt_n ?? 0,
-                               output,
+                               averageTokensPerSecond,
                                cacheTotal: 0,
-                               averageTokensPerSecond
+                               output,
+                               read: llm.prompt_n ?? 0
                        };
                }
 
@@ -195,27 +203,27 @@ export function useContextGauge(): UseContextGaugeReturn {
                let output = 0;
                let outputMs = 0;
                let cacheTotal = 0;
+
                for (const m of messages) {
                        if (m.role !== MessageRole.ASSISTANT || !m.timings) continue;
+
                        read += m.timings.prompt_n ?? 0;
                        cacheTotal += m.timings.cache_n ?? 0;
                        output += m.timings.predicted_n ?? 0;
                        outputMs += m.timings.predicted_ms ?? 0;
                }
                const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
-               return { read, output, cacheTotal, averageTokensPerSecond };
-       });
 
+               return { averageTokensPerSecond, cacheTotal, output, read };
+       });
        const contextPercent = $derived.by(() => {
                if (contextTotal === null || contextTotal <= 0) return null;
+
                return Math.round((contextUsed / contextTotal) * 100);
        });
-
        const colorLevel = $derived(colorLevelFromPercent(contextPercent));
-
        // Drop lines the surrounding Context / Output / speed rows already render.
        const transientDetails = $derived(filterTransientDetails(processingState.getTechnicalDetails()));
-
        const hasAnyUsage = $derived(
                cumulative.read > 0 ||
                        cumulative.output > 0 ||
@@ -227,6 +235,7 @@ export function useContextGauge(): UseContextGaugeReturn {
 
        async function loadModel() {
                if (!activeModelId || isActiveModelLoading) return;
+
                try {
                        await modelsStore.loadModel(activeModelId);
                } catch {
@@ -238,11 +247,14 @@ export function useContextGauge(): UseContextGaugeReturn {
                get activeModelId() {
                        return activeModelId;
                },
-               get isActiveModelLoaded() {
-                       return isActiveModelLoaded;
+               get averageTokensPerSecond() {
+                       return cumulative.averageTokensPerSecond;
                },
-               get isActiveModelLoading() {
-                       return isActiveModelLoading;
+               get colorLevel() {
+                       return colorLevel;
+               },
+               get contextPercent() {
+                       return contextPercent;
                },
                get contextTotal() {
                        return contextTotal;
@@ -250,46 +262,43 @@ export function useContextGauge(): UseContextGaugeReturn {
                get contextUsed() {
                        return contextUsed;
                },
-               get currentRead() {
-                       return currentRead;
+               get cumulativeCacheTotal() {
+                       return cumulative.cacheTotal;
                },
-               get currentFresh() {
-                       return currentFresh;
+               get cumulativeOutput() {
+                       return cumulative.output;
+               },
+               get cumulativeRead() {
+                       return cumulative.read;
                },
                get currentCache() {
                        return currentCache;
                },
+               get currentFresh() {
+                       return currentFresh;
+               },
                get currentOutput() {
                        return currentOutput;
                },
-               get kvTotal() {
-                       return kvTotal;
-               },
-               get cumulativeRead() {
-                       return cumulative.read;
-               },
-               get cumulativeOutput() {
-                       return cumulative.output;
+               get currentRead() {
+                       return currentRead;
                },
-               get cumulativeCacheTotal() {
-                       return cumulative.cacheTotal;
+               get hasAnyUsage() {
+                       return hasAnyUsage;
                },
-               get averageTokensPerSecond() {
-                       return cumulative.averageTokensPerSecond;
+               get isActiveModelLoaded() {
+                       return isActiveModelLoaded;
                },
-               get contextPercent() {
-                       return contextPercent;
+               get isActiveModelLoading() {
+                       return isActiveModelLoading;
                },
-               get colorLevel() {
-                       return colorLevel;
+               get kvTotal() {
+                       return kvTotal;
                },
+               loadModel,
+               startMonitoring: () => processingState.startMonitoring(),
                get transientDetails() {
                        return transientDetails;
-               },
-               get hasAnyUsage() {
-                       return hasAnyUsage;
-               },
-               loadModel,
-               startMonitoring: () => processingState.startMonitoring()
+               }
        };
 }
index cb838ff3d349ceb918b4bea8f3a3b80e2b82cb0b..f4f8d5db146d5714c10752c38555c0675fe917fb 100644 (file)
@@ -33,14 +33,17 @@ export function useDebouncedSearch(opts: UseDebouncedSearchOptions) {
 
        const schedule = debounce((query: string) => {
                if (!opts.canRun() || query !== opts.getQuery().trim()) return;
+
                void start(query);
        }, opts.debounceMs);
 
        async function start(query: string) {
                cancel();
                const fresh = new AbortController();
+
                controller = fresh;
                const mySeq = ++searchSeq;
+
                isSearching = true;
                try {
                        await opts.run(query, fresh.signal, () => isCurrent(mySeq));
@@ -50,17 +53,17 @@ export function useDebouncedSearch(opts: UseDebouncedSearchOptions) {
        }
 
        return {
+               cancel,
                get isSearching() {
                        return isSearching;
                },
-               /** Bump the loading flag synchronously (e.g. before the debounce fires). */
-               setLoading(value: boolean) {
-                       isSearching = value;
-               },
                run(query: string) {
                        schedule(query);
                },
-               cancel
+               /** Bump the loading flag synchronously (e.g. before the debounce fires). */
+               setLoading(value: boolean) {
+                       isSearching = value;
+               }
        };
 }
 
index 11305b20552037d4936babf4adb6dc09ae652f27..d7ef140c5776740be15ba5f8a9d26cb107384cf4 100644 (file)
@@ -1,6 +1,6 @@
-import { onMount } from 'svelte';
 import { afterNavigate, beforeNavigate } from '$app/navigation';
 import { draftMessagesStore } from '$lib/stores/draft-messages.svelte';
+import { onMount } from 'svelte';
 
 interface UseDraftMessagesOptions {
        getChatId: () => string | undefined;
@@ -24,6 +24,7 @@ export function useDraftMessages(options: UseDraftMessagesOptions) {
 
        beforeNavigate(() => {
                const chatId = options.getChatId();
+
                draftMessagesStore.saveDraftMessage(chatId, options.getMessage(), options.getFiles());
        });
 
@@ -31,6 +32,7 @@ export function useDraftMessages(options: UseDraftMessagesOptions) {
                if (navigation?.from != null) {
                        const chatId = options.getChatId();
                        const draft = draftMessagesStore.getDraftMessage(chatId);
+
                        options.setMessage(draft.message);
                        options.setFiles(draft.files);
                }
@@ -38,6 +40,7 @@ export function useDraftMessages(options: UseDraftMessagesOptions) {
 
        function clearDraft() {
                const chatId = options.getChatId();
+
                draftMessagesStore.clearDraftMessage(chatId);
        }
 
index 61df30b79a56a70ddd33e3bf8fdcbfed353138f3..f99b273e8445e89729b557354fdffc7a07639587 100644 (file)
@@ -1,6 +1,6 @@
 import { goto } from '$app/navigation';
-import { KeyboardKey } from '$lib/enums';
 import { ROUTES } from '$lib/constants/routes';
+import { KeyboardKey } from '$lib/enums';
 
 interface KeyboardShortcutsCallbacks {
        activateSearchMode?: () => void;
index c0bede1dd0c76329dd6ef53c37ee613f32ee2be5..05436ecc8d255c8513234f2acdb6afae5d57e628 100644 (file)
@@ -63,11 +63,15 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
                const order = options.orderedIds();
                const fromIdx = order.indexOf(fromId);
                const toIdx = order.indexOf(toId);
+
                if (fromIdx === -1 || toIdx === -1) return;
+
                const [lo, hi] = fromIdx < toIdx ? [fromIdx, toIdx] : [toIdx, fromIdx];
                const shouldSelect = !selected.has(toId);
+
                for (let i = lo; i <= hi; i++) {
                        const id = order[i];
+
                        if (shouldSelect) selected.add(id);
                        else selected.delete(id);
                }
@@ -77,22 +81,27 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
                const attr = resolveAttributeName();
                const selector = `[data-${attr}]`;
                const key = datasetKey(attr);
+
                let bestMatch: HTMLElement | null = null;
                let bestCenterDistance = Infinity;
 
                for (const row of document.querySelectorAll<HTMLElement>(selector)) {
                        const rect = row.getBoundingClientRect();
+
                        if (y >= rect.top && y <= rect.bottom && x >= rect.left && x <= rect.right) {
                                return row.dataset[key] ?? null;
                        }
+
                        if (x >= rect.left && x <= rect.right) {
                                const centerDistance = Math.abs(y - (rect.top + rect.height / 2));
+
                                if (centerDistance < bestCenterDistance) {
                                        bestCenterDistance = centerDistance;
                                        bestMatch = row;
                                }
                        }
                }
+
                return bestMatch ? (bestMatch.dataset[key] ?? null) : null;
        }
 
@@ -109,6 +118,7 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
 
                for (const row of document.querySelectorAll<HTMLElement>(selector)) {
                        const id = row.dataset[key];
+
                        if (!id || !visibleIds.has(id)) continue;
 
                        const rect = row.getBoundingClientRect();
@@ -132,17 +142,22 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
 
                if (event.shiftKey && dragAnchorId !== null) {
                        const target = findRowAtPoint(event.clientX, event.clientY);
+
                        if (target && target !== mousedownRowId) rangeSelect(dragAnchorId, target);
+
                        return;
                }
 
                if (!isMarqueeDragging) {
                        const dx = event.clientX - dragStartX;
                        const dy = event.clientY - dragStartY;
+
                        if (Math.hypot(dx, dy) < dragThresholdPx) return;
+
                        isMarqueeDragging = true;
                        dragMode = decideDragMode(mousedownRowId, options.selectedIds());
                }
+
                updateMarqueeRect(event.clientX, event.clientY);
        }
 
@@ -150,8 +165,10 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
                if (isMarqueeDragging) {
                        suppressNextClick = true;
                        const target = findRowAtPoint(event.clientX, event.clientY);
+
                        if (target) dragAnchorId = target;
                }
+
                isMarqueeDragging = false;
                mouseDownActive = false;
                mousedownRowId = null;
@@ -171,11 +188,14 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
        $effect(() => {
                if (!options.enabled()) {
                        reset();
+
                        return;
                }
+
                document.addEventListener('mousemove', handleDocumentMouseMove);
                document.addEventListener('mouseup', handleDocumentMouseUp);
                document.addEventListener('click', handleClickCapture, { capture: true });
+
                return () => {
                        document.removeEventListener('mousemove', handleDocumentMouseMove);
                        document.removeEventListener('mouseup', handleDocumentMouseUp);
@@ -185,7 +205,9 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
 
        function rowMouseDown(id: string, event: MouseEvent) {
                if (!options.enabled()) return;
+
                if (event.button !== 0) return;
+
                event.preventDefault();
                mouseDownActive = true;
                mousedownRowId = id;
@@ -197,10 +219,12 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
 
        function rowClick(id: string, shiftKey: boolean) {
                if (!options.enabled()) return;
+
                const selected = options.selectedIds();
 
                if (shiftKey) {
                        const anchor = dragAnchorId;
+
                        if (anchor !== null && anchor !== id) {
                                rangeSelect(anchor, id);
                        } else if (selected.has(id)) {
@@ -208,12 +232,15 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
                        } else {
                                selected.add(id);
                        }
+
                        dragAnchorId = id;
+
                        return;
                }
 
                if (selected.has(id)) selected.delete(id);
                else selected.add(id);
+
                dragAnchorId = id;
        }
 
@@ -229,11 +256,11 @@ export function useMarqueeSelection(options: UseMarqueeSelectionOptions) {
        }
 
        return {
-               rowMouseDown,
-               rowClick,
-               reset,
                get dragAnchorId() {
                        return dragAnchorId;
-               }
+               },
+               reset,
+               rowClick,
+               rowMouseDown
        };
 }
index 71d1b66f8d3f22f038ce8ae2b9cd3657c4632233..2716754045797086be6ecdfacc7523e001e55171 100644 (file)
@@ -24,13 +24,16 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
 
        async function handleSaveEdit() {
                const trimmed = editedContent.trim();
+
                if (!trimmed && editedExtras.length === 0 && editedUploadedFiles.length === 0) return;
 
                let finalExtras: DatabaseMessageExtra[] = $state.snapshot(editedExtras);
+
                if (editedUploadedFiles.length > 0) {
                        const plainFiles = $state.snapshot(editedUploadedFiles);
                        const result = await parseFilesToMessageExtras(plainFiles);
                        const newExtras = result?.extras || [];
+
                        finalExtras = [...finalExtras, ...newExtras];
                }
 
@@ -43,9 +46,7 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
        }
 
        setMessageEditContext({
-               get isEditing() {
-                       return isEditing;
-               },
+               cancel: handleCancelEdit,
                get editedContent() {
                        return editedContent;
                },
@@ -55,24 +56,20 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
                get editedUploadedFiles() {
                        return editedUploadedFiles;
                },
+               get isEditing() {
+                       return isEditing;
+               },
+               get messageRole() {
+                       return MessageRole.USER;
+               },
                get originalContent() {
                        return options.getContent();
                },
                get originalExtras() {
                        return options.getExtras();
                },
-               get showSaveOnlyOption() {
-                       return options.showSaveOnlyOption ?? false;
-               },
-               get showBranchAfterEditOption() {
-                       return false;
-               },
-               get shouldBranchAfterEdit() {
-                       return false;
-               },
-               get messageRole() {
-                       return MessageRole.USER;
-               },
+               save: handleSaveEdit,
+               saveOnly: handleSaveEdit,
                setContent: (c: string) => {
                        editedContent = c;
                },
@@ -82,18 +79,24 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) {
                setUploadedFiles: (f: ChatUploadedFile[]) => {
                        editedUploadedFiles = f;
                },
-               save: handleSaveEdit,
-               saveOnly: handleSaveEdit,
-               cancel: handleCancelEdit,
+               get shouldBranchAfterEdit() {
+                       return false;
+               },
+               get showBranchAfterEditOption() {
+                       return false;
+               },
+               get showSaveOnlyOption() {
+                       return options.showSaveOnlyOption ?? false;
+               },
                startEdit: handleEdit
        });
 
        return {
-               get isEditing() {
-                       return isEditing;
-               },
+               handleCancelEdit,
                handleEdit,
                handleSaveEdit,
-               handleCancelEdit
+               get isEditing() {
+                       return isEditing;
+               }
        };
 }
index 44764ee5edc89068b09e41454b68307b1fab7d14..f554643c60f7440081479eae78f037abe89bdd66 100644 (file)
@@ -1,16 +1,16 @@
-import { onMount } from 'svelte';
+import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils';
+import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
 import {
-       modelsStore,
        modelOptions,
        modelsLoading,
+       modelsStore,
        modelsUpdating,
        selectedModelId,
        singleModelName
 } from '$lib/stores/models.svelte';
 import { isRouterMode } from '$lib/stores/server.svelte';
-import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
-import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils';
 import type { ModelOption } from '$lib/types/models';
+import { onMount } from 'svelte';
 
 export interface UseModelsSelectorOptions {
        currentModel: () => string | null;
@@ -65,18 +65,18 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
        const activeId = $derived(selectedModelId());
        const isRouter = $derived(isRouterMode());
        const serverModel = $derived(singleModelName());
-
        const currentModel = $derived(opts.currentModel());
        const onModelChange = $derived(opts.onModelChange?.());
-
        const isHighlightedCurrentModelActive = $derived.by(() => {
                if (!isRouter || !currentModel) return false;
+
                const currentOption = options.find((option) => option.model === currentModel);
+
                return currentOption ? currentOption.id === activeId : false;
        });
-
        const isCurrentModelInCache = $derived.by(() => {
                if (!isRouter || !currentModel) return true;
+
                return options.some((option) => option.model === currentModel);
        });
 
@@ -84,6 +84,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
        let searchTerm = $state('');
        let showModelDialog = $state(false);
        let infoModelId = $state<string | null>(null);
+
        const filteredOptions = $derived(filterModelOptions(options, searchTerm));
        const groupedFilteredOptions = $derived(
                groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) =>
@@ -122,6 +123,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
 
        async function handleSelect(modelId: string) {
                const option = options.find((opt) => opt.id === modelId);
+
                if (!option) return;
 
                let shouldCloseMenu = true;
@@ -162,10 +164,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
 
                        if (displayModel) {
                                return {
+                                       capabilities: [],
                                        id: serverModel ? 'current' : 'offline-current',
                                        model: displayModel,
-                                       name: displayModel.split('/').pop() || displayModel,
-                                       capabilities: []
+                                       name: displayModel.split('/').pop() || displayModel
                                };
                        }
 
@@ -175,10 +177,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
                if (currentModel) {
                        if (!isCurrentModelInCache) {
                                return {
+                                       capabilities: [],
                                        id: 'not-in-cache',
                                        model: currentModel,
-                                       name: currentModel.split('/').pop() || currentModel,
-                                       capabilities: []
+                                       name: currentModel.split('/').pop() || currentModel
                                };
                        }
 
@@ -193,60 +195,64 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
        }
 
        return {
-               get options() {
-                       return options;
+               get activeId() {
+                       return activeId;
                },
 
-               get loading() {
-                       return loading;
+               get filteredOptions() {
+                       return filteredOptions;
                },
 
-               get updating() {
-                       return updating;
+               getDisplayOption,
+
+               get groupedFilteredOptions() {
+                       return groupedFilteredOptions;
                },
 
-               get activeId() {
-                       return activeId;
+               handleInfoClick,
+
+               handleOpenChange,
+
+               handleSelect,
+
+               get infoModelId() {
+                       return infoModelId;
                },
 
-               get isRouter() {
-                       return isRouter;
+               get isCurrentModelInCache() {
+                       return isCurrentModelInCache;
                },
 
-               get serverModel() {
-                       return serverModel;
+               isFavorite(model: string) {
+                       return modelsStore.favoriteModelIds.has(model);
                },
 
                get isHighlightedCurrentModelActive() {
                        return isHighlightedCurrentModelActive;
                },
 
-               get isCurrentModelInCache() {
-                       return isCurrentModelInCache;
+               get isLoadingModel() {
+                       return isLoadingModel;
                },
 
-               get filteredOptions() {
-                       return filteredOptions;
+               get isRouter() {
+                       return isRouter;
                },
 
-               get groupedFilteredOptions() {
-                       return groupedFilteredOptions;
+               get loading() {
+                       return loading;
                },
 
-               get isLoadingModel() {
-                       return isLoadingModel;
+               get options() {
+                       return options;
                },
 
                get searchTerm() {
                        return searchTerm;
                },
 
-               get showModelDialog() {
-                       return showModelDialog;
-               },
-
-               get infoModelId() {
-                       return infoModelId;
+               get serverModel() {
+                       return serverModel;
                },
 
                setSearchTerm(value: string) {
@@ -257,16 +263,12 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
                        showModelDialog = value;
                },
 
-               handleInfoClick,
-
-               handleSelect,
-
-               handleOpenChange,
-
-               isFavorite(model: string) {
-                       return modelsStore.favoriteModelIds.has(model);
+               get showModelDialog() {
+                       return showModelDialog;
                },
 
-               getDisplayOption
+               get updating() {
+                       return updating;
+               }
        };
 }
index 08a677ccfe89072dbcb1257e9e5a09de973b0a86..f986525cd0aac773aabe3c16a2b50864aef1db29 100644 (file)
@@ -28,13 +28,17 @@ export function usePickerNavigation(opts: UsePickerNavigationOptions) {
 
        function resolve(from: number, dir: 1 | -1): number {
                const n = opts.count();
+
                if (n === 0) return -1;
+
                if (opts.step) return opts.step(from, dir);
+
                return wrapStep(from, dir, n);
        }
 
        function move(dir: 1 | -1) {
                const next = resolve(hoveredIndex, dir);
+
                if (next >= 0) {
                        hoveredIndex = next;
                        scrollTrigger++;
@@ -62,18 +66,21 @@ export function usePickerNavigation(opts: UsePickerNavigationOptions) {
                if (event.key === KeyboardKey.ESCAPE) {
                        event.preventDefault();
                        opts.onClose();
+
                        return true;
                }
 
                if (event.key === KeyboardKey.ARROW_DOWN) {
                        event.preventDefault();
                        move(1);
+
                        return true;
                }
 
                if (event.key === KeyboardKey.ARROW_UP) {
                        event.preventDefault();
                        move(-1);
+
                        return true;
                }
 
@@ -81,8 +88,10 @@ export function usePickerNavigation(opts: UsePickerNavigationOptions) {
                        if (hoveredIndex >= 0 && hoveredIndex < opts.count()) {
                                event.preventDefault();
                                opts.onSelect(hoveredIndex);
+
                                return true;
                        }
+
                        // No selectable row - let the caller's Enter-to-submit run.
                        return false;
                }
@@ -91,17 +100,17 @@ export function usePickerNavigation(opts: UsePickerNavigationOptions) {
        }
 
        return {
+               bumpScroll,
+               handleKeydown,
                get hoveredIndex() {
                        return hoveredIndex;
                },
+               move,
+               reset,
                get scrollTrigger() {
                        return scrollTrigger;
                },
-               reset,
-               setHover,
-               move,
-               bumpScroll,
-               handleKeydown
+               setHover
        };
 }
 
index 9fbda75d67240b5842cef797da56043cc7772d85..0087c5683daa7c28d6a815cbc00fd0903cfd691f 100644 (file)
@@ -1,6 +1,6 @@
-import { activeProcessingState } from '$lib/stores/chat.svelte';
 import { STATS_UNITS } from '$lib/constants';
-import type { ApiProcessingState, LiveProcessingStats, LiveGenerationStats } from '$lib/types';
+import { activeProcessingState } from '$lib/stores/chat.svelte';
+import type { ApiProcessingState, LiveGenerationStats, LiveProcessingStats } from '$lib/types';
 
 export interface UseProcessingStateReturn {
        readonly processingState: ApiProcessingState | null;
@@ -41,6 +41,7 @@ export function useProcessingState(): UseProcessingStateReturn {
                if (!isMonitoring) {
                        return lastKnownState;
                }
+
                // Read directly from the reactive state export
                return activeProcessingState();
        });
@@ -54,17 +55,18 @@ export function useProcessingState(): UseProcessingStateReturn {
        // Track last known processing stats for when promptProgress disappears
        $effect(() => {
                if (processingState?.promptProgress) {
-                       const { processed, total, time_ms, cache } = processingState.promptProgress;
+                       const { cache, processed, time_ms, total } = processingState.promptProgress;
                        const actualProcessed = processed - cache;
                        const actualTotal = total - cache;
 
                        if (actualProcessed > 0 && time_ms > 0) {
                                const tokensPerSecond = actualProcessed / (time_ms / 1000);
+
                                lastKnownProcessingStats = {
-                                       tokensProcessed: actualProcessed,
-                                       totalTokens: actualTotal,
                                        timeMs: time_ms,
-                                       tokensPerSecond
+                                       tokensPerSecond,
+                                       tokensProcessed: actualProcessed,
+                                       totalTokens: actualTotal
                                };
                        }
                }
@@ -76,11 +78,13 @@ export function useProcessingState(): UseProcessingStateReturn {
                        done === 0 || elapsedSecs < 0.5
                                ? undefined // can be the case for the 0% progress report
                                : elapsedSecs * (total / done - 1);
+
                return progressETASecs;
        }
 
        function startMonitoring(): void {
                if (isMonitoring) return;
+
                isMonitoring = true;
        }
 
@@ -102,6 +106,7 @@ export function useProcessingState(): UseProcessingStateReturn {
                                if (processingState.progressPercent !== undefined) {
                                        return `Processing (${processingState.progressPercent}%)`;
                                }
+
                                return 'Preparing response...';
                        case 'generating':
                                return '';
@@ -113,6 +118,7 @@ export function useProcessingState(): UseProcessingStateReturn {
        function getProcessingDetails(): string[] {
                // Use current processing state or fall back to last known state
                const stateToUse = processingState || lastKnownState;
+
                if (!stateToUse) {
                        return [];
                }
@@ -121,7 +127,7 @@ export function useProcessingState(): UseProcessingStateReturn {
 
                // Show prompt processing progress with ETA during preparation phase
                if (stateToUse.promptProgress) {
-                       const { processed, total, time_ms, cache } = stateToUse.promptProgress;
+                       const { cache, processed, time_ms, total } = stateToUse.promptProgress;
                        const actualProcessed = processed - cache;
                        const actualTotal = total - cache;
 
@@ -131,6 +137,7 @@ export function useProcessingState(): UseProcessingStateReturn {
 
                                if (eta !== undefined) {
                                        const etaSecs = Math.ceil(eta);
+
                                        details.push(`Processing ${percent}% (ETA: ${etaSecs}s)`);
                                } else {
                                        details.push(`Processing ${percent}%`);
@@ -182,6 +189,7 @@ export function useProcessingState(): UseProcessingStateReturn {
         */
        function getTechnicalDetails(): string[] {
                const stateToUse = processingState || lastKnownState;
+
                if (!stateToUse) {
                        return [];
                }
@@ -237,8 +245,7 @@ export function useProcessingState(): UseProcessingStateReturn {
        function getPromptProgressText(): string | null {
                if (!processingState?.promptProgress) return null;
 
-               const { processed, total, cache } = processingState.promptProgress;
-
+               const { cache, processed, total } = processingState.promptProgress;
                const actualProcessed = processed - cache;
                const actualTotal = total - cache;
                const percent = Math.round((actualProcessed / actualTotal) * 100);
@@ -246,6 +253,7 @@ export function useProcessingState(): UseProcessingStateReturn {
 
                if (eta !== undefined) {
                        const etaSecs = Math.ceil(eta);
+
                        return `Processing ${percent}% (ETA: ${etaSecs}s)`;
                }
 
@@ -258,8 +266,7 @@ export function useProcessingState(): UseProcessingStateReturn {
         */
        function getLiveProcessingStats(): LiveProcessingStats | null {
                if (processingState?.promptProgress) {
-                       const { processed, total, time_ms, cache } = processingState.promptProgress;
-
+                       const { cache, processed, time_ms, total } = processingState.promptProgress;
                        const actualProcessed = processed - cache;
                        const actualTotal = total - cache;
 
@@ -267,10 +274,10 @@ export function useProcessingState(): UseProcessingStateReturn {
                                const tokensPerSecond = actualProcessed / (time_ms / 1000);
 
                                return {
-                                       tokensProcessed: actualProcessed,
-                                       totalTokens: actualTotal,
                                        timeMs: time_ms,
-                                       tokensPerSecond
+                                       tokensPerSecond,
+                                       tokensProcessed: actualProcessed,
+                                       totalTokens: actualTotal
                                };
                        }
                }
@@ -294,22 +301,22 @@ export function useProcessingState(): UseProcessingStateReturn {
                        tokensPerSecond && tokensPerSecond > 0 ? (tokensDecoded / tokensPerSecond) * 1000 : 0;
 
                return {
-                       tokensGenerated: tokensDecoded,
                        timeMs,
+                       tokensGenerated: tokensDecoded,
                        tokensPerSecond: tokensPerSecond || 0
                };
        }
 
        return {
-               get processingState() {
-                       return processingState;
-               },
+               getLiveGenerationStats,
+               getLiveProcessingStats,
                getProcessingDetails,
-               getTechnicalDetails,
                getProcessingMessage,
                getPromptProgressText,
-               getLiveProcessingStats,
-               getLiveGenerationStats,
+               getTechnicalDetails,
+               get processingState() {
+                       return processingState;
+               },
                shouldShowDetails,
                startMonitoring,
                stopMonitoring
index e1f46e1bc2771e128037124374e9fd76b5be5cef..1a02710677b0b093388bb760068d151610c41aa8 100644 (file)
@@ -1,8 +1,8 @@
 import { browser } from '$app/environment';
-import { useRegisterSW } from 'virtual:pwa-register/svelte';
-import { versionStore } from '$lib/stores/version.svelte';
-import { BUILD_VERSION_LOCALSTORAGE_KEY } from '$lib/constants/storage';
 import { SW_CONFIG } from '$lib/constants/pwa';
+import { BUILD_VERSION_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { versionStore } from '$lib/stores/version.svelte';
+import { useRegisterSW } from 'virtual:pwa-register/svelte';
 
 /**
  * Hook for PWA service worker registration, update polling, and build version mismatch detection.
@@ -24,6 +24,7 @@ export function usePwa() {
                        if (swCheckInterval) {
                                clearInterval(swCheckInterval);
                        }
+
                        swCheckInterval = setInterval(async () => {
                                if (!r || r.installing || !navigator?.onLine) return;
 
@@ -35,6 +36,7 @@ export function usePwa() {
                                                        'cache-control': SW_CONFIG.UPDATE_FETCH_OPTIONS.HEADERS.CACHE_CONTROL
                                                }
                                        });
+
                                        if (resp?.status === 200) {
                                                await r.update();
                                        }
@@ -53,14 +55,17 @@ export function usePwa() {
        // This comparison detects server upgrades for non-PWA users.
        $effect(() => {
                if (!browser) return;
+
                // PWA pages update via the service worker path; the storage check is the non-PWA fallback only
                if (navigator.serviceWorker?.controller) return;
 
                const currentVersion = versionStore.value;
+
                if (!currentVersion) return;
 
                try {
                        const storedVersion = localStorage.getItem(BUILD_VERSION_LOCALSTORAGE_KEY);
+
                        needRefreshByStorage = !!storedVersion && storedVersion !== currentVersion;
                        localStorage.setItem(BUILD_VERSION_LOCALSTORAGE_KEY, currentVersion);
                } catch {
@@ -73,10 +78,10 @@ export function usePwa() {
                get needRefresh() {
                        return pwaNeedRefresh;
                },
-               updateServiceWorker,
                /** Version mismatch detected via localStorage (non-PWA users) */
                get needRefreshByStorage() {
                        return needRefreshByStorage;
-               }
+               },
+               updateServiceWorker
        };
 }
index ce9b77884d2aa9574083116171c54e62ae0a5bdc..6c47b160fe0be60d8b1f38b0796175bb1ee6262f 100644 (file)
@@ -1,18 +1,18 @@
-import { ReasoningEffort } from '$lib/enums';
 import { REASONING_EFFORT_LEVELS } from '$lib/constants/reasoning-effort';
 import { REASONING_EFFORT_TOKENS } from '$lib/constants/reasoning-effort-tokens';
-import type { ReasoningEffortLevel } from '$lib/types';
-import type { DatabaseMessage } from '$lib/types/database';
+import { ReasoningEffort } from '$lib/enums';
+import { chatStore } from '$lib/stores/chat.svelte';
+import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
 import {
-       modelsStore,
        checkModelSupportsThinking,
-       supportsThinking,
+       loadedModelIds,
+       modelsStore,
        propsCacheVersion,
-       loadedModelIds
+       supportsThinking
 } from '$lib/stores/models.svelte';
-import { chatStore } from '$lib/stores/chat.svelte';
-import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
 import { isRouterMode } from '$lib/stores/server.svelte';
+import type { ReasoningEffortLevel } from '$lib/types';
+import type { DatabaseMessage } from '$lib/types/database';
 
 export interface UseReasoningMenuReturn {
        readonly modelSupportsThinking: boolean;
@@ -36,62 +36,64 @@ export function useReasoningMenu(): UseReasoningMenuReturn {
        const conversationModel = $derived(
                chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
        );
-
        // a router chat can carry reasoning from an earlier turn before the props
        // cache is primed, so a model that already produced thinking still qualifies
        const modelSupportsThinkingFromMessages = $derived.by(() => {
                const modelId = isRouterMode() ? modelsStore.selectedModelName || conversationModel : null;
+
                if (!modelId) return false;
 
                return conversationsStore.activeMessages.some(
                        (m) => m.role === 'assistant' && m.model === modelId && !!m.reasoningContent
                );
        });
-
        const modelSupportsThinking = $derived.by(() => {
                loadedModelIds();
                propsCacheVersion();
 
                if (isRouterMode()) {
                        const modelId = modelsStore.selectedModelName || conversationModel;
+
                        return checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages;
                }
 
                return supportsThinking() || modelSupportsThinkingFromMessages;
        });
-
        const currentEffort = $derived(conversationsStore.getReasoningEffort());
        const thinkingEnabled = $derived(
                currentEffort !== ReasoningEffort.OFF && currentEffort !== ReasoningEffort.DEFAULT
        );
 
        return {
-               get modelSupportsThinking() {
-                       return modelSupportsThinking;
-               },
-               get thinkingEnabled() {
-                       return thinkingEnabled;
+               get currentEffort() {
+                       return currentEffort;
                },
                get isOff() {
                        return currentEffort === ReasoningEffort.OFF;
                },
-               get currentEffort() {
-                       return currentEffort;
+               isSelected(level: ReasoningEffortLevel): boolean {
+                       return currentEffort === level.value;
                },
                get levels() {
                        return REASONING_EFFORT_LEVELS;
                },
-               isSelected(level: ReasoningEffortLevel): boolean {
-                       return currentEffort === level.value;
+               get modelSupportsThinking() {
+                       return modelSupportsThinking;
+               },
+               select(level: ReasoningEffortLevel): void {
+                       conversationsStore.setReasoningEffort(level.value as ReasoningEffort);
+               },
+               get thinkingEnabled() {
+                       return thinkingEnabled;
                },
                tokenLabel(level: ReasoningEffortLevel): string | null {
                        if (level.value === ReasoningEffort.DEFAULT) return 'Model default';
+
                        const tokens = REASONING_EFFORT_TOKENS[level.value];
+
                        if (tokens === undefined) return null;
+
                        return tokens === -1 ? 'Unlimited' : `Max ${tokens.toLocaleString()} tokens`;
-               },
-               select(level: ReasoningEffortLevel): void {
-                       conversationsStore.setReasoningEffort(level.value as ReasoningEffort);
                }
        };
 }
index 9f44b7b8f88787eff7a10382dff68e0b5d43021a..3e7cf0544044c0bbdb2456840a4a88873081991f 100644 (file)
@@ -20,6 +20,7 @@ export function useScrollActiveRow(opts: UseScrollActiveRowOptions) {
 
        $effect(() => {
                const trigger = opts.getTrigger();
+
                if (trigger === undefined) return;
 
                // Skip the initial run on mount: the list opens with the first row
@@ -27,18 +28,23 @@ export function useScrollActiveRow(opts: UseScrollActiveRowOptions) {
                // positioned, which would scroll the whole page to the top.
                if (lastTrigger === null) {
                        lastTrigger = trigger;
+
                        return;
                }
 
                if (trigger === lastTrigger) return;
+
                lastTrigger = trigger;
                untrack(() => {
                        const container = opts.getContainer();
                        const index = opts.getIndex();
+
                        if (!container || index < 0 || index >= opts.getCount()) return;
+
                        const row = container.querySelector(
                                `[data-${opts.dataIndex}-index="${index}"]`
                        ) as HTMLElement | null;
+
                        row?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
                });
        });
index e4c75d236504bd6ca84b49a4fc3b31174178018d..d23b8536b86829406d1f6c37b4d79ed7d345ce02 100644 (file)
@@ -8,28 +8,30 @@ export function useScrollCarousel() {
 
                const containerRect = scrollContainer.getBoundingClientRect();
                const elementRect = element.getBoundingClientRect();
-
                const elementCenter = elementRect.left + elementRect.width / 2;
                const containerCenter = containerRect.left + containerRect.width / 2;
                const scrollOffset = elementCenter - containerCenter;
 
-               scrollContainer.scrollBy({ left: scrollOffset, behavior: 'smooth' });
+               scrollContainer.scrollBy({ behavior: 'smooth', left: scrollOffset });
        }
 
        function scrollLeft() {
                if (!scrollContainer) return;
-               scrollContainer.scrollBy({ left: -250, behavior: 'smooth' });
+
+               scrollContainer.scrollBy({ behavior: 'smooth', left: -250 });
        }
 
        function scrollRight() {
                if (!scrollContainer) return;
-               scrollContainer.scrollBy({ left: 250, behavior: 'smooth' });
+
+               scrollContainer.scrollBy({ behavior: 'smooth', left: 250 });
        }
 
        function updateScrollButtons() {
                if (!scrollContainer) return;
 
-               const { scrollLeft: sl, scrollWidth, clientWidth } = scrollContainer;
+               const { clientWidth, scrollLeft: sl, scrollWidth } = scrollContainer;
+
                canScrollLeft = sl > 0;
                canScrollRight = sl < scrollWidth - clientWidth - 1;
        }
@@ -53,9 +55,9 @@ export function useScrollCarousel() {
                set scrollContainer(el: HTMLDivElement | undefined) {
                        scrollContainer = el;
                },
-               scrollToCenter,
                scrollLeft,
                scrollRight,
+               scrollToCenter,
                updateScrollButtons
        };
 }
index 3cbcaaeda5f3904894058d9d3a553ffd85e57909..e326d0c8abe3773d37c0c7ca4c9cd226546ed2dd 100644 (file)
@@ -1,7 +1,7 @@
-import { page } from '$app/state';
 import { beforeNavigate } from '$app/navigation';
-import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
+import { page } from '$app/state';
 import { ROUTES } from '$lib/constants/routes';
+import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
 
 export interface ChatSettings {
        reset: () => void;
@@ -12,10 +12,9 @@ export function useSettingsNavigation() {
                activePanel: 'chat' as 'chat' | 'settings' | 'mcp',
                chatSettingsRef: undefined as ChatSettings | undefined
        });
-
        const isSettingsRoute = $derived(!!page.route.id?.startsWith('/settings'));
 
-       beforeNavigate(({ to, from }) => {
+       beforeNavigate(({ from, to }) => {
                if (to?.route?.id?.startsWith('/settings') && !from?.route?.id?.startsWith('/settings')) {
                        settingsReferrer.url = window.location.hash || ROUTES.START;
                }
@@ -35,12 +34,12 @@ export function useSettingsNavigation() {
        });
 
        return {
-               get panel() {
-                       return subroute;
-               },
-
                get isSettingsRoute() {
                        return isSettingsRoute;
+               },
+
+               get panel() {
+                       return subroute;
                }
        };
 }
index fe4d1e457fbe18cf4ba237d6062536c5f785a199..1ffb2a4d4f7a4973f75ab188a24d49135e2b4d72 100644 (file)
@@ -1,10 +1,10 @@
 import { CLI_FLAGS } from '$lib/constants';
-import { SvelteSet } from 'svelte/reactivity';
 import { ToolSource } from '$lib/enums';
 import { conversationsStore } from '$lib/stores/conversations.svelte';
 import { mcpStore } from '$lib/stores/mcp.svelte';
 import { toolsStore } from '$lib/stores/tools.svelte';
 import type { ToolGroup } from '$lib/types';
+import { SvelteSet } from 'svelte/reactivity';
 
 export interface UseToolsPanelReturn {
        readonly expandedGroups: SvelteSet<string>;
@@ -31,7 +31,6 @@ export interface UseToolsPanelReturn {
  */
 export function useToolsPanel(): UseToolsPanelReturn {
        const expandedGroups = new SvelteSet<string>();
-
        const groups = $derived(toolsStore.toolGroups);
        const activeGroups = $derived(
                groups.filter(
@@ -44,13 +43,17 @@ export function useToolsPanel(): UseToolsPanelReturn {
        const totalToolCount = $derived(activeGroups.reduce((n, g) => n + g.tools.length, 0));
        const noToolsInfoMessage = $derived.by(() => {
                if (toolsStore.loading) return null;
+
                if (toolsStore.toolGroups.length > 0) return null;
+
                // Tools endpoint is unreachable (404) — server started without --tools
                if (toolsStore.isToolsEndpointUnreachable) {
                        return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
                }
+
                // Other errors — return null so UI shows "Failed to load tools"
                if (toolsStore.error) return null;
+
                return `To enable Built-In Tools you need to run llama-server with ${CLI_FLAGS.TOOLS} all or ${CLI_FLAGS.TOOLS} <name> flag. To see MCP Tools you need to add / enable MCP Server(s).`;
        });
 
@@ -87,7 +90,9 @@ export function useToolsPanel(): UseToolsPanelReturn {
        function toggleGroupByKey(key: string): void {
                // Find current group by key to get up-to-date tool references
                const group = activeGroups.find((g) => g.key === key);
+
                if (!group) return;
+
                toolsStore.toggleGroup(group);
        }
 
@@ -95,29 +100,30 @@ export function useToolsPanel(): UseToolsPanelReturn {
                if (toolsStore.builtinTools.length === 0 && !toolsStore.loading) {
                        toolsStore.fetchBuiltinTools();
                }
+
                mcpStore.runHealthChecksForServers(mcpStore.getServers().filter((s) => s.enabled));
        }
 
        return {
-               expandedGroups,
-               get groups() {
-                       return groups;
-               },
                get activeGroups() {
                        return activeGroups;
                },
-               get totalToolCount() {
-                       return totalToolCount;
+               expandedGroups,
+               getEnabledToolCount,
+               getFavicon,
+               get groups() {
+                       return groups;
                },
+               handleOpen,
+               isGroupChecked,
+               isGroupDisabled,
                get noToolsInfoMessage() {
                        return noToolsInfoMessage;
                },
-               isGroupChecked,
-               getEnabledToolCount,
-               getFavicon,
-               isGroupDisabled,
-               toggleGroupExpanded,
                toggleGroupByKey,
-               handleOpen
+               toggleGroupExpanded,
+               get totalToolCount() {
+                       return totalToolCount;
+               }
        };
 }
index 4ce396533de8954f301805e012f4714107601cf4..775a5d6e060e38dcf9a2ba6c402910b10cb7a706 100644 (file)
@@ -1,23 +1,21 @@
-import { getAuthHeaders, getJsonHeaders } from '$lib/utils/api-headers';
-import { formatAttachmentText } from '$lib/utils/formatters';
-import { isAbortError } from '$lib/utils/abort';
-import { streamIdentity } from '$lib/utils/stream-identity';
+import { settingsStore } from '../stores/settings.svelte';
+import { capImageDataURLSize } from '../utils/cap-img-size';
 import {
-       ATTACHMENT_LABEL_PDF_FILE,
+       API_CHAT,
+       API_SLOTS,
+       API_STREAM,
        ATTACHMENT_LABEL_MCP_PROMPT,
        ATTACHMENT_LABEL_MCP_RESOURCE,
+       ATTACHMENT_LABEL_PDF_FILE,
+       CONTROL_ACTION,
        LEGACY_AGENTIC_REGEX,
        REASONING_EFFORT_TOKENS,
        SETTINGS_KEYS,
-       API_CHAT,
-       API_SLOTS,
-       CONTROL_ACTION,
-       SSE_LINE_SEPARATOR,
        SSE_DATA_PREFIX,
        SSE_DONE_MARKER,
-       STREAM_VISIBILITY_KICK_MS,
+       SSE_LINE_SEPARATOR,
        STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX,
-       API_STREAM
+       STREAM_VISIBILITY_KICK_MS
 } from '$lib/constants';
 import {
        AttachmentType,
@@ -28,20 +26,22 @@ import {
        ReasoningFormat,
        StreamConnectionState
 } from '$lib/enums';
-import type {
-       ApiChatMessageContentPart,
-       ApiChatMessageData,
-       ApiChatCompletionToolCall,
-       ApiStreamSession
-} from '$lib/types/api';
+import { modelsStore } from '$lib/stores/models.svelte';
 import type {
        AudioInputFormat,
        DatabaseMessageExtraMcpPrompt,
        DatabaseMessageExtraMcpResource
 } from '$lib/types';
-import { modelsStore } from '$lib/stores/models.svelte';
-import { settingsStore } from '../stores/settings.svelte';
-import { capImageDataURLSize } from '../utils/cap-img-size';
+import type {
+       ApiChatCompletionToolCall,
+       ApiChatMessageContentPart,
+       ApiChatMessageData,
+       ApiStreamSession
+} from '$lib/types/api';
+import { isAbortError } from '$lib/utils/abort';
+import { getAuthHeaders, getJsonHeaders } from '$lib/utils/api-headers';
+import { formatAttachmentText } from '$lib/utils/formatters';
+import { streamIdentity } from '$lib/utils/stream-identity';
 
 function getAudioInputFormat(mimeType: string): AudioInputFormat {
        const normalizedMimeType = mimeType.trim().toLowerCase();
@@ -98,16 +98,17 @@ export class ChatService {
                signal?: AbortSignal
        ): Promise<string> {
                let titleResponse = '';
+
                try {
                        await ChatService.sendMessage(
                                [message],
                                {
-                                       model: model || undefined,
-                                       stream: true,
                                        custom: { chat_template_kwargs: { enable_thinking: false } },
+                                       model: model || undefined,
                                        onChunk: (chunk: string) => {
                                                titleResponse += chunk;
-                                       }
+                                       },
+                                       stream: true
                                },
                                undefined,
                                signal
@@ -115,6 +116,7 @@ export class ChatService {
                } catch {
                        return '';
                }
+
                return titleResponse;
        }
 
@@ -143,52 +145,51 @@ export class ChatService {
                signal?: AbortSignal
        ): Promise<string | void> {
                const {
-                       stream,
+                       backend_sampling,
+                       continueFinalMessage,
+                       custom,
+                       // Config options
+                       disableReasoningParsing,
+                       dry_allowed_length,
+                       dry_base,
+                       dry_multiplier,
+                       dry_penalty_last_n,
+                       dynatemp_exponent,
+                       // Sampling parameters
+                       dynatemp_range,
+                       enableThinking,
+                       excludeReasoningFromContext,
+                       frequency_penalty,
+                       max_tokens,
+                       min_p,
                        onChunk,
                        onComplete,
-                       onError,
+                       onCompletionId,
                        onConnectionState,
-                       onReasoningChunk,
-                       onToolCallChunk,
+                       onError,
                        onModel,
-                       onCompletionId,
+                       onReasoningChunk,
                        onTimings,
-                       // Tools for function calling
-                       tools,
-                       // Generation parameters
-                       temperature,
-                       max_tokens,
-                       // Sampling parameters
-                       dynatemp_range,
-                       dynatemp_exponent,
-                       top_k,
-                       top_p,
-                       min_p,
-                       xtc_probability,
-                       xtc_threshold,
-                       typ_p,
+                       onToolCallChunk,
+                       presence_penalty,
+                       reasoningEffort,
                        // Penalty parameters
                        repeat_last_n,
                        repeat_penalty,
-                       presence_penalty,
-                       frequency_penalty,
-                       dry_multiplier,
-                       dry_base,
-                       dry_allowed_length,
-                       dry_penalty_last_n,
                        // Other parameters
                        samplers,
-                       backend_sampling,
-                       custom,
+                       stream,
+                       // Generation parameters
+                       temperature,
                        timings_per_token,
-                       // Config options
-                       disableReasoningParsing,
-                       excludeReasoningFromContext,
-                       enableThinking,
-                       reasoningEffort,
-                       continueFinalMessage
+                       // Tools for function calling
+                       tools,
+                       top_k,
+                       top_p,
+                       typ_p,
+                       xtc_probability,
+                       xtc_threshold
                } = options;
-
                const normalizedMessages: ApiChatMessageData[] = (
                        await Promise.all(
                                messages.map((msg) => {
@@ -227,6 +228,7 @@ export class ChatService {
 
                                                return true;
                                        });
+
                                        // If only text remains and it's a single part, simplify to string
                                        if (
                                                msg.content.length === 1 &&
@@ -242,20 +244,22 @@ export class ChatService {
                const requestBody: ApiChatCompletionRequest = {
                        messages: normalizedMessages.map((msg: ApiChatMessageData) => {
                                const mapped: ApiChatCompletionRequest['messages'][0] = {
-                                       role: msg.role,
                                        content: msg.content,
-                                       tool_calls: msg.tool_calls,
-                                       tool_call_id: msg.tool_call_id
+                                       role: msg.role,
+                                       tool_call_id: msg.tool_call_id,
+                                       tool_calls: msg.tool_calls
                                };
+
                                // Include reasoning_content from the dedicated field
                                if (!excludeReasoningFromContext && msg.reasoning_content) {
                                        mapped.reasoning_content = msg.reasoning_content;
                                }
+
                                return mapped;
                        }),
-                       stream,
                        return_progress: stream ? true : undefined,
                        sse_ping_interval: stream ? 1 : undefined,
+                       stream,
                        tools: tools && tools.length > 0 ? tools : undefined
                };
 
@@ -293,27 +297,42 @@ export class ChatService {
                }
 
                if (temperature !== undefined) requestBody.temperature = temperature;
+
                if (max_tokens !== undefined) {
                        // Set max_tokens to -1 (infinite) when explicitly configured as 0 or null
                        requestBody.max_tokens = max_tokens !== null && max_tokens !== 0 ? max_tokens : -1;
                }
 
                if (dynatemp_range !== undefined) requestBody.dynatemp_range = dynatemp_range;
+
                if (dynatemp_exponent !== undefined) requestBody.dynatemp_exponent = dynatemp_exponent;
+
                if (top_k !== undefined) requestBody.top_k = top_k;
+
                if (top_p !== undefined) requestBody.top_p = top_p;
+
                if (min_p !== undefined) requestBody.min_p = min_p;
+
                if (xtc_probability !== undefined) requestBody.xtc_probability = xtc_probability;
+
                if (xtc_threshold !== undefined) requestBody.xtc_threshold = xtc_threshold;
+
                if (typ_p !== undefined) requestBody.typ_p = typ_p;
 
                if (repeat_last_n !== undefined) requestBody.repeat_last_n = repeat_last_n;
+
                if (repeat_penalty !== undefined) requestBody.repeat_penalty = repeat_penalty;
+
                if (presence_penalty !== undefined) requestBody.presence_penalty = presence_penalty;
+
                if (frequency_penalty !== undefined) requestBody.frequency_penalty = frequency_penalty;
+
                if (dry_multiplier !== undefined) requestBody.dry_multiplier = dry_multiplier;
+
                if (dry_base !== undefined) requestBody.dry_base = dry_base;
+
                if (dry_allowed_length !== undefined) requestBody.dry_allowed_length = dry_allowed_length;
+
                if (dry_penalty_last_n !== undefined) requestBody.dry_penalty_last_n = dry_penalty_last_n;
 
                if (samplers !== undefined) {
@@ -330,6 +349,7 @@ export class ChatService {
                if (custom) {
                        try {
                                const customParams = typeof custom === 'string' ? JSON.parse(custom) : custom;
+
                                Object.assign(requestBody, customParams);
                        } catch (error) {
                                console.warn('Failed to parse custom parameters:', error);
@@ -338,6 +358,7 @@ export class ChatService {
 
                try {
                        const headers: Record<string, string> = { ...getJsonHeaders() };
+
                        // tag streaming requests with the conversation id, this single header is the opt in for the
                        // server side replay buffer and powers discoverActiveStream on tab reopen. with an explicit
                        // model the ::model suffix keeps the per model session distinct
@@ -349,9 +370,9 @@ export class ChatService {
                        }
 
                        const response = await fetch(API_CHAT.COMPLETIONS, {
-                               method: 'POST',
-                               headers,
                                body: JSON.stringify(requestBody),
+                               headers,
+                               method: 'POST',
                                signal
                        });
 
@@ -361,6 +382,7 @@ export class ChatService {
                                if (conversationId) {
                                        ChatService.clearStreamState(conversationId);
                                }
+
                                const error = await ChatService.parseErrorResponse(response);
 
                                if (onError) {
@@ -400,6 +422,7 @@ export class ChatService {
                } catch (error) {
                        if (isAbortError(error)) {
                                console.log('Chat completion request was aborted');
+
                                return;
                        }
 
@@ -448,9 +471,11 @@ export class ChatService {
                try {
                        const url = model ? `${API_SLOTS.LIST}?model=${encodeURIComponent(model)}` : API_SLOTS.LIST;
                        const res = await fetch(url, { signal });
+
                        if (!res.ok) return true;
 
                        const slots: { is_processing: boolean }[] = await res.json();
+
                        return slots.every((s) => !s.is_processing);
                } catch {
                        return true;
@@ -469,34 +494,39 @@ export class ChatService {
                        console.error(
                                'stopReasoning: no completion id for the active message, cannot target the running completion'
                        );
+
                        return false;
                }
 
                const body: Record<string, unknown> = {
-                       id: completionId,
-                       action: CONTROL_ACTION.END_REASONING
+                       action: CONTROL_ACTION.END_REASONING,
+                       id: completionId
                };
+
                if (model) body.model = model;
 
                try {
                        const res = await fetch(API_CHAT.CONTROL, {
-                               method: 'POST',
+                               body: JSON.stringify(body),
                                headers: getJsonHeaders(),
-                               body: JSON.stringify(body)
+                               method: 'POST'
                        });
-
                        const data = await res.json().catch(() => null);
+
                        if (!res.ok || data?.success !== true) {
                                console.error('stopReasoning: control request failed', {
-                                       status: res.status,
                                        completionId,
-                                       response: data
+                                       response: data,
+                                       status: res.status
                                });
+
                                return false;
                        }
+
                        return true;
                } catch (error) {
                        console.error('stopReasoning: control request threw', { completionId, error });
+
                        return false;
                }
        }
@@ -518,11 +548,13 @@ export class ChatService {
         */
        static async cancelServerStream(conversationId: string, model?: string | null): Promise<void> {
                if (!conversationId) return;
+
                try {
                        const id = streamIdentity(conversationId, model);
+
                        await fetch(`${API_STREAM.BASE}?conv_id=${encodeURIComponent(id)}`, {
-                               method: 'DELETE',
-                               headers: getAuthHeaders()
+                               headers: getAuthHeaders(),
+                               method: 'DELETE'
                        });
                } catch (e) {
                        console.warn('cancelServerStream failed:', e);
@@ -545,10 +577,13 @@ export class ChatService {
                if (!Array.isArray(sessions) || sessions.length === 0) {
                        return null;
                }
+
                const running = sessions.filter((s) => !s.is_done);
+
                if (running.length === 0) {
                        return null;
                }
+
                return running.reduce((best, cur) => (cur.started_at > best.started_at ? cur : best));
        }
 
@@ -560,12 +595,14 @@ export class ChatService {
                model?: string | null
        ): void {
                if (!conversationId) return;
+
                try {
                        const state: ResumableStreamState = {
                                bytesReceived,
-                               updatedAt: Date.now(),
-                               model: model ?? null
+                               model: model ?? null,
+                               updatedAt: Date.now()
                        };
+
                        localStorage.setItem(streamStorageKey(conversationId), JSON.stringify(state));
                } catch {
                        // localStorage may be full or disabled, silently ignore
@@ -574,11 +611,16 @@ export class ChatService {
 
        static getStreamState(conversationId: string): ResumableStreamState | null {
                if (!conversationId) return null;
+
                try {
                        const raw = localStorage.getItem(streamStorageKey(conversationId));
+
                        if (!raw) return null;
+
                        const parsed = JSON.parse(raw) as ResumableStreamState;
+
                        if (!parsed || typeof parsed.bytesReceived !== 'number') return null;
+
                        return parsed;
                } catch {
                        return null;
@@ -587,6 +629,7 @@ export class ChatService {
 
        static clearStreamState(conversationId: string): void {
                if (!conversationId) return;
+
                try {
                        localStorage.removeItem(streamStorageKey(conversationId));
                } catch {
@@ -605,6 +648,7 @@ export class ChatService {
                fallbackModel: string | null
        ): string {
                const model = state && state.model !== undefined ? state.model : fallbackModel;
+
                return streamIdentity(conversationId, model);
        }
 
@@ -617,7 +661,9 @@ export class ChatService {
        // so issue the GET and abort it right after the status line. 0 on network error
        static async probeResumeStatus(streamId: string): Promise<number> {
                if (!streamId) return 0;
+
                const ac = new AbortController();
+
                try {
                        const resp = await fetch(
                                `${API_STREAM.BASE}?conv_id=${encodeURIComponent(streamId)}&from=0`,
@@ -626,7 +672,9 @@ export class ChatService {
                                        signal: ac.signal
                                }
                        );
+
                        ac.abort();
+
                        return resp.status;
                } catch {
                        return 0;
@@ -639,11 +687,13 @@ export class ChatService {
                model?: string | null
        ): Promise<Response | null> {
                if (!conversationId) return null;
+
                const state = ChatService.getStreamState(conversationId);
                const from = state?.bytesReceived ?? 0;
                const id = streamIdentity(conversationId, model);
                const url = `${API_STREAM.BASE}?conv_id=${encodeURIComponent(id)}&from=${from}`;
-               return await fetch(url, { method: 'GET', signal, headers: getAuthHeaders() });
+
+               return await fetch(url, { headers: getAuthHeaders(), method: 'GET', signal });
        }
 
        static async preEncode(
@@ -673,14 +723,13 @@ export class ChatService {
 
                        return true;
                });
-
                const requestBody: Record<string, unknown> = {
                        messages: normalizedMessages.map((msg: ApiChatMessageData) => {
                                const mapped: Record<string, unknown> = {
-                                       role: msg.role,
                                        content: excludeReasoning ? ChatService.stripReasoningContent(msg.content) : msg.content,
-                                       tool_calls: msg.tool_calls,
-                                       tool_call_id: msg.tool_call_id
+                                       role: msg.role,
+                                       tool_call_id: msg.tool_call_id,
+                                       tool_calls: msg.tool_calls
                                };
 
                                if (!excludeReasoning && msg.reasoning_content) {
@@ -689,8 +738,8 @@ export class ChatService {
 
                                return mapped;
                        }),
-                       stream: false,
-                       n_predict: 0
+                       n_predict: 0,
+                       stream: false
                };
 
                if (model) {
@@ -699,9 +748,9 @@ export class ChatService {
 
                try {
                        await fetch(API_CHAT.COMPLETIONS, {
-                               method: 'POST',
-                               headers: getJsonHeaders(),
                                body: JSON.stringify(requestBody),
+                               headers: getJsonHeaders(),
+                               method: 'POST',
                                signal
                        });
                } catch (error) {
@@ -767,10 +816,13 @@ export class ChatService {
                // if a resume returns 200 but yields nothing, we abandon
                // since the session has a bounded size, the total number of retries is bounded by construction
                let madeProgress = true;
+
                const encoder = new TextEncoder();
+
                if (conversationId) {
                        ChatService.saveStreamState(conversationId, 0, streamModel);
                }
+
                onConnectionState?.(StreamConnectionState.STREAMING);
 
                let decoder = new TextDecoder();
@@ -792,7 +844,6 @@ export class ChatService {
                        toolCallIndexOffset = aggregatedToolCalls.length;
                        hasOpenToolCallBatch = false;
                };
-
                const processToolCallDelta = (toolCalls?: ApiChatCompletionToolCallDelta[]) => {
                        if (!toolCalls || toolCalls.length === 0) {
                                return;
@@ -824,24 +875,29 @@ export class ChatService {
                                onToolCallChunk?.(serializedToolCalls);
                        }
                };
-
                const onVisibilityChange = () => {
                        if (typeof document === 'undefined') return;
+
                        if (document.visibilityState !== 'visible') return;
+
                        if (streamFinished) return;
+
                        if (!conversationId) return;
+
                        // the bytes have been quiet for too long, the OS likely killed the socket
                        // kicking the reader unblocks reader.read with done=true so the outer loop can resume
                        if (Date.now() - lastByteAt > STREAM_VISIBILITY_KICK_MS) {
                                reader!.cancel().catch(() => {});
                        }
                };
+
                if (typeof document !== 'undefined') {
                        document.addEventListener('visibilitychange', onVisibilityChange);
                }
 
                try {
                        let chunk = '';
+
                        // outer loop drives the resume cycle, swaps reader on premature end of stream
                        while (true) {
                                while (true) {
@@ -849,8 +905,10 @@ export class ChatService {
 
                                        let done: boolean;
                                        let value: Uint8Array | undefined;
+
                                        try {
                                                const r = await reader.read();
+
                                                done = r.done;
                                                value = r.value;
                                        } catch (readErr) {
@@ -860,10 +918,12 @@ export class ChatService {
                                                if (isAbortError(readErr)) {
                                                        throw readErr;
                                                }
+
                                                console.warn('reader.read() rejected, treating as premature end:', readErr);
                                                done = true;
                                                value = undefined;
                                        }
+
                                        if (done) break;
 
                                        if (abortSignal?.aborted) break;
@@ -871,6 +931,7 @@ export class ChatService {
                                        if (value && value.byteLength > 0) {
                                                segmentBytesRead += value.byteLength;
                                                lastByteAt = Date.now();
+
                                                if (!madeProgress) {
                                                        madeProgress = true;
                                                        onConnectionState?.(StreamConnectionState.STREAMING);
@@ -879,12 +940,14 @@ export class ChatService {
 
                                        chunk += decoder.decode(value, { stream: true });
                                        const lines = chunk.split(SSE_LINE_SEPARATOR);
+
                                        chunk = lines.pop() || '';
 
                                        // the persisted offset must point right after the last fully parsed line,
                                        // the trailing `chunk` is partial bytes still waiting for a newline
                                        if (conversationId) {
                                                const tailBytes = encoder.encode(chunk).byteLength;
+
                                                bytesParsed = segmentStartOffset + segmentBytesRead - tailBytes;
                                                ChatService.saveStreamState(conversationId, bytesParsed, streamModel);
                                        }
@@ -894,6 +957,7 @@ export class ChatService {
 
                                                if (line.startsWith(SSE_DATA_PREFIX)) {
                                                        const data = line.slice(SSE_DATA_PREFIX.length).trim();
+
                                                        if (data === SSE_DONE_MARKER) {
                                                                streamFinished = true;
 
@@ -908,8 +972,8 @@ export class ChatService {
                                                                const toolCalls = choice?.delta?.tool_calls;
                                                                const timings = parsed.timings;
                                                                const promptProgress = parsed.prompt_progress;
-
                                                                const chunkModel = ChatService.extractModelName(parsed);
+
                                                                if (chunkModel && !modelEmitted) {
                                                                        modelEmitted = true;
                                                                        onModel?.(chunkModel);
@@ -932,6 +996,7 @@ export class ChatService {
                                                                if (content) {
                                                                        finalizeOpenToolCallBatch();
                                                                        aggregatedContent += content;
+
                                                                        if (!abortSignal?.aborted) {
                                                                                onChunk?.(content);
                                                                        }
@@ -940,6 +1005,7 @@ export class ChatService {
                                                                if (reasoningContent) {
                                                                        finalizeOpenToolCallBatch();
                                                                        fullReasoningContent += reasoningContent;
+
                                                                        if (!abortSignal?.aborted) {
                                                                                onReasoningChunk?.(reasoningContent);
                                                                        }
@@ -953,17 +1019,21 @@ export class ChatService {
                                        }
 
                                        if (abortSignal?.aborted) break;
+
                                        if (streamFinished) break;
                                }
 
                                // inner reader done, decide whether to try a resume
                                if (abortSignal?.aborted) break;
+
                                if (streamFinished) break;
+
                                if (!conversationId) break;
 
                                if (!madeProgress) {
                                        onConnectionState?.(StreamConnectionState.LOST);
                                        onError?.(new Error('Stream resume produced no new bytes, giving up'));
+
                                        break;
                                }
 
@@ -978,14 +1048,19 @@ export class ChatService {
                                        abortSignal,
                                        streamModel
                                ).catch(() => null);
+
                                // an abort landing during the resume request is intentional, not a lost connection
                                if (abortSignal?.aborted) break;
+
                                if (!resumeResp || resumeResp.status !== 200) {
                                        onConnectionState?.(StreamConnectionState.LOST);
                                        onError?.(new Error('Stream connection lost and could not be resumed'));
+
                                        break;
                                }
+
                                const newReader = resumeResp.body?.getReader();
+
                                if (!newReader) break;
 
                                try {
@@ -1030,6 +1105,7 @@ export class ChatService {
                        if (typeof document !== 'undefined') {
                                document.removeEventListener('visibilitychange', onVisibilityChange);
                        }
+
                        try {
                                reader.releaseLock();
                        } catch {
@@ -1070,8 +1146,8 @@ export class ChatService {
                        }
 
                        const data: ApiChatCompletionResponse = JSON.parse(responseText);
-
                        const responseModel = ChatService.extractModelName(data);
+
                        if (responseModel) {
                                onModel?.(responseModel);
                        }
@@ -1087,6 +1163,7 @@ export class ChatService {
 
                                if (mergedToolCalls.length > 0) {
                                        serializedToolCalls = JSON.stringify(mergedToolCalls);
+
                                        if (serializedToolCalls) {
                                                onToolCallChunk?.(serializedToolCalls);
                                        }
@@ -1194,14 +1271,15 @@ export class ChatService {
                // Handle tool result messages (role: 'tool')
                if (message.role === MessageRole.TOOL && message.toolCallId) {
                        return {
-                               role: MessageRole.TOOL,
                                content: message.content,
+                               role: MessageRole.TOOL,
                                tool_call_id: message.toolCallId
                        };
                }
 
                // Parse tool calls for assistant messages
                let toolCalls: ApiChatCompletionToolCall[] | undefined;
+
                if (message.toolCalls) {
                        try {
                                toolCalls = JSON.parse(message.toolCalls);
@@ -1212,8 +1290,8 @@ export class ChatService {
 
                if (!message.extra || message.extra.length === 0) {
                        const result: ApiChatMessageData = {
-                               role: message.role as MessageRole,
-                               content: message.content
+                               content: message.content,
+                               role: message.role as MessageRole
                        };
 
                        if (message.reasoningContent) {
@@ -1228,7 +1306,6 @@ export class ChatService {
                }
 
                const contentParts: ApiChatMessageContentPart[] = [];
-
                const textFiles = message.extra.filter(
                        (extra: DatabaseMessageExtra): extra is DatabaseMessageExtraTextFile =>
                                extra.type === AttachmentType.TEXT
@@ -1236,8 +1313,8 @@ export class ChatService {
 
                for (const textFile of textFiles) {
                        contentParts.push({
-                               type: ContentPartType.TEXT,
-                               text: formatAttachmentText('File', textFile.name, textFile.content)
+                               text: formatAttachmentText('File', textFile.name, textFile.content),
+                               type: ContentPartType.TEXT
                        });
                }
 
@@ -1249,8 +1326,8 @@ export class ChatService {
 
                for (const legacyContextFile of legacyContextFiles) {
                        contentParts.push({
-                               type: ContentPartType.TEXT,
-                               text: formatAttachmentText('File', legacyContextFile.name, legacyContextFile.content)
+                               text: formatAttachmentText('File', legacyContextFile.name, legacyContextFile.content),
+                               type: ContentPartType.TEXT
                        });
                }
 
@@ -1261,14 +1338,13 @@ export class ChatService {
 
                for (const image of imageFiles) {
                        const maxImageResolution = settingsStore.getConfig(SETTINGS_KEYS.MAX_IMAGE_RESOLUTION);
-
                        // Caps the resolution and bakes the jpeg exif orientation in one pass,
                        // untouched images pass through as is
                        const base64Url = await capImageDataURLSize(image.base64Url, maxImageResolution);
 
                        contentParts.push({
-                               type: ContentPartType.IMAGE_URL,
-                               image_url: { url: base64Url }
+                               image_url: { url: base64Url },
+                               type: ContentPartType.IMAGE_URL
                        });
                }
 
@@ -1279,18 +1355,18 @@ export class ChatService {
 
                for (const audio of audioFiles) {
                        contentParts.push({
-                               type: ContentPartType.INPUT_AUDIO,
                                input_audio: {
                                        data: audio.base64Data,
                                        format: getAudioInputFormat(audio.mimeType)
-                               }
+                               },
+                               type: ContentPartType.INPUT_AUDIO
                        });
                }
 
                if (message.content) {
                        contentParts.push({
-                               type: ContentPartType.TEXT,
-                               text: message.content
+                               text: message.content,
+                               type: ContentPartType.TEXT
                        });
                }
 
@@ -1301,7 +1377,6 @@ export class ChatService {
 
                for (const video of videoFiles) {
                        contentParts.push({
-                               type: ContentPartType.INPUT_VIDEO,
                                input_video: {
                                        data: video.base64Data,
                                        format: video.mimeType.includes('mp4')
@@ -1309,7 +1384,8 @@ export class ChatService {
                                                : video.mimeType.includes('ogg')
                                                        ? 'ogg'
                                                        : 'auto'
-                               }
+                               },
+                               type: ContentPartType.INPUT_VIDEO
                        });
                }
 
@@ -1322,14 +1398,14 @@ export class ChatService {
                        if (pdfFile.processedAsImages && pdfFile.images) {
                                for (let i = 0; i < pdfFile.images.length; i++) {
                                        contentParts.push({
-                                               type: ContentPartType.IMAGE_URL,
-                                               image_url: { url: pdfFile.images[i] }
+                                               image_url: { url: pdfFile.images[i] },
+                                               type: ContentPartType.IMAGE_URL
                                        });
                                }
                        } else {
                                contentParts.push({
-                                       type: ContentPartType.TEXT,
-                                       text: formatAttachmentText(ATTACHMENT_LABEL_PDF_FILE, pdfFile.name, pdfFile.content)
+                                       text: formatAttachmentText(ATTACHMENT_LABEL_PDF_FILE, pdfFile.name, pdfFile.content),
+                                       type: ContentPartType.TEXT
                                });
                        }
                }
@@ -1341,13 +1417,13 @@ export class ChatService {
 
                for (const mcpPrompt of mcpPrompts) {
                        contentParts.push({
-                               type: ContentPartType.TEXT,
                                text: formatAttachmentText(
                                        ATTACHMENT_LABEL_MCP_PROMPT,
                                        mcpPrompt.name,
                                        mcpPrompt.content,
                                        mcpPrompt.serverName
-                               )
+                               ),
+                               type: ContentPartType.TEXT
                        });
                }
 
@@ -1358,26 +1434,29 @@ export class ChatService {
 
                for (const mcpResource of mcpResources) {
                        contentParts.push({
-                               type: ContentPartType.TEXT,
                                text: formatAttachmentText(
                                        ATTACHMENT_LABEL_MCP_RESOURCE,
                                        mcpResource.name,
                                        mcpResource.content,
                                        mcpResource.serverName
-                               )
+                               ),
+                               type: ContentPartType.TEXT
                        });
                }
 
                const result: ApiChatMessageData = {
-                       role: message.role as MessageRole,
-                       content: contentParts
+                       content: contentParts,
+                       role: message.role as MessageRole
                };
+
                if (message.reasoningContent) {
                        result.reasoning_content = message.reasoningContent;
                }
+
                if (toolCalls && toolCalls.length > 0) {
                        result.tool_calls = toolCalls;
                }
+
                return result;
        }
 
@@ -1407,6 +1486,7 @@ export class ChatService {
                        if (part.type === ContentPartType.TEXT && part.text) {
                                return { ...part, text: stripFromString(part.text) };
                        }
+
                        return part;
                });
        }
@@ -1422,17 +1502,17 @@ export class ChatService {
                try {
                        const errorText = await response.text();
                        const errorData: ApiErrorResponse = JSON.parse(errorText);
-
                        const message = errorData.error?.message || 'Unknown server error';
                        const error = new Error(message) as Error & {
                                contextInfo?: { n_prompt_tokens: number; n_ctx: number };
                        };
+
                        error.name = response.status === 400 ? 'ServerError' : 'HttpError';
 
                        if (errorData.error && 'n_prompt_tokens' in errorData.error && 'n_ctx' in errorData.error) {
                                error.contextInfo = {
-                                       n_prompt_tokens: errorData.error.n_prompt_tokens,
-                                       n_ctx: errorData.error.n_ctx
+                                       n_ctx: errorData.error.n_ctx,
+                                       n_prompt_tokens: errorData.error.n_prompt_tokens
                                };
                        }
 
@@ -1443,6 +1523,7 @@ export class ChatService {
                        ) as Error & {
                                contextInfo?: { n_prompt_tokens: number; n_ctx: number };
                        };
+
                        fallback.name = 'HttpError';
 
                        return fallback;
@@ -1466,33 +1547,36 @@ export class ChatService {
                                ? (value as Record<string, unknown>)
                                : undefined;
                };
-
                const getTrimmedString = (value: unknown): string | undefined => {
                        return typeof value === 'string' && value.trim() ? value.trim() : undefined;
                };
-
                const root = asRecord(data);
+
                if (!root) return undefined;
 
                // 1) root (some implementations provide `model` at the top level)
                const rootModel = getTrimmedString(root.model);
+
                if (rootModel) {
                        return rootModel;
                }
 
                // 2) streaming choice (delta) or final response (message)
                const firstChoice = Array.isArray(root.choices) ? asRecord(root.choices[0]) : undefined;
+
                if (!firstChoice) {
                        return undefined;
                }
 
                // priority: delta.model (first chunk) else message.model (final response)
                const deltaModel = getTrimmedString(asRecord(firstChoice.delta)?.model);
+
                if (deltaModel) {
                        return deltaModel;
                }
 
                const messageModel = getTrimmedString(asRecord(firstChoice.message)?.model);
+
                if (messageModel) {
                        return messageModel;
                }
index 43259b73bec7b34f0d3eb04b61e07f08dc533204..89dc58b005dd56a4bd8ca5651ba184d7f296be22 100644 (file)
@@ -1,9 +1,9 @@
-import Dexie, { type EntityTable } from 'dexie';
-import { findDescendantMessages, uuid, filterByLeafNodeId } from '$lib/utils';
-import { IDXDB_TABLES, IDXDB_STORES, STORAGE_APP_NAME } from '$lib/constants';
+import { IDXDB_STORES, IDXDB_TABLES, STORAGE_APP_NAME } from '$lib/constants';
 import { MessageRole } from '$lib/enums';
 import type { McpServerOverride } from '$lib/types/database';
 import type { ExportedConversation } from '$lib/types/database';
+import { filterByLeafNodeId, findDescendantMessages, uuid } from '$lib/utils';
+import Dexie, { type EntityTable } from 'dexie';
 
 class LlamaUiDatabase extends Dexie {
        [IDXDB_TABLES.conversations]!: EntityTable<DatabaseConversation, string>;
@@ -39,14 +39,15 @@ export class DatabaseService {
                fields?: Partial<Omit<DatabaseConversation, 'id' | 'name' | 'lastModified'>>
        ): Promise<DatabaseConversation> {
                const conversation: DatabaseConversation = {
+                       currNode: '',
                        id: uuid(),
-                       name,
                        lastModified: Date.now(),
-                       currNode: '',
+                       name,
                        ...fields
                };
 
                await db[IDXDB_TABLES.conversations].add(conversation);
+
                return conversation;
        }
 
@@ -77,6 +78,7 @@ export class DatabaseService {
                                // Handle null parent (root message case)
                                if (parentId !== null) {
                                        const parentMessage = await db[IDXDB_TABLES.messages].get(parentId);
+
                                        if (!parentMessage) {
                                                throw new Error(`Parent message ${parentId} not found`);
                                        }
@@ -84,10 +86,10 @@ export class DatabaseService {
 
                                const newMessage: DatabaseMessage = {
                                        ...message,
+                                       children: [],
                                        id: uuid(),
                                        parent: parentId,
-                                       toolCalls: message.toolCalls ?? '',
-                                       children: []
+                                       toolCalls: message.toolCalls ?? ''
                                };
 
                                await db[IDXDB_TABLES.messages].add(newMessage);
@@ -95,6 +97,7 @@ export class DatabaseService {
                                // Update parent's children array if parent exists
                                if (parentId !== null) {
                                        const parentMessage = await db[IDXDB_TABLES.messages].get(parentId);
+
                                        if (parentMessage) {
                                                await db[IDXDB_TABLES.messages].update(parentId, {
                                                        children: [...parentMessage.children, newMessage.id]
@@ -120,18 +123,19 @@ export class DatabaseService {
         */
        static async createRootMessage(convId: string): Promise<string> {
                const rootMessage: DatabaseMessage = {
-                       id: uuid(),
-                       convId,
-                       type: 'root',
-                       timestamp: Date.now(),
-                       role: MessageRole.SYSTEM,
+                       children: [],
                        content: '',
+                       convId,
+                       id: uuid(),
                        parent: null,
+                       role: MessageRole.SYSTEM,
+                       timestamp: Date.now(),
                        toolCalls: '',
-                       children: []
+                       type: 'root'
                };
 
                await db[IDXDB_TABLES.messages].add(rootMessage);
+
                return rootMessage.id;
        }
 
@@ -150,25 +154,27 @@ export class DatabaseService {
                parentId: string
        ): Promise<DatabaseMessage> {
                const trimmedPrompt = systemPrompt.trim();
+
                if (!trimmedPrompt) {
                        throw new Error('Cannot create system message with empty content');
                }
 
                return await db.transaction('rw', db[IDXDB_TABLES.messages], async () => {
                        const parentMessage = await db[IDXDB_TABLES.messages].get(parentId);
+
                        if (!parentMessage) {
                                throw new Error(`Parent message ${parentId} not found`);
                        }
 
                        const systemMessage: DatabaseMessage = {
-                               id: uuid(),
-                               convId,
-                               type: MessageRole.SYSTEM,
-                               timestamp: Date.now(),
-                               role: MessageRole.SYSTEM,
+                               children: [],
                                content: trimmedPrompt,
+                               convId,
+                               id: uuid(),
                                parent: parentId,
-                               children: []
+                               role: MessageRole.SYSTEM,
+                               timestamp: Date.now(),
+                               type: MessageRole.SYSTEM
                        };
 
                        await db[IDXDB_TABLES.messages].add(systemMessage);
@@ -240,35 +246,46 @@ export class DatabaseService {
                prefetched?: ReadonlyMap<string, DatabaseConversation>
        ): Promise<void> {
                const conv = prefetched?.get(parentId) ?? (await db[IDXDB_TABLES.conversations].get(parentId));
+
                if (!conv) return;
 
                let newParent = conv.forkedFromConversationId;
+
                const visited = new Set<string>([parentId]);
+
                while (newParent && excludeIds.has(newParent)) {
                        if (visited.has(newParent)) {
                                newParent = undefined;
+
                                break;
                        }
+
                        visited.add(newParent);
                        const next =
                                prefetched?.get(newParent) ?? (await db[IDXDB_TABLES.conversations].get(newParent));
+
                        if (!next) {
                                newParent = undefined;
+
                                break;
                        }
+
                        newParent = next.forkedFromConversationId;
                }
 
                const directChildren = await db[IDXDB_TABLES.conversations]
                        .filter((c) => c.forkedFromConversationId === parentId)
                        .toArray();
-
                const updates: DatabaseConversation[] = [];
+
                for (const child of directChildren) {
                        if (excludeIds.has(child.id)) continue;
+
                        updates.push({ ...child, forkedFromConversationId: newParent });
                }
+
                if (updates.length === 0) return;
+
                await db[IDXDB_TABLES.conversations].bulkPut(updates);
        }
 
@@ -282,7 +299,9 @@ export class DatabaseService {
         */
        static async bulkDeleteConversations(ids: string[]): Promise<void> {
                const cleanIds = ids.filter((id): id is string => typeof id === 'string' && id.length > 0);
+
                if (cleanIds.length === 0) return;
+
                const idSet = new Set(cleanIds);
 
                await db.transaction(
@@ -292,16 +311,23 @@ export class DatabaseService {
                                // Pre-load each to-delete conversation so the per-id reparent
                                // walk-up doesn't ping-pong the same ancestry chain.
                                const prefetched = new Map<string, DatabaseConversation>();
+
                                let frontier = [...cleanIds];
+
                                const requested = new Set<string>(frontier);
+
                                while (frontier.length > 0) {
                                        const fetched = await db[IDXDB_TABLES.conversations].bulkGet(frontier);
+
                                        frontier = [];
                                        for (let i = 0; i < fetched.length; i++) {
                                                const conv = fetched[i];
+
                                                if (!conv || !conv.id) continue;
+
                                                prefetched.set(conv.id, conv);
                                                const ancestor = conv.forkedFromConversationId;
+
                                                if (ancestor && !prefetched.has(ancestor) && !requested.has(ancestor)) {
                                                        frontier.push(ancestor);
                                                        requested.add(ancestor);
@@ -327,11 +353,13 @@ export class DatabaseService {
        static async deleteMessage(messageId: string): Promise<void> {
                await db.transaction('rw', db[IDXDB_TABLES.messages], async () => {
                        const message = await db[IDXDB_TABLES.messages].get(messageId);
+
                        if (!message) return;
 
                        // Remove this message from its parent's children array
                        if (message.parent) {
                                const parent = await db[IDXDB_TABLES.messages].get(message.parent);
+
                                if (parent) {
                                        parent.children = parent.children.filter((childId: string) => childId !== messageId);
                                        await db[IDXDB_TABLES.messages].put(parent);
@@ -361,15 +389,15 @@ export class DatabaseService {
                                .where('convId')
                                .equals(conversationId)
                                .toArray();
-
                        // Find all descendant messages
                        const descendants = findDescendantMessages(allMessages, messageId);
                        const allToDelete = [messageId, ...descendants];
-
                        // Get the message to delete for parent cleanup
                        const message = await db[IDXDB_TABLES.messages].get(messageId);
+
                        if (message && message.parent) {
                                const parent = await db[IDXDB_TABLES.messages].get(message.parent);
+
                                if (parent) {
                                        parent.children = parent.children.filter((childId: string) => childId !== messageId);
                                        await db[IDXDB_TABLES.messages].put(parent);
@@ -424,28 +452,34 @@ export class DatabaseService {
        ): Promise<Map<string, ExportedConversation>> {
                const result = new Map<string, ExportedConversation>();
                const cleanIds = convIds.filter((id): id is string => typeof id === 'string' && id.length > 0);
+
                if (cleanIds.length === 0) return result;
 
                const [convs, allMessages] = await Promise.all([
                        db[IDXDB_TABLES.conversations].bulkGet(cleanIds),
                        db[IDXDB_TABLES.messages].where('convId').anyOf(cleanIds).toArray()
                ]);
-
                const messagesByConv = new Map<string, DatabaseMessage[]>();
+
                for (const msg of allMessages) {
                        const bucket = messagesByConv.get(msg.convId);
+
                        if (bucket) bucket.push(msg);
                        else messagesByConv.set(msg.convId, [msg]);
                }
 
                for (let i = 0; i < cleanIds.length; i++) {
                        const conv = convs[i];
+
                        if (!conv) continue;
+
                        const messages = (messagesByConv.get(conv.id) ?? []).sort(
                                (a, b) => a.timestamp - b.timestamp
                        );
+
                        result.set(conv.id, { conv, messages });
                }
+
                return result;
        }
 
@@ -480,11 +514,15 @@ export class DatabaseService {
         */
        static async toggleConversationPin(id: string): Promise<boolean> {
                const conversation = await db[IDXDB_TABLES.conversations].get(id);
+
                if (!conversation) {
                        throw new Error(`Conversation ${id} not found`);
                }
+
                const newPinnedState = !conversation.pinned;
+
                await this.updateConversation(id, { pinned: newPinnedState });
+
                return newPinnedState;
        }
 
@@ -501,21 +539,29 @@ export class DatabaseService {
        static async bulkToggleConversationPins(ids: string[]): Promise<Map<string, boolean>> {
                const cleanIds = ids.filter((id): id is string => typeof id === 'string' && id.length > 0);
                const result = new Map<string, boolean>();
+
                if (cleanIds.length === 0) return result;
 
                await db.transaction('rw', db[IDXDB_TABLES.conversations], async () => {
                        const convs = await db[IDXDB_TABLES.conversations].bulkGet(cleanIds);
                        const updates: DatabaseConversation[] = [];
+
                        for (let i = 0; i < cleanIds.length; i++) {
                                const conv = convs[i];
+
                                if (!conv) continue;
+
                                const newPinned = !conv.pinned;
+
                                updates.push({ ...conv, pinned: newPinned });
                                result.set(cleanIds[i], newPinned);
                        }
+
                        if (updates.length === 0) return;
+
                        await db[IDXDB_TABLES.conversations].bulkPut(updates);
                });
+
                return result;
        }
 
@@ -573,10 +619,11 @@ export class DatabaseService {
                        async () => {
                                for (const item of data) {
                                        const { conv, messages } = item;
-
                                        const existing = await db[IDXDB_TABLES.conversations].get(conv.id);
+
                                        if (existing) {
                                                skipped.push(conv);
+
                                                continue;
                                        }
 
@@ -620,6 +667,7 @@ export class DatabaseService {
                        [db[IDXDB_TABLES.conversations], db[IDXDB_TABLES.messages]],
                        async () => {
                                const sourceConv = await db[IDXDB_TABLES.conversations].get(sourceConvId);
+
                                if (!sourceConv) {
                                        throw new Error(`Source conversation ${sourceConvId} not found`);
                                }
@@ -628,12 +676,12 @@ export class DatabaseService {
                                        .where('convId')
                                        .equals(sourceConvId)
                                        .toArray();
-
                                const pathMessages = filterByLeafNodeId(
                                        allMessages,
                                        atMessageId,
                                        true
                                ) as DatabaseMessage[];
+
                                if (pathMessages.length === 0) {
                                        throw new Error(`Could not resolve message path to ${atMessageId}`);
                                }
@@ -654,28 +702,27 @@ export class DatabaseService {
 
                                        return {
                                                ...msg,
-                                               id: newId,
-                                               convId: newConvId,
-                                               parent: newParent,
                                                children: newChildren,
-                                               extra: options.includeAttachments ? msg.extra : undefined
+                                               convId: newConvId,
+                                               extra: options.includeAttachments ? msg.extra : undefined,
+                                               id: newId,
+                                               parent: newParent
                                        };
                                });
-
                                const lastClonedMessage = clonedMessages[clonedMessages.length - 1];
                                const newConv: DatabaseConversation = {
-                                       id: newConvId,
-                                       name: options.name,
-                                       lastModified: Date.now(),
                                        currNode: lastClonedMessage.id,
+                                       cwd: sourceConv.cwd,
                                        forkedFromConversationId: sourceConvId,
+                                       id: newConvId,
+                                       lastModified: Date.now(),
                                        mcpServerOverrides: sourceConv.mcpServerOverrides
                                                ? sourceConv.mcpServerOverrides.map((o: McpServerOverride) => ({
-                                                               serverId: o.serverId,
-                                                               enabled: o.enabled
+                                                               enabled: o.enabled,
+                                                               serverId: o.serverId
                                                        }))
                                                : undefined,
-                                       cwd: sourceConv.cwd
+                                       name: options.name
                                };
 
                                await db[IDXDB_TABLES.conversations].add(newConv);
index e36faaac247152ec44034b059756ab08902f10cf..fbac5b7b58e67b982f9f619998d9ebc2e497ae06 100644 (file)
@@ -1,63 +1,63 @@
 import { Client } from '@modelcontextprotocol/sdk/client';
+import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
 import {
        StreamableHTTPClientTransport,
        StreamableHTTPError
 } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
-import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
 import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
+import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
 import type {
-       Tool,
-       Prompt,
        GetPromptResult,
-       ListChangedHandlers
+       ListChangedHandlers,
+       Prompt,
+       Tool
 } from '@modelcontextprotocol/sdk/types.js';
-import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
 import {
-       DEFAULT_MCP_CONFIG,
+       CORS_PROXY_ENDPOINT,
+       CORS_PROXY_HEADER_PREFIX,
        DEFAULT_CLIENT_VERSION,
        DEFAULT_IMAGE_MIME_TYPE,
-       CORS_PROXY_HEADER_PREFIX,
-       MCP_PARTIAL_REDACT_HEADERS,
-       CORS_PROXY_ENDPOINT
+       DEFAULT_MCP_CONFIG,
+       MCP_PARTIAL_REDACT_HEADERS
 } from '$lib/constants';
 import {
        MCPConnectionPhase,
-       MCPLogLevel,
-       MCPTransportType,
        MCPContentType,
-       MCPRefType
+       MCPLogLevel,
+       MCPRefType,
+       MCPTransportType
 } from '$lib/enums';
 import type {
-       MCPServerConfig,
-       MCPResourceIcon,
-       ToolCallParams,
-       ToolExecutionResult,
-       Implementation,
        ClientCapabilities,
+       Implementation,
        MCPConnection,
-       MCPPhaseCallback,
        MCPConnectionLog,
-       MCPServerInfo,
+       MCPPhaseCallback,
+       MCPReadResourceResult,
        MCPResource,
-       MCPResourceTemplate,
        MCPResourceContent,
-       MCPReadResourceResult
+       MCPResourceIcon,
+       MCPResourceTemplate,
+       MCPServerConfig,
+       MCPServerInfo,
+       ToolCallParams,
+       ToolExecutionResult
 } from '$lib/types';
 import {
-       buildProxiedUrl,
        buildProxiedHeaders,
-       getAuthHeaders,
-       sanitizeHeaders,
-       throwIfAborted,
-       isAbortError,
+       buildProxiedUrl,
        createBase64DataUrl,
-       getRequestUrl,
-       getRequestMethod,
+       extractJsonRpcMethods,
+       formatDiagnosticErrorMessage,
+       getAuthHeaders,
        getRequestBody,
+       getRequestMethod,
+       getRequestUrl,
+       isAbortError,
+       type RequestBodySummary,
+       sanitizeHeaders,
        summarizeRequestBody,
-       formatDiagnosticErrorMessage,
-       extractJsonRpcMethods,
-       type RequestBodySummary
+       throwIfAborted
 } from '$lib/utils';
 
 interface ToolResultContentItem {
@@ -101,11 +101,11 @@ export class MCPService {
                details?: unknown
        ): MCPConnectionLog {
                return {
-                       timestamp: new Date(),
-                       phase,
-                       message,
+                       details,
                        level,
-                       details
+                       message,
+                       phase,
+                       timestamp: new Date()
                };
        }
 
@@ -118,12 +118,12 @@ export class MCPService {
        ): DiagnosticRequestDetails {
                const body = getRequestBody(input, init);
                const details: DiagnosticRequestDetails = {
-                       url: getRequestUrl(input),
-                       method: getRequestMethod(input, init, baseInit).toUpperCase(),
+                       body: summarizeRequestBody(body),
                        credentials: init?.credentials ?? baseInit.credentials,
-                       mode: init?.mode ?? baseInit.mode,
                        headers: sanitizeHeaders(requestHeaders, extraRedactedHeaders, MCP_PARTIAL_REDACT_HEADERS),
-                       body: summarizeRequestBody(body)
+                       method: getRequestMethod(input, init, baseInit).toUpperCase(),
+                       mode: init?.mode ?? baseInit.mode,
+                       url: getRequestUrl(input)
                };
                const jsonRpcMethods = extractJsonRpcMethods(body);
 
@@ -144,6 +144,7 @@ export class MCPService {
                                useProxy && !key.toLowerCase().startsWith(CORS_PROXY_HEADER_PREFIX)
                                        ? `${CORS_PROXY_HEADER_PREFIX}${key}`
                                        : key;
+
                        requestHeaders.set(proxiedKey, value);
                }
        }
@@ -151,12 +152,12 @@ export class MCPService {
        private static summarizeError(error: unknown): Record<string, unknown> {
                if (error instanceof Error) {
                        return {
-                               name: error.name,
-                               message: error.message,
                                cause:
                                        error.cause instanceof Error
-                                               ? { name: error.cause.name, message: error.cause.message }
+                                               ? { message: error.cause.message, name: error.cause.name }
                                                : error.cause,
+                               message: error.message,
+                               name: error.name,
                                stack: error.stack?.split('\n').slice(0, 6).join('\n')
                        };
                }
@@ -173,13 +174,13 @@ export class MCPService {
                }
 
                return {
+                       isSecureContext: window.isSecureContext,
                        location: window.location.href,
                        origin: window.location.origin,
                        protocol: window.location.protocol,
-                       isSecureContext: window.isSecureContext,
+                       sameOrigin: window.location.origin === targetUrl.origin,
                        targetOrigin: targetUrl.origin,
                        targetProtocol: targetUrl.protocol,
-                       sameOrigin: window.location.origin === targetUrl.origin,
                        useProxy
                };
        }
@@ -244,6 +245,7 @@ export class MCPService {
                disable: () => void;
        } {
                let enabled = true;
+
                const logIfEnabled = (log: MCPConnectionLog) => {
                        if (enabled) {
                                onLog?.(log);
@@ -251,9 +253,13 @@ export class MCPService {
                };
 
                return {
+                       disable: () => {
+                               enabled = false;
+                       },
                        fetch: async (input, init) => {
                                if (useProxy && typeof window !== 'undefined') {
                                        let requestUrlStr = '';
+
                                        if (typeof input === 'string') {
                                                requestUrlStr = input;
                                        } else if (input instanceof URL) {
@@ -262,6 +268,7 @@ export class MCPService {
 
                                        if (requestUrlStr) {
                                                const parsedRequestUrl = new URL(requestUrlStr, window.location.origin);
+
                                                if (
                                                        parsedRequestUrl.origin === window.location.origin &&
                                                        !parsedRequestUrl.pathname.includes(CORS_PROXY_ENDPOINT)
@@ -308,8 +315,8 @@ export class MCPService {
                                                `HTTP ${method} ${url}`,
                                                MCPLogLevel.INFO,
                                                {
-                                                       serverName,
-                                                       request
+                                                       request,
+                                                       serverName
                                                }
                                        )
                                );
@@ -324,11 +331,11 @@ export class MCPService {
                                                        MCPLogLevel.INFO,
                                                        {
                                                                response: {
-                                                                       url,
+                                                                       durationMs: 0,
+                                                                       isFake: true,
                                                                        status: response.status,
                                                                        statusText: response.statusText,
-                                                                       durationMs: 0,
-                                                                       isFake: true
+                                                                       url
                                                                }
                                                        }
                                                )
@@ -353,11 +360,11 @@ export class MCPService {
                                                        response.ok ? MCPLogLevel.INFO : MCPLogLevel.WARN,
                                                        {
                                                                response: {
-                                                                       url,
+                                                                       durationMs,
+                                                                       headers: sanitizeHeaders(response.headers, undefined, MCP_PARTIAL_REDACT_HEADERS),
                                                                        status: response.status,
                                                                        statusText: response.statusText,
-                                                                       headers: sanitizeHeaders(response.headers, undefined, MCP_PARTIAL_REDACT_HEADERS),
-                                                                       durationMs
+                                                                       url
                                                                }
                                                        }
                                                )
@@ -373,21 +380,18 @@ export class MCPService {
                                                        `HTTP ${method} ${url} failed: ${formatDiagnosticErrorMessage(error)}`,
                                                        MCPLogLevel.ERROR,
                                                        {
-                                                               serverName,
-                                                               request,
-                                                               error: this.summarizeError(error),
                                                                browser: this.getBrowserContext(targetUrl, useProxy),
+                                                               durationMs,
+                                                               error: this.summarizeError(error),
                                                                hints: this.getConnectionHints(targetUrl, config, error),
-                                                               durationMs
+                                                               request,
+                                                               serverName
                                                        }
                                                )
                                        );
 
                                        throw error;
                                }
-                       },
-                       disable: () => {
-                               enabled = false;
                        }
                };
        }
@@ -463,15 +467,15 @@ export class MCPService {
                        }
 
                        return {
+                               stopPhaseLogging: () => {},
                                transport: new WebSocketClientTransport(url),
-                               type: MCPTransportType.WEBSOCKET,
-                               stopPhaseLogging: () => {}
+                               type: MCPTransportType.WEBSOCKET
                        };
                }
 
                if (config.transport === MCPTransportType.SSE) {
                        const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url);
-                       const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch(
+                       const { disable: stopPhaseLogging, fetch: diagnosticFetch } = this.createDiagnosticFetch(
                                serverName,
                                config,
                                requestInit,
@@ -485,18 +489,18 @@ export class MCPService {
                        }
 
                        return {
+                               stopPhaseLogging,
                                transport: new SSEClientTransport(url, {
-                                       requestInit,
+                                       eventSourceInit: { fetch: diagnosticFetch },
                                        fetch: diagnosticFetch,
-                                       eventSourceInit: { fetch: diagnosticFetch }
+                                       requestInit
                                }),
-                               type: MCPTransportType.SSE,
-                               stopPhaseLogging
+                               type: MCPTransportType.SSE
                        };
                }
 
                const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url);
-               const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch(
+               const { disable: stopPhaseLogging, fetch: diagnosticFetch } = this.createDiagnosticFetch(
                        serverName,
                        config,
                        requestInit,
@@ -515,25 +519,25 @@ export class MCPService {
                        }
 
                        return {
+                               stopPhaseLogging,
                                transport: new StreamableHTTPClientTransport(url, {
-                                       requestInit,
-                                       fetch: diagnosticFetch
+                                       fetch: diagnosticFetch,
+                                       requestInit
                                }),
-                               type: MCPTransportType.STREAMABLE_HTTP,
-                               stopPhaseLogging
+                               type: MCPTransportType.STREAMABLE_HTTP
                        };
                } catch (httpError) {
                        console.warn(`[MCPService] StreamableHTTP failed, trying SSE transport...`, httpError);
 
                        try {
                                return {
+                                       stopPhaseLogging,
                                        transport: new SSEClientTransport(url, {
-                                               requestInit,
+                                               eventSourceInit: { fetch: diagnosticFetch },
                                                fetch: diagnosticFetch,
-                                               eventSourceInit: { fetch: diagnosticFetch }
+                                               requestInit
                                        }),
-                                       type: MCPTransportType.SSE,
-                                       stopPhaseLogging
+                                       type: MCPTransportType.SSE
                                };
                        } catch (sseError) {
                                const httpMsg = httpError instanceof Error ? httpError.message : String(httpError);
@@ -557,17 +561,17 @@ export class MCPService {
                }
 
                return {
-                       name: impl.name,
-                       version: impl.version,
-                       title: impl.title,
                        description: impl.description,
-                       websiteUrl: impl.websiteUrl,
                        icons: impl.icons?.map((icon: MCPResourceIcon) => ({
-                               src: icon.src,
                                mimeType: icon.mimeType,
                                sizes: icon.sizes,
+                               src: icon.src,
                                theme: icon.theme
-                       }))
+                       })),
+                       name: impl.name,
+                       title: impl.title,
+                       version: impl.version,
+                       websiteUrl: impl.websiteUrl
                };
        }
 
@@ -617,9 +621,9 @@ export class MCPService {
                }
 
                const {
+                       stopPhaseLogging,
                        transport,
-                       type: transportType,
-                       stopPhaseLogging
+                       type: transportType
                } = this.createTransport(serverName, serverConfig, (log) => onPhase?.(log.phase, log));
 
                // Setup WebSocket reconnection handler
@@ -650,7 +654,6 @@ export class MCPService {
                                listChanged: listChangedHandlers
                        }
                );
-
                const runtimeErrorHandler = (error: Error) => {
                        // the SDK reports any post initialize error here, including the abort we trigger
                        // ourselves on the next health check cycle, on tab unload, or on server teardown.
@@ -661,7 +664,9 @@ export class MCPService {
                        if (isAbortError(error)) {
                                return;
                        }
+
                        const msg = error?.message ?? '';
+
                        if (
                                /SSE stream disconnected:.*AbortError/i.test(msg) ||
                                /AbortError: .*aborted/i.test(msg) ||
@@ -669,6 +674,7 @@ export class MCPService {
                        ) {
                                return;
                        }
+
                        console.error(`[MCPService][${serverName}] Protocol error after initialize:`, error);
                };
 
@@ -701,6 +707,7 @@ export class MCPService {
 
                try {
                        let handshakeTimer: ReturnType<typeof setTimeout> | undefined;
+
                        const handshakeDeadline = new Promise<never>((_, reject) => {
                                handshakeTimer = setTimeout(() => {
                                        void transport.close().catch(() => {});
@@ -736,21 +743,21 @@ export class MCPService {
                                        }`,
                                        MCPLogLevel.ERROR,
                                        {
-                                               error: this.summarizeError(error),
+                                               browser: this.getBrowserContext(url, serverConfig.useProxy ?? false),
                                                config: {
-                                                       serverName,
                                                        configuredUrl: serverConfig.url,
+                                                       credentials: serverConfig.credentials,
                                                        effectiveUrl: url.href,
-                                                       transportType,
-                                                       useProxy: serverConfig.useProxy ?? false,
                                                        headers: sanitizeHeaders(
                                                                serverConfig.headers,
                                                                Object.keys(serverConfig.headers ?? {}),
                                                                MCP_PARTIAL_REDACT_HEADERS
                                                        ),
-                                                       credentials: serverConfig.credentials
+                                                       serverName,
+                                                       transportType,
+                                                       useProxy: serverConfig.useProxy ?? false
                                                },
-                                               browser: this.getBrowserContext(url, serverConfig.useProxy ?? false),
+                                               error: this.summarizeError(error),
                                                hints: this.getConnectionHints(url, serverConfig, error)
                                        }
                                )
@@ -777,10 +784,10 @@ export class MCPService {
                                }
                        ),
                        {
-                               serverInfo,
-                               serverCapabilities,
                                clientCapabilities: effectiveCapabilities,
-                               instructions
+                               instructions,
+                               serverCapabilities,
+                               serverInfo
                        }
                );
 
@@ -796,15 +803,14 @@ export class MCPService {
 
                const tools = await this.listTools({
                        client,
-                       transport,
-                       tools: [],
-                       serverName,
-                       transportType,
                        connectionTimeMs: 0,
                        requestTimeoutMs:
-                               serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000
+                               serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000,
+                       serverName,
+                       tools: [],
+                       transport,
+                       transportType
                });
-
                const connectionTimeMs = Math.round(performance.now() - startTime);
 
                // Phase: Connected
@@ -815,6 +821,7 @@ export class MCPService {
                                `Connection established with ${tools.length} tools (${connectionTimeMs}ms)`
                        )
                );
+
                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
                        console.log(
                                `[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms`
@@ -823,18 +830,18 @@ export class MCPService {
 
                return {
                        client,
-                       transport,
-                       tools,
-                       serverName,
-                       transportType,
-                       serverInfo,
-                       serverCapabilities,
                        clientCapabilities: effectiveCapabilities,
-                       protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
-                       instructions,
                        connectionTimeMs,
+                       instructions,
+                       protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
                        requestTimeoutMs:
-                               serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000
+                               serverConfig.requestTimeoutMs ?? DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000,
+                       serverCapabilities,
+                       serverInfo,
+                       serverName,
+                       tools,
+                       transport,
+                       transportType
                };
        }
 
@@ -861,6 +868,7 @@ export class MCPService {
                        // by not setting onerror, but since we use it for protocol logging,
                        // we must clear it before disconnect.
                        connection.client.onerror = undefined;
+
                        if (connection.transport.onclose) {
                                connection.transport.onclose = undefined;
                        }
@@ -936,7 +944,7 @@ export class MCPService {
                args?: Record<string, string>
        ): Promise<GetPromptResult> {
                try {
-                       return await connection.client.getPrompt({ name, arguments: args });
+                       return await connection.client.getPrompt({ arguments: args, name });
                } catch (error) {
                        console.error(`[MCPService][${connection.serverName}] Failed to get prompt:`, error);
 
@@ -964,7 +972,7 @@ export class MCPService {
 
                try {
                        const result = await connection.client.callTool(
-                               { name: params.name, arguments: params.arguments },
+                               { arguments: params.arguments, name: params.name },
                                undefined,
                                { signal, timeout: connection.requestTimeoutMs }
                        );
@@ -1001,6 +1009,7 @@ export class MCPService {
         */
        private static formatToolResult(result: ToolCallResult): string {
                const content = result.content;
+
                if (!Array.isArray(content)) return '';
 
                return content
@@ -1022,6 +1031,7 @@ export class MCPService {
                        const resource = content.resource;
 
                        if (resource.text) return resource.text;
+
                        if (resource.blob) return resource.blob;
 
                        return JSON.stringify(resource);
@@ -1058,8 +1068,8 @@ export class MCPService {
        ): Promise<{ values: string[]; total?: number; hasMore?: boolean } | null> {
                try {
                        const result = await connection.client.complete({
-                               ref,
-                               argument
+                               argument,
+                               ref
                        });
 
                        return result.completion;
@@ -1092,8 +1102,8 @@ export class MCPService {
                        const result = await connection.client.listResources(cursor ? { cursor } : undefined);
 
                        return {
-                               resources: (result.resources ?? []) as MCPResource[],
-                               nextCursor: result.nextCursor
+                               nextCursor: result.nextCursor,
+                               resources: (result.resources ?? []) as MCPResource[]
                        };
                } catch (error) {
                        if (this.isSessionExpiredError(error)) {
@@ -1113,10 +1123,12 @@ export class MCPService {
         */
        static async listAllResources(connection: MCPConnection): Promise<MCPResource[]> {
                const allResources: MCPResource[] = [];
+
                let cursor: string | undefined;
 
                do {
                        const result = await this.listResources(connection, cursor);
+
                        allResources.push(...result.resources);
                        cursor = result.nextCursor;
                } while (cursor);
@@ -1138,8 +1150,8 @@ export class MCPService {
                        const result = await connection.client.listResourceTemplates(cursor ? { cursor } : undefined);
 
                        return {
-                               resourceTemplates: (result.resourceTemplates ?? []) as MCPResourceTemplate[],
-                               nextCursor: result.nextCursor
+                               nextCursor: result.nextCursor,
+                               resourceTemplates: (result.resourceTemplates ?? []) as MCPResourceTemplate[]
                        };
                } catch (error) {
                        if (this.isSessionExpiredError(error)) {
@@ -1162,10 +1174,12 @@ export class MCPService {
         */
        static async listAllResourceTemplates(connection: MCPConnection): Promise<MCPResourceTemplate[]> {
                const allTemplates: MCPResourceTemplate[] = [];
+
                let cursor: string | undefined;
 
                do {
                        const result = await this.listResourceTemplates(connection, cursor);
+
                        allTemplates.push(...result.resourceTemplates);
                        cursor = result.nextCursor;
                } while (cursor);
@@ -1187,8 +1201,8 @@ export class MCPService {
                        const result = await connection.client.readResource({ uri });
 
                        return {
-                               contents: (result.contents ?? []) as MCPResourceContent[],
-                               _meta: result._meta
+                               _meta: result._meta,
+                               contents: (result.contents ?? []) as MCPResourceContent[]
                        };
                } catch (error) {
                        console.error(`[MCPService][${connection.serverName}] Failed to read resource:`, error);
index 53004729bfd8f09aadb76dabdcf840d9d785a941..b2626c794aa8edb119956b0902322abc083f61fd 100644 (file)
  * 4. Theme key: Copy standalone `theme` → config object (both preserved)
  */
 
-import Dexie from 'dexie';
 import {
-       STORAGE_APP_NAME,
-       STORAGE_APP_NAME_DEPRECATED,
-       DB_APP_NAME_DEPRECATED,
        CONFIG_LOCALSTORAGE_KEY,
-       IDXDB_TABLES,
+       DB_APP_NAME_DEPRECATED,
        IDXDB_STORES,
-       NEW_TO_DEPRECATED_MAP
+       IDXDB_TABLES,
+       NEW_TO_DEPRECATED_MAP,
+       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';
 
 // Types
 
@@ -58,11 +58,15 @@ const MIGRATION_STATE_VERSION = 1;
 function getMigrationState(): MigrationState {
        try {
                const raw = localStorage.getItem(MIGRATION_STATE_KEY);
+
                if (!raw) return { completed: [], failed: [], lastRun: '' };
+
                const parsed = JSON.parse(raw);
+
                if (parsed.version !== MIGRATION_STATE_VERSION) {
                        return { completed: [], failed: [], lastRun: '' };
                }
+
                return {
                        completed: parsed.completed ?? [],
                        failed: parsed.failed ?? [],
@@ -86,48 +90,56 @@ function saveMigrationState(state: MigrationState): void {
 
 function isMigrationCompleted(id: string): boolean {
        const state = getMigrationState();
+
        return state.completed.includes(id);
 }
 
 function markMigrationCompleted(id: string): void {
        const state = getMigrationState();
+
        if (!state.completed.includes(id)) {
                state.completed.push(id);
        }
+
        state.failed = state.failed.filter((f) => f !== id);
        saveMigrationState(state);
 }
 
 function markMigrationFailed(id: string): void {
        const state = getMigrationState();
+
        if (!state.failed.includes(id)) {
                state.failed.push(id);
        }
+
        saveMigrationState(state);
 }
 
 // Migration 1: LocalStorage Key Prefix (Non-Destructive)
 
 const LOCALSTORAGE_MIGRATION_ID = 'localstorage-prefix-v1';
-
 const localStorageMigration: Migration = {
-       id: LOCALSTORAGE_MIGRATION_ID,
        description: 'Copy localStorage keys from LlamaCppWebui to LlamaUi prefix (non-destructive)',
+       id: LOCALSTORAGE_MIGRATION_ID,
 
        async run(): Promise<void> {
                // Non-destructive: copy to new key, but KEEP the old key
                for (const [newKey, deprecatedKey] of Object.entries(NEW_TO_DEPRECATED_MAP)) {
                        // Only migrate if new key doesn't already exist
                        const newValue = localStorage.getItem(newKey);
+
                        if (newValue !== null) {
                                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                        console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
+
                                continue;
                        }
 
                        const oldValue = localStorage.getItem(deprecatedKey);
+
                        if (oldValue !== null) {
                                localStorage.setItem(newKey, oldValue);
+
                                // Keep old key for downgrade compatibility - DO NOT DELETE
                                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
                                        console.log(
@@ -141,27 +153,32 @@ const localStorageMigration: Migration = {
 
 // Migration 2: IndexedDB Database Name (Non-Destructive)
 
+// eslint-disable-next-line padding-line-between-statements -- comment header separates this const group
 const IDXDB_MIGRATION_ID = 'idxdb-database-v1';
-
 const idxdbMigration: Migration = {
-       id: IDXDB_MIGRATION_ID,
        description: 'Copy IndexedDB from LlamacppWebui to LlamaUi database (non-destructive)',
+       id: IDXDB_MIGRATION_ID,
 
        async run(): Promise<void> {
                const oldDbNames = await Dexie.getDatabaseNames();
+
                if (!oldDbNames.includes(DB_APP_NAME_DEPRECATED)) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] IndexedDB: no old database found, skipping');
+
                        return;
                }
 
                // Check if new database already has data
                const newDb = new Dexie(STORAGE_APP_NAME);
+
                newDb.version(1).stores(IDXDB_STORES);
                const existingConvs = await newDb.table(IDXDB_TABLES.conversations).count();
+
                if (existingConvs > 0) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] IndexedDB: new database already has data, skipping');
+
                        return;
                }
 
@@ -169,6 +186,7 @@ const idxdbMigration: Migration = {
                        console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
 
                const oldDb = new Dexie(DB_APP_NAME_DEPRECATED);
+
                oldDb.version(1).stores(IDXDB_STORES);
 
                const conversations = await oldDb.table(IDXDB_TABLES.conversations).toArray();
@@ -176,11 +194,14 @@ const idxdbMigration: Migration = {
 
                if (conversations.length > 0) {
                        await newDb.table(IDXDB_TABLES.conversations).bulkAdd(conversations);
+
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
                }
+
                if (messages.length > 0) {
                        await newDb.table(IDXDB_TABLES.messages).bulkAdd(messages);
+
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
                }
@@ -193,6 +214,7 @@ const idxdbMigration: Migration = {
 
 // Migration 3: Legacy Message Format
 
+// eslint-disable-next-line padding-line-between-statements -- comment header separates this const group
 const LEGACY_MESSAGE_MIGRATION_ID = 'legacy-message-format-v2';
 
 interface ParsedTurn {
@@ -219,8 +241,8 @@ function parseLegacyToolCalls(content: string): ParsedTurn[] {
                }
 
                currentTurn.toolCalls.push({
-                       name: match[1],
                        args: match[2],
+                       name: match[1],
                        result: match[3].replace(/^\n+|\n+$/g, '')
                });
 
@@ -237,6 +259,7 @@ function parseLegacyToolCalls(content: string): ParsedTurn[] {
                const cleanRemaining = remainingText
                        .replace(LEGACY_AGENTIC_REGEX.AGENTIC_TOOL_CALL_OPEN, '')
                        .trim();
+
                if (cleanRemaining) {
                        turns.push({ textBefore: cleanRemaining, toolCalls: [] });
                }
@@ -254,7 +277,9 @@ function extractLegacyReasoning(content: string): { reasoning: string; cleanCont
        let cleanContent = content;
 
        const re = new RegExp(LEGACY_AGENTIC_REGEX.REASONING_EXTRACT.source, 'g');
+
        let match;
+
        while ((match = re.exec(content)) !== null) {
                reasoning += match[1];
        }
@@ -263,7 +288,7 @@ function extractLegacyReasoning(content: string): { reasoning: string; cleanCont
                .replace(new RegExp(LEGACY_AGENTIC_REGEX.REASONING_BLOCK.source, 'g'), '')
                .replace(LEGACY_AGENTIC_REGEX.REASONING_OPEN, '');
 
-       return { reasoning, cleanContent };
+       return { cleanContent, reasoning };
 }
 
 function hasLegacyMarkers(content: string): boolean {
@@ -275,18 +300,21 @@ let DatabaseService: typeof import('./database.service').DatabaseService | null
 async function getDatabaseService() {
        if (!DatabaseService) {
                const module = await import('./database.service');
+
                DatabaseService = module.DatabaseService;
        }
+
        return DatabaseService;
 }
 
 const legacyMessageMigration: Migration = {
-       id: LEGACY_MESSAGE_MIGRATION_ID,
        description: 'Migrate legacy marker-based messages to structured format',
+       id: LEGACY_MESSAGE_MIGRATION_ID,
 
        async run(): Promise<void> {
                const db = await getDatabaseService();
                const conversations = await db.getAllConversations();
+
                let migratedCount = 0;
 
                for (const conv of conversations) {
@@ -295,25 +323,28 @@ const legacyMessageMigration: Migration = {
                        for (const message of allMessages) {
                                if (message.role !== MessageRole.ASSISTANT) {
                                        if (message.content?.includes(LEGACY_REASONING_TAGS.START)) {
-                                               const { reasoning, cleanContent } = extractLegacyReasoning(message.content);
+                                               const { cleanContent, reasoning } = extractLegacyReasoning(message.content);
+
                                                await db.updateMessage(message.id, {
                                                        content: cleanContent.trim(),
                                                        reasoningContent: reasoning || undefined
                                                });
                                                migratedCount++;
                                        }
+
                                        continue;
                                }
 
                                if (!hasLegacyMarkers(message.content ?? '')) continue;
 
-                               const { reasoning, cleanContent } = extractLegacyReasoning(message.content);
+                               const { cleanContent, reasoning } = extractLegacyReasoning(message.content);
                                const turns = parseLegacyToolCalls(cleanContent);
 
                                let existingToolCalls: Array<{
                                        id: string;
                                        function?: { name: string; arguments: string };
                                }> = [];
+
                                if (message.toolCalls) {
                                        try {
                                                existingToolCalls = JSON.parse(message.toolCalls);
@@ -323,15 +354,17 @@ const legacyMessageMigration: Migration = {
                                }
 
                                const firstTurn = turns[0];
+
                                if (!firstTurn) continue;
 
                                const firstTurnToolCalls = firstTurn.toolCalls.map((tc, i) => {
                                        const existing =
                                                existingToolCalls.find((e) => e.function?.name === tc.name) || existingToolCalls[i];
+
                                        return {
+                                               function: { arguments: tc.args, name: tc.name },
                                                id: existing?.id || `legacy_tool_${i}`,
-                                               type: 'function' as const,
-                                               function: { name: tc.name, arguments: tc.args }
+                                               type: 'function' as const
                                        };
                                });
 
@@ -347,69 +380,71 @@ const legacyMessageMigration: Migration = {
                                for (let i = 0; i < firstTurn.toolCalls.length; i++) {
                                        const tc = firstTurn.toolCalls[i];
                                        const toolCallId = firstTurnToolCalls[i]?.id || `legacy_tool_${i}`;
-
                                        const toolMsg = await db.createMessageBranch(
                                                {
+                                                       children: [],
+                                                       content: tc.result,
                                                        convId: conv.id,
-                                                       type: 'text',
                                                        role: MessageRole.TOOL,
-                                                       content: tc.result,
-                                                       toolCallId,
                                                        timestamp: message.timestamp + i + 1,
+                                                       toolCallId,
                                                        toolCalls: '',
-                                                       children: []
+                                                       type: 'text'
                                                },
                                                currentParentId
                                        );
+
                                        currentParentId = toolMsg.id;
                                }
 
                                for (let turnIdx = 1; turnIdx < turns.length; turnIdx++) {
                                        const turn = turns[turnIdx];
-
                                        const turnToolCalls = turn.toolCalls.map((tc, i) => {
                                                const idx = toolCallIdCounter + i;
                                                const existing = existingToolCalls[idx];
+
                                                return {
+                                                       function: { arguments: tc.args, name: tc.name },
                                                        id: existing?.id || `legacy_tool_${idx}`,
-                                                       type: 'function' as const,
-                                                       function: { name: tc.name, arguments: tc.args }
+                                                       type: 'function' as const
                                                };
                                        });
+
                                        toolCallIdCounter += turn.toolCalls.length;
 
                                        const assistantMsg = await db.createMessageBranch(
                                                {
+                                                       children: [],
+                                                       content: turn.textBefore,
                                                        convId: conv.id,
-                                                       type: 'text',
+                                                       model: message.model,
                                                        role: MessageRole.ASSISTANT,
-                                                       content: turn.textBefore,
                                                        timestamp: message.timestamp + turnIdx * 100,
                                                        toolCalls: turnToolCalls.length > 0 ? JSON.stringify(turnToolCalls) : '',
-                                                       children: [],
-                                                       model: message.model
+                                                       type: 'text'
                                                },
                                                currentParentId
                                        );
+
                                        currentParentId = assistantMsg.id;
 
                                        for (let i = 0; i < turn.toolCalls.length; i++) {
                                                const tc = turn.toolCalls[i];
                                                const toolCallId = turnToolCalls[i]?.id || `legacy_tool_${toolCallIdCounter + i}`;
-
                                                const toolMsg = await db.createMessageBranch(
                                                        {
+                                                               children: [],
+                                                               content: tc.result,
                                                                convId: conv.id,
-                                                               type: 'text',
                                                                role: MessageRole.TOOL,
-                                                               content: tc.result,
-                                                               toolCallId,
                                                                timestamp: message.timestamp + turnIdx * 100 + i + 1,
+                                                               toolCallId,
                                                                toolCalls: '',
-                                                               children: []
+                                                               type: 'text'
                                                        },
                                                        currentParentId
                                                );
+
                                                currentParentId = toolMsg.id;
                                        }
                                }
@@ -417,7 +452,9 @@ const legacyMessageMigration: Migration = {
                                if (message.children.length > 0 && currentParentId !== message.id) {
                                        for (const childId of message.children) {
                                                const child = allMessages.find((m) => m.id === childId);
+
                                                if (!child) continue;
+
                                                if (child.role !== MessageRole.TOOL) {
                                                        await db.updateMessage(childId, { parent: currentParentId });
                                                }
@@ -436,17 +473,19 @@ const legacyMessageMigration: Migration = {
 
 // Migration 4: Theme Key (Non-Destructive)
 
+// eslint-disable-next-line padding-line-between-statements -- comment header separates this const group
 const THEME_MIGRATION_ID = 'theme-key-v1';
-
 const themeMigration: Migration = {
-       id: THEME_MIGRATION_ID,
        description: 'Copy standalone theme key to config object (non-destructive)',
+       id: THEME_MIGRATION_ID,
 
        async run(): Promise<void> {
                const legacyTheme = localStorage.getItem('theme');
+
                if (legacyTheme === null) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] Theme: no legacy theme key found, skipping');
+
                        return;
                }
 
@@ -457,6 +496,7 @@ const themeMigration: Migration = {
                if (SETTINGS_KEYS.THEME in config) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] Theme: config already has theme, skipping');
+
                        return;
                }
 
@@ -471,19 +511,21 @@ const themeMigration: Migration = {
 
 // Migration Registry & Runner
 
+// eslint-disable-next-line padding-line-between-statements -- comment header separates this const group
 const CUSTOM_JSON_MIGRATION_ID = 'custom-json-key-v1';
-
 const customJsonKeyMigration: Migration = {
-       id: CUSTOM_JSON_MIGRATION_ID,
        description: 'Copy legacy custom config key to customJson (non-destructive)',
+       id: CUSTOM_JSON_MIGRATION_ID,
 
        async run(): Promise<void> {
                const configRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY);
+
                if (configRaw === null) return;
 
                const config = JSON.parse(configRaw);
 
                if (!('custom' in config)) return;
+
                if (SETTINGS_KEYS.CUSTOM_JSON in config) return;
 
                config[SETTINGS_KEYS.CUSTOM_JSON] = config.custom;
@@ -494,16 +536,13 @@ const customJsonKeyMigration: Migration = {
                        console.log(`[Migration] Custom JSON: copied custom to customJson (preserved old key)`);
        }
 };
-
 const MCP_DEFAULT_ENABLED_MIGRATION_ID = 'mcp-default-enabled-to-config-v1';
-
 const LEGACY_MCP_DEFAULT_ENABLED_KEY = `${STORAGE_APP_NAME}.mcpDefaultEnabled`;
 const DEPRECATED_LEGACY_MCP_DEFAULT_ENABLED_KEY = `${STORAGE_APP_NAME_DEPRECATED}.mcpDefaultEnabled`;
-
 const mcpDefaultEnabledMigration: Migration = {
-       id: MCP_DEFAULT_ENABLED_MIGRATION_ID,
        description:
                'Copy mcpDefaultEnabled localStorage key into settings config (preserves legacy keys)',
+       id: MCP_DEFAULT_ENABLED_MIGRATION_ID,
 
        async run(): Promise<void> {
                const raw =
@@ -515,6 +554,7 @@ const mcpDefaultEnabledMigration: Migration = {
                if (raw === null) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] MCP default enabled: no legacy key found, skipping');
+
                        return;
                }
 
@@ -525,12 +565,15 @@ const mcpDefaultEnabledMigration: Migration = {
                if (MCP_DEFAULT_OVERRIDES_LEGACY_KEY in config) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] MCP default enabled: config already has overrides, skipping');
+
                        return;
                }
 
                try {
                        const parsed = JSON.parse(raw);
+
                        if (!Array.isArray(parsed)) return;
+
                        const valid = parsed.every(
                                (o) =>
                                        typeof o === 'object' &&
@@ -538,6 +581,7 @@ const mcpDefaultEnabledMigration: Migration = {
                                        typeof (o as Record<string, unknown>).serverId === 'string' &&
                                        typeof (o as Record<string, unknown>).enabled === 'boolean'
                        );
+
                        if (!valid) return;
                } catch {
                        return;
@@ -550,18 +594,18 @@ const mcpDefaultEnabledMigration: Migration = {
                        console.log('[Migration] MCP default enabled: moved legacy key into config');
        }
 };
-
 const CONFIG_TYPES_MIGRATION_ID = 'config-type-normalization-v1';
-
 const configTypesMigration: Migration = {
-       id: CONFIG_TYPES_MIGRATION_ID,
        description: 'Coerce legacy string-encoded booleans in persisted config to real booleans',
+       id: CONFIG_TYPES_MIGRATION_ID,
 
        async run(): Promise<void> {
                const configRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY);
+
                if (configRaw === null) return;
 
                const config = JSON.parse(configRaw);
+
                let changed = false;
 
                // Pre-schema configs persisted booleans as "true"/"false" strings; the strict server
@@ -585,10 +629,8 @@ const configTypesMigration: Migration = {
                        console.log(`[Migration] Config types: coerced string booleans (changed=${changed})`);
        }
 };
-
 const MCP_DEFAULT_OVERRIDES_LEGACY_KEY = `${STORAGE_APP_NAME}.mcpDefaultServerOverrides`;
 const MCP_DEFAULT_OVERRIDES_MERGE_MIGRATION_ID = 'mcp-default-overrides-merge-v1';
-
 /**
  * Folds `mcpDefaultServerOverrides` (the legacy "default for new chats" list,
  * JSON-encoded as `[{ serverId, enabled }, ...]`) into `mcpServers[i].enabled`.
@@ -597,12 +639,13 @@ const MCP_DEFAULT_OVERRIDES_MERGE_MIGRATION_ID = 'mcp-default-overrides-merge-v1
  * standalone overrides are already inside the config.
  */
 const mcpDefaultOverridesMergeMigration: Migration = {
-       id: MCP_DEFAULT_OVERRIDES_MERGE_MIGRATION_ID,
        description:
                'Merge mcpDefaultServerOverrides entries onto mcpServers[i].enabled (preserves legacy key)',
+       id: MCP_DEFAULT_OVERRIDES_MERGE_MIGRATION_ID,
 
        async run(): Promise<void> {
                const configRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY);
+
                if (configRaw === null) return;
 
                const config = JSON.parse(configRaw);
@@ -611,13 +654,17 @@ const mcpDefaultOverridesMergeMigration: Migration = {
                if (typeof raw !== 'string' || raw.length === 0) {
                        if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                console.log('[Migration] MCP default overrides merge: nothing to merge');
+
                        return;
                }
 
                let overrides: { serverId: string; enabled: boolean }[];
+
                try {
                        const parsed = JSON.parse(raw);
+
                        if (!Array.isArray(parsed)) return;
+
                        overrides = parsed.filter(
                                (o) =>
                                        typeof o === 'object' &&
@@ -630,7 +677,9 @@ const mcpDefaultOverridesMergeMigration: Migration = {
                }
 
                const serversRaw = config[SETTINGS_KEYS.MCP_SERVERS];
+
                let servers: { id: string; enabled?: boolean }[];
+
                try {
                        servers = typeof serversRaw === 'string' ? JSON.parse(serversRaw) : [];
                } catch {
@@ -640,9 +689,12 @@ const mcpDefaultOverridesMergeMigration: Migration = {
                if (!Array.isArray(servers)) servers = [];
 
                let serversChanged = false;
+
                const knownIds = new Set(servers.map((s) => s.id));
+
                for (const override of overrides) {
                        if (!knownIds.has(override.serverId)) continue;
+
                        const index = servers.findIndex((s) => s.id === override.serverId);
 
                        if (index >= 0 && servers[index].enabled !== override.enabled) {
@@ -662,7 +714,6 @@ const mcpDefaultOverridesMergeMigration: Migration = {
                        );
        }
 };
-
 const migrations: Migration[] = [
        localStorageMigration,
        idxdbMigration,
@@ -683,17 +734,17 @@ export const MigrationService = {
        },
 
        /**
-        * Check if a specific migration has been completed
+        * Get current migration state
         */
-       isCompleted(id: string): boolean {
-               return isMigrationCompleted(id);
+       getState(): MigrationState {
+               return getMigrationState();
        },
 
        /**
-        * Get current migration state
+        * Check if a specific migration has been completed
         */
-       getState(): MigrationState {
-               return getMigrationState();
+       isCompleted(id: string): boolean {
+               return isMigrationCompleted(id);
        },
 
        /**
@@ -701,6 +752,7 @@ export const MigrationService = {
         */
        resetState(): void {
                localStorage.removeItem(MIGRATION_STATE_KEY);
+
                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                        console.log('[Migration] State reset - all migrations will run again');
        },
@@ -711,6 +763,7 @@ export const MigrationService = {
         */
        async runAllMigrations(): Promise<void> {
                const state = getMigrationState();
+
                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                        console.log('[Migration] Starting migration run, state:', state);
 
@@ -718,14 +771,17 @@ export const MigrationService = {
                        if (isMigrationCompleted(migration.id)) {
                                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                        console.log(`[Migration] ${migration.id}: already completed, skipping`);
+
                                continue;
                        }
 
                        try {
                                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                        console.log(`[Migration] ${migration.id}: running...`);
+
                                await migration.run();
                                markMigrationCompleted(migration.id);
+
                                if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
                                        console.log(`[Migration] ${migration.id}: completed successfully`);
                        } catch (error) {
index 9574da59ef9f6a0b4e56910f97fa60e2f9b48343..7399a3cc6e433ec2f60b744ab850651df49a4c9e 100644 (file)
@@ -1,19 +1,19 @@
-import { ServerModelStatus } from '$lib/enums';
-import { apiFetch, apiPost, normalizeModelName } from '$lib/utils';
-import type { ParsedModelId } from '$lib/types/models';
 import {
-       MODEL_QUANTIZATION_SEGMENT_RE,
-       MODEL_CUSTOM_QUANTIZATION_PREFIX_RE,
-       MODEL_PARAMS_RE,
+       API_MODELS,
        MODEL_ACTIVATED_PARAMS_RE,
-       MODEL_IGNORED_SEGMENTS,
-       MODEL_WEIGHT_EXTENSION_RE,
+       MODEL_CUSTOM_QUANTIZATION_PREFIX_RE,
        MODEL_ID_NOT_FOUND,
        MODEL_ID_ORG_SEPARATOR,
-       MODEL_ID_SEGMENT_SEPARATOR,
        MODEL_ID_QUANTIZATION_SEPARATOR,
-       API_MODELS
+       MODEL_ID_SEGMENT_SEPARATOR,
+       MODEL_IGNORED_SEGMENTS,
+       MODEL_PARAMS_RE,
+       MODEL_QUANTIZATION_SEGMENT_RE,
+       MODEL_WEIGHT_EXTENSION_RE
 } from '$lib/constants';
+import { ServerModelStatus } from '$lib/enums';
+import type { ParsedModelId } from '$lib/types/models';
+import { apiFetch, apiPost, normalizeModelName } from '$lib/utils';
 
 export class ModelsService {
        /**
@@ -64,6 +64,7 @@ export class ModelsService {
         */
        static async load(modelId: string, extraArgs?: string[]): Promise<ApiRouterModelsLoadResponse> {
                const payload: { model: string; extra_args?: string[] } = { model: modelId };
+
                if (extraArgs && extraArgs.length > 0) {
                        payload.extra_args = extraArgs;
                }
@@ -131,21 +132,20 @@ export class ModelsService {
         */
        static parseModelId(modelId: string): ParsedModelId {
                const result: ParsedModelId = {
-                       raw: modelId,
-                       orgName: null,
+                       activatedParams: null,
                        modelName: null,
+                       orgName: null,
                        params: null,
-                       activatedParams: null,
                        quantization: null,
+                       raw: modelId,
                        tags: []
                };
-
                // 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, '');
-
                // 1. Extract colon-separated quantization (e.g. `model:Q4_K_M`)
                const colonIdx = source.indexOf(MODEL_ID_QUANTIZATION_SEPARATOR);
+
                let modelPath: string;
 
                if (colonIdx !== MODEL_ID_NOT_FOUND) {
@@ -157,6 +157,7 @@ export class ModelsService {
 
                // 2. Extract org name (e.g. `org/model` -> org = "org")
                const slashIdx = modelPath.indexOf(MODEL_ID_ORG_SEPARATOR);
+
                let modelStr: string;
 
                if (slashIdx !== MODEL_ID_NOT_FOUND) {
@@ -222,6 +223,7 @@ export class ModelsService {
                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());
index 1cf1624df7cf76e5668787ad2a6f11cb95c64b5c..aa130fe5eaf4335c54e659fa15e11983ac2c3c65 100644 (file)
@@ -1,64 +1,63 @@
-import { describe, it, expect } from 'vitest';
 import { ParameterSyncService } from './parameter-sync.service';
+import { describe, expect, it } from 'vitest';
 
 describe('ParameterSyncService', () => {
        describe('roundFloatingPoint', () => {
                it('should fix JavaScript floating-point precision issues', () => {
                        // Test the specific values from the screenshot
                        const mockServerParams = {
-                               top_p: 0.949999988079071,
                                min_p: 0.009999999776482582,
+                               samplers: ['top_k', 'typ_p', 'top_p', 'min_p', 'temperature'],
                                temperature: 0.800000011920929,
                                top_k: 40,
-                               samplers: ['top_k', 'typ_p', 'top_p', 'min_p', 'temperature']
+                               top_p: 0.949999988079071
                        };
-
                        const result = ParameterSyncService.extractServerDefaults({
                                ...mockServerParams,
-                               // Add other required fields to match the API type
-                               n_predict: 512,
-                               seed: -1,
-                               dynatemp_range: 0.0,
-                               dynatemp_exponent: 1.0,
-                               xtc_probability: 0.0,
-                               xtc_threshold: 0.1,
-                               typ_p: 1.0,
-                               repeat_last_n: 64,
-                               repeat_penalty: 1.0,
-                               presence_penalty: 0.0,
-                               frequency_penalty: 0.0,
-                               dry_multiplier: 0.0,
-                               dry_base: 1.75,
+                               chat_format: '',
                                dry_allowed_length: 2,
+                               dry_base: 1.75,
+                               dry_multiplier: 0.0,
                                dry_penalty_last_n: 64,
+                               dry_sequence_breakers: [],
+                               dynatemp_exponent: 1.0,
+                               dynatemp_range: 0.0,
+                               frequency_penalty: 0.0,
+                               generation_prompt: '',
+                               grammar: '',
+                               grammar_lazy: false,
+                               grammar_triggers: [],
+                               ignore_eos: false,
+                               logit_bias: [],
+                               lora: [],
+                               max_tokens: -1,
+                               min_keep: 0,
                                mirostat: 0,
-                               mirostat_tau: 5.0,
                                mirostat_eta: 0.1,
-                               stop: [],
-                               max_tokens: -1,
-                               n_keep: 0,
+                               mirostat_tau: 5.0,
                                n_discard: 0,
-                               ignore_eos: false,
-                               stream: true,
-                               logit_bias: [],
+                               n_keep: 0,
+                               // Add other required fields to match the API type
+                               n_predict: 512,
                                n_probs: 0,
-                               min_keep: 0,
-                               grammar: '',
-                               grammar_lazy: false,
-                               grammar_triggers: [],
+                               post_sampling_probs: false,
+                               presence_penalty: 0.0,
                                preserved_tokens: [],
-                               chat_format: '',
                                reasoning_format: '',
                                reasoning_in_content: false,
-                               generation_prompt: '',
+                               repeat_last_n: 64,
+                               repeat_penalty: 1.0,
+                               seed: -1,
                                'speculative.n_max': 0,
                                'speculative.n_min': 0,
                                'speculative.p_min': 0.0,
+                               stop: [],
+                               stream: true,
                                timings_per_token: false,
-                               post_sampling_probs: false,
-                               lora: [],
                                top_n_sigma: 0.0,
-                               dry_sequence_breakers: []
+                               typ_p: 1.0,
+                               xtc_probability: 0.0,
+                               xtc_threshold: 0.1
                        } as ApiLlamaCppServerProps['default_generation_settings']['params']);
 
                        // Check that the problematic floating-point values are rounded correctly
@@ -71,59 +70,58 @@ describe('ParameterSyncService', () => {
 
                it('should preserve non-numeric values', () => {
                        const mockServerParams = {
-                               samplers: ['top_k', 'temperature'],
                                max_tokens: -1,
+                               samplers: ['top_k', 'temperature'],
                                temperature: 0.7
                        };
-
                        const result = ParameterSyncService.extractServerDefaults({
                                ...mockServerParams,
-                               // Minimal required fields
-                               n_predict: 512,
-                               seed: -1,
-                               dynatemp_range: 0.0,
-                               dynatemp_exponent: 1.0,
-                               top_k: 40,
-                               top_p: 0.95,
-                               min_p: 0.05,
-                               xtc_probability: 0.0,
-                               xtc_threshold: 0.1,
-                               typ_p: 1.0,
-                               repeat_last_n: 64,
-                               repeat_penalty: 1.0,
-                               presence_penalty: 0.0,
-                               frequency_penalty: 0.0,
-                               dry_multiplier: 0.0,
-                               dry_base: 1.75,
+                               chat_format: '',
                                dry_allowed_length: 2,
+                               dry_base: 1.75,
+                               dry_multiplier: 0.0,
                                dry_penalty_last_n: 64,
+                               dry_sequence_breakers: [],
+                               dynatemp_exponent: 1.0,
+                               dynatemp_range: 0.0,
+                               frequency_penalty: 0.0,
+                               generation_prompt: '',
+                               grammar: '',
+                               grammar_lazy: false,
+                               grammar_triggers: [],
+                               ignore_eos: false,
+                               logit_bias: [],
+                               lora: [],
+                               min_keep: 0,
+                               min_p: 0.05,
                                mirostat: 0,
-                               mirostat_tau: 5.0,
                                mirostat_eta: 0.1,
-                               stop: [],
-                               n_keep: 0,
+                               mirostat_tau: 5.0,
                                n_discard: 0,
-                               ignore_eos: false,
-                               stream: true,
-                               logit_bias: [],
+                               n_keep: 0,
+                               // Minimal required fields
+                               n_predict: 512,
                                n_probs: 0,
-                               min_keep: 0,
-                               grammar: '',
-                               grammar_lazy: false,
-                               grammar_triggers: [],
+                               post_sampling_probs: false,
+                               presence_penalty: 0.0,
                                preserved_tokens: [],
-                               chat_format: '',
                                reasoning_format: '',
                                reasoning_in_content: false,
-                               generation_prompt: '',
+                               repeat_last_n: 64,
+                               repeat_penalty: 1.0,
+                               seed: -1,
                                'speculative.n_max': 0,
                                'speculative.n_min': 0,
                                'speculative.p_min': 0.0,
+                               stop: [],
+                               stream: true,
                                timings_per_token: false,
-                               post_sampling_probs: false,
-                               lora: [],
+                               top_k: 40,
                                top_n_sigma: 0.0,
-                               dry_sequence_breakers: []
+                               top_p: 0.95,
+                               typ_p: 1.0,
+                               xtc_probability: 0.0,
+                               xtc_threshold: 0.1
                        } as ApiLlamaCppServerProps['default_generation_settings']['params']);
 
                        expect(result.samplers).toBe('top_k;temperature');
index becaa1298abb07bd33e431d3964687beae7d142c..0ed9ebd48ce3cdfb5b53428088d40ea36c215a94 100644 (file)
@@ -1,7 +1,7 @@
-import { normalizeFloatingPoint } from '$lib/utils';
 import { SETTINGS_KEYS, SYNCABLE_PARAMETERS } from '$lib/constants';
-import type { ParameterRecord, ParameterInfo, ParameterValue } from '$lib/types';
-import { SyncableParameterType, ParameterSource } from '$lib/enums';
+import { ParameterSource, SyncableParameterType } from '$lib/enums';
+import type { ParameterInfo, ParameterRecord, ParameterValue } from '$lib/types';
+import { normalizeFloatingPoint } from '$lib/utils';
 
 export class ParameterSyncService {
        /**
@@ -42,6 +42,7 @@ export class ParameterSyncService {
                                        const value = (serverParams as unknown as Record<string, ParameterValue>)[
                                                param.serverKey
                                        ];
+
                                        if (value !== undefined) {
                                                // Apply precision rounding to avoid JavaScript floating-point issues
                                                extracted[param.key] = this.roundFloatingPoint(value);
@@ -120,15 +121,14 @@ export class ParameterSyncService {
        ): ParameterInfo {
                const hasPropsDefault = propsDefaults[key] !== undefined;
                const isUserOverride = userOverrides.has(key);
-
                // Simple logic: either using default (from props) or custom (user override)
                const source = isUserOverride ? ParameterSource.CUSTOM : ParameterSource.DEFAULT;
 
                return {
-                       value: currentValue,
-                       source,
                        serverDefault: hasPropsDefault ? propsDefaults[key] : undefined, // Keep same field name for compatibility
-                       userOverride: isUserOverride ? currentValue : undefined
+                       source,
+                       userOverride: isUserOverride ? currentValue : undefined,
+                       value: currentValue
                };
        }
 
@@ -160,6 +160,7 @@ export class ParameterSyncService {
         */
        static validateServerParameter(key: string, value: ParameterValue): boolean {
                const param = SYNCABLE_PARAMETERS.find((p) => p.key === key);
+
                if (!param) return false;
 
                switch (param.type) {
@@ -207,8 +208,8 @@ export class ParameterSyncService {
                        if (serverValue !== undefined) {
                                diff[key] = {
                                        current: currentValue,
-                                       server: serverValue,
-                                       differs: currentValue !== serverValue
+                                       differs: currentValue !== serverValue,
+                                       server: serverValue
                                };
                        }
                }
index 45c3e457732c1b7bb17f36f3c9583aee22b5eb47..46f4915fadb849e417b579bff0da7ac249b5db64 100644 (file)
@@ -20,6 +20,7 @@ export class PropsService {
         */
        static async fetch(autoload = false): Promise<ApiLlamaCppServerProps> {
                const params: Record<string, string> = {};
+
                if (!autoload) {
                        params.autoload = 'false';
                }
@@ -38,6 +39,7 @@ export class PropsService {
         */
        static async fetchForModel(modelId: string, autoload = false): Promise<ApiLlamaCppServerProps> {
                const params: Record<string, string> = { model: modelId };
+
                if (!autoload) {
                        params.autoload = 'false';
                }
index 40502121fb37e09baee6109ae396c1e89c344a30..189ff59a5ec9714c06ad4d7fed34b9013b599f8d 100644 (file)
@@ -1,5 +1,5 @@
-import { NEWLINE } from '$lib/constants';
 import WORKER_SHIM from './sandbox-worker.js?raw';
+import { NEWLINE } from '$lib/constants';
 
 /**
  * CSP for the harness document, inherited by the blob worker. connect-src
index f49a774a08e911280c66e14e2b0e567c43d8e1b6..7fb66e339fe01679a3b77b94237e1f2a0d0dd5b4 100644 (file)
@@ -1,3 +1,4 @@
+import { buildSandboxHarness } from './sandbox-harness';
 import {
        NEWLINE,
        SANDBOX_EMPTY_OUTPUT,
@@ -7,7 +8,6 @@ import {
        SANDBOX_TOOL_NAME,
        SANDBOX_TRUNCATION_NOTICE
 } from '$lib/constants';
-import { buildSandboxHarness } from './sandbox-harness';
 import { config } from '$lib/stores/settings.svelte';
 import type { ToolExecutionResult } from '$lib/types';
 
@@ -22,14 +22,17 @@ const harnessCache: Record<string, string> = {};
 async function getHarness(): Promise<string> {
        const enabled = !!config().symbolicMathEnabled;
        const key = enabled ? 'nerdamer' : 'plain';
+
        if (!harnessCache[key]) {
                if (enabled) {
                        const { default: nerdamerJs } = await import('virtual:nerdamer');
+
                        harnessCache[key] = buildSandboxHarness(nerdamerJs);
                } else {
                        harnessCache[key] = buildSandboxHarness('');
                }
        }
+
        return harnessCache[key];
 }
 
@@ -53,7 +56,9 @@ function formatReply(reply: SandboxReply): ToolExecutionResult {
        }
 
        let content = lines.join(NEWLINE);
+
        if (!content) content = SANDBOX_EMPTY_OUTPUT;
+
        if (content.length > SANDBOX_OUTPUT_MAX_CHARS) {
                content = `${content.slice(0, SANDBOX_OUTPUT_MAX_CHARS)}${NEWLINE}${SANDBOX_TRUNCATION_NOTICE}`;
        }
@@ -78,12 +83,12 @@ export class SandboxService {
                }
 
                const code = typeof params.code === 'string' ? params.code : '';
+
                if (!code) {
                        return { content: 'Missing required parameter: code', isError: true };
                }
 
                const harness = await getHarness();
-
                const requested = Number(params.timeout_ms);
                const timeoutMs =
                        Number.isFinite(requested) && requested > 0
@@ -92,6 +97,7 @@ export class SandboxService {
 
                return new Promise<ToolExecutionResult>((resolve, reject) => {
                        const iframe = document.createElement('iframe');
+
                        iframe.setAttribute('sandbox', 'allow-scripts');
                        iframe.style.display = 'none';
                        iframe.srcdoc = harness;
@@ -105,24 +111,23 @@ export class SandboxService {
                                signal?.removeEventListener('abort', onAbort);
                                iframe.remove();
                        };
-
                        const finish = (result: ToolExecutionResult) => {
                                if (settled) return;
+
                                cleanup();
                                resolve(result);
                        };
-
                        const onAbort = () => {
                                if (settled) return;
+
                                cleanup();
                                reject(new DOMException('Sandbox execution aborted', 'AbortError'));
                        };
-
                        const onMessage = (event: MessageEvent) => {
                                if (event.source !== iframe.contentWindow) return;
+
                                finish(formatReply((event.data ?? {}) as SandboxReply));
                        };
-
                        const timer = setTimeout(
                                () => finish({ content: `Execution timed out after ${timeoutMs} ms`, isError: true }),
                                timeoutMs
index 765fb532212040e2a7f34c69d3a112bbcd30d9e5..cd6b12cebe0644a6b219e6c313a3cbf3094b6886 100644 (file)
@@ -1,10 +1,10 @@
 import { base } from '$app/paths';
-import { getJsonHeaders } from '$lib/utils/api-headers';
-import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
-import { apiFetch } from '$lib/utils';
 import { API_TOOLS, X_TOOL_CWD_HEADER } from '$lib/constants';
 import { ToolResponseField } from '$lib/enums';
-import type { ToolExecutionResult, ServerBuiltinToolInfo } from '$lib/types';
+import type { ServerBuiltinToolInfo, ToolExecutionResult } from '$lib/types';
+import { apiFetch } from '$lib/utils';
+import { getJsonHeaders } from '$lib/utils/api-headers';
+import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
 
 export class ToolsService {
        /**
@@ -30,9 +30,9 @@ export class ToolsService {
                cwd?: string
        ): Promise<ToolExecutionResult> {
                const result = await apiFetch<Record<string, unknown>>(API_TOOLS.EXECUTE, {
-                       method: 'POST',
-                       body: JSON.stringify({ tool: toolName, params }),
+                       body: JSON.stringify({ params, tool: toolName }),
                        headers: cwd ? { [X_TOOL_CWD_HEADER]: cwd } : undefined,
+                       method: 'POST',
                        signal
                });
 
@@ -59,9 +59,9 @@ export class ToolsService {
                cwd?: string
        ): Promise<Record<string, unknown>> {
                return apiFetch<Record<string, unknown>>(API_TOOLS.EXECUTE, {
-                       method: 'POST',
-                       body: JSON.stringify({ tool: toolName, params }),
+                       body: JSON.stringify({ params, tool: toolName }),
                        headers: cwd ? { [X_TOOL_CWD_HEADER]: cwd } : undefined,
+                       method: 'POST',
                        signal
                });
        }
@@ -88,16 +88,19 @@ export class ToolsService {
                cwd?: string
        ): AsyncGenerator<ToolStreamEvent> {
                const headers = getJsonHeaders();
+
                if (cwd) headers[X_TOOL_CWD_HEADER] = cwd;
+
                const response = await fetch(`${base}${API_TOOLS.EXECUTE}`, {
-                       method: 'POST',
+                       body: JSON.stringify({ params, stream: true, tool: toolName }),
                        headers,
-                       body: JSON.stringify({ tool: toolName, params, stream: true }),
+                       method: 'POST',
                        signal
                });
 
                if (!response.ok || !response.body) {
                        const detail = await formatNonOkResponse(response);
+
                        throw new Error(detail);
                }
 
@@ -105,14 +108,18 @@ export class ToolsService {
 
                while (true) {
                        const next: IteratorResult<SseJsonEvent<ToolServerEvent>> = await iterator.next();
+
                        if (next.done) return;
+
                        const event = next.value.data;
 
                        if (event.chunk !== undefined) {
                                yield { chunk: event.chunk, done: false };
                        }
+
                        if (event.done) {
                                yield { chunk: null, done: true, error: event.error };
+
                                return;
                        }
                }
@@ -140,18 +147,23 @@ interface ToolServerEvent {
 
 async function formatNonOkResponse(response: Response): Promise<string> {
        const status = `${response.status} ${response.statusText}`.trim();
+
        try {
                const errBody = (await response.clone().json()) as { error?: string; message?: string };
+
                if (errBody?.error) return `${status}: ${errBody.error}`;
+
                if (errBody?.message) return `${status}: ${errBody.message}`;
        } catch (error) {
                console.error('[tools] Non-JSON error response, falling back to raw text:', error);
                try {
                        const text = await response.text();
+
                        if (text.trim()) return `${status}: ${text.trim()}`;
                } catch (error) {
                        console.error('[tools] Failed to read error response as text:', error);
                }
        }
+
        return status || `HTTP ${response.status}`;
 }
index 71c3347be6a40cbe7bcefb4a0ab1727b2c6331db..080ed7c89ab74b0d4489feccda87b9f16e77c251 100644 (file)
  * @see mcpStore in stores/mcp.svelte.ts for MCP operations
  */
 
-import { ChatService } from '$lib/services';
-import { config } from '$lib/stores/settings.svelte';
-import { conversationsStore } from '$lib/stores/conversations.svelte';
-import { mcpStore } from '$lib/stores/mcp.svelte';
-import { modelsStore } from '$lib/stores/models.svelte';
-import { toolsStore } from '$lib/stores/tools.svelte';
-import { permissionsStore } from '$lib/stores/permissions.svelte';
-import { BuiltInTool, ToolSource, ToolPermissionDecision } from '$lib/enums';
-import { SvelteMap } from 'svelte/reactivity';
-import { ToolsService } from '$lib/services/tools.service';
-import { SandboxService } from '$lib/services/sandbox.service';
-import { isAbortError } from '$lib/utils';
 import { DEFAULT_AGENTIC_CONFIG, NEWLINE } from '$lib/constants';
 import {
-       IMAGE_MIME_TO_EXTENSION,
        DATA_URI_BASE64_REGEX,
-       MCP_ATTACHMENT_NAME_PREFIX,
-       DEFAULT_IMAGE_EXTENSION
+       DEFAULT_IMAGE_EXTENSION,
+       IMAGE_MIME_TO_EXTENSION,
+       MCP_ATTACHMENT_NAME_PREFIX
 } from '$lib/constants';
+import { BuiltInTool, ToolPermissionDecision, ToolSource } from '$lib/enums';
 import {
        AttachmentType,
        ContentPartType,
@@ -46,49 +35,60 @@ import {
        MimeTypePrefix,
        ToolCallType
 } from '$lib/enums';
+import { ChatService } from '$lib/services';
+import { SandboxService } from '$lib/services/sandbox.service';
+import { ToolsService } from '$lib/services/tools.service';
+import { conversationsStore } from '$lib/stores/conversations.svelte';
+import { mcpStore } from '$lib/stores/mcp.svelte';
+import { modelsStore } from '$lib/stores/models.svelte';
+import { permissionsStore } from '$lib/stores/permissions.svelte';
+import { config } from '$lib/stores/settings.svelte';
+import { toolsStore } from '$lib/stores/tools.svelte';
 import type {
+       AgenticConfig,
        AgenticFlowParams,
        AgenticFlowResult,
        AgenticSession,
-       AgenticConfig,
-       SettingsConfigType,
        McpServerOverride,
-       MCPToolCall
+       MCPToolCall,
+       SettingsConfigType
 } from '$lib/types';
 import type {
-       AgenticMessage,
-       AgenticToolCallList,
        AgenticFlowCallbacks,
        AgenticFlowOptions,
+       AgenticMessage,
+       AgenticToolCallList,
        SteeringMessage
 } from '$lib/types/agentic';
 import type {
        ApiChatCompletionToolCall,
-       ApiChatMessageData,
-       ApiChatMessageContentPart
+       ApiChatMessageContentPart,
+       ApiChatMessageData
 } from '$lib/types/api';
 import type {
+       ChatMessageAgenticTimings,
+       ChatMessageAgenticTurnStats,
        ChatMessagePromptProgress,
        ChatMessageTimings,
-       ChatMessageAgenticTimings,
-       ChatMessageToolCallTiming,
-       ChatMessageAgenticTurnStats
+       ChatMessageToolCallTiming
 } from '$lib/types/chat';
 import type {
        DatabaseMessage,
        DatabaseMessageExtra,
        DatabaseMessageExtraImageFile
 } from '$lib/types/database';
+import { isAbortError } from '$lib/utils';
+import { SvelteMap } from 'svelte/reactivity';
 
 function createDefaultSession(): AgenticSession {
        return {
-               isRunning: false,
                currentTurn: 0,
-               totalToolCalls: 0,
+               executingToolCallId: null,
+               isRunning: false,
                lastError: null,
-               streamingToolCall: null,
                pendingPermissionRequest: null,
-               executingToolCallId: null
+               streamingToolCall: null,
+               totalToolCalls: 0
        };
 }
 
@@ -100,36 +100,39 @@ function toAgenticMessages(messages: ApiChatMessageData[]): AgenticMessage[] {
                        message.tool_calls.length > 0
                ) {
                        return {
-                               role: MessageRole.ASSISTANT,
                                content: message.content,
                                reasoning_content: message.reasoning_content,
+                               role: MessageRole.ASSISTANT,
                                tool_calls: message.tool_calls.map((call, index) => ({
-                                       id: call.id ?? `call_${index}`,
-                                       type: (call.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION,
                                        function: {
-                                               name: call.function?.name ?? '',
-                                               arguments: call.function?.arguments ?? ''
-                                       }
+                                               arguments: call.function?.arguments ?? '',
+                                               name: call.function?.name ?? ''
+                                       },
+                                       id: call.id ?? `call_${index}`,
+                                       type: (call.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION
                                }))
                        } satisfies AgenticMessage;
                }
+
                if (message.role === MessageRole.ASSISTANT) {
                        return {
-                               role: MessageRole.ASSISTANT,
                                content: message.content,
-                               reasoning_content: message.reasoning_content
+                               reasoning_content: message.reasoning_content,
+                               role: MessageRole.ASSISTANT
                        } satisfies AgenticMessage;
                }
+
                if (message.role === MessageRole.TOOL && message.tool_call_id) {
                        return {
+                               content: typeof message.content === 'string' ? message.content : '',
                                role: MessageRole.TOOL,
-                               tool_call_id: message.tool_call_id,
-                               content: typeof message.content === 'string' ? message.content : ''
+                               tool_call_id: message.tool_call_id
                        } satisfies AgenticMessage;
                }
+
                return {
-                       role: message.role as MessageRole.SYSTEM | MessageRole.USER,
-                       content: message.content
+                       content: message.content,
+                       role: message.role as MessageRole.SYSTEM | MessageRole.USER
                } satisfies AgenticMessage;
        });
 }
@@ -159,20 +162,24 @@ class AgenticStore {
                for (const session of this._sessions.values()) {
                        if (session.isRunning) return true;
                }
+
                return false;
        }
 
        getSession(conversationId: string): AgenticSession {
                let session = this._sessions.get(conversationId);
+
                if (!session) {
                        session = createDefaultSession();
                        this._sessions.set(conversationId, session);
                }
+
                return session;
        }
 
        private updateSession(conversationId: string, update: Partial<AgenticSession>): void {
                const session = this.getSession(conversationId);
+
                this._sessions.set(conversationId, { ...session, ...update });
        }
 
@@ -182,9 +189,11 @@ class AgenticStore {
 
        getActiveSessions(): Array<{ conversationId: string; session: AgenticSession }> {
                const active: Array<{ conversationId: string; session: AgenticSession }> = [];
+
                for (const [conversationId, session] of this._sessions.entries()) {
                        if (session.isRunning) active.push({ conversationId, session });
                }
+
                return active;
        }
 
@@ -224,6 +233,7 @@ class AgenticStore {
 
        resolveContinue(conversationId: string, shouldContinue: boolean): void {
                const resolver = this._continueResolvers.get(conversationId);
+
                if (resolver) {
                        this._continueResolvers.delete(conversationId);
                        resolver(shouldContinue);
@@ -232,6 +242,7 @@ class AgenticStore {
 
        resolvePermission(conversationId: string, decision: ToolPermissionDecision): void {
                const resolver = this._permissionResolvers.get(conversationId);
+
                if (resolver) {
                        this._permissionResolvers.delete(conversationId);
                        resolver(decision);
@@ -279,8 +290,11 @@ class AgenticStore {
         */
        consumePendingSteeringMessage(conversationId: string): SteeringMessage | null {
                const msg = this._steeringMessages.get(conversationId);
+
                if (!msg) return null;
+
                this._steeringMessages.delete(conversationId);
+
                return msg;
        }
 
@@ -291,6 +305,7 @@ class AgenticStore {
                        toolsStore.builtinTools.length > 0 ||
                        toolsStore.frontendTools.length > 0 ||
                        toolsStore.customTools.length > 0;
+
                return {
                        enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled,
                        maxTurns
@@ -299,8 +314,11 @@ class AgenticStore {
 
        private parseToolArguments(args: string | Record<string, unknown>): Record<string, unknown> {
                if (typeof args === 'object') return args;
+
                const trimmed = args.trim();
+
                if (trimmed === '') return {};
+
                return JSON.parse(trimmed) as Record<string, unknown>;
        }
 
@@ -311,21 +329,24 @@ class AgenticStore {
                signal?: AbortSignal
        ): Promise<ToolPermissionDecision> {
                const permissionKey = toolsStore.getPermissionKey(toolName);
+
                if (permissionKey && permissionsStore.hasTool(permissionKey)) {
                        return ToolPermissionDecision.ONCE;
                }
 
-               this._pendingPermissions.set(conversationId, { toolName, serverLabel });
+               this._pendingPermissions.set(conversationId, { serverLabel, toolName });
 
                return new Promise<ToolPermissionDecision>((resolve) => {
                        if (signal?.aborted) {
                                this._pendingPermissions.set(conversationId, null);
                                resolve(ToolPermissionDecision.DENY);
+
                                return;
                        }
 
                        this._permissionResolvers.set(conversationId, (decision) => {
                                this._pendingPermissions.set(conversationId, null);
+
                                if (decision === ToolPermissionDecision.ALWAYS && permissionKey) {
                                        permissionsStore.allowTool(permissionKey);
                                } else if (decision === ToolPermissionDecision.ALWAYS_SERVER) {
@@ -337,8 +358,10 @@ class AgenticStore {
                                                )
                                                .map((t) => toolsStore.getPermissionKey(t.definition.function.name)!)
                                                .filter((k): k is string => k !== null);
+
                                        permissionsStore.allowTools(serverToolKeys);
                                }
+
                                resolve(decision);
                        });
 
@@ -346,6 +369,7 @@ class AgenticStore {
                                'abort',
                                () => {
                                        const resolver = this._permissionResolvers.get(conversationId);
+
                                        if (resolver) {
                                                this._permissionResolvers.delete(conversationId);
                                                this._pendingPermissions.set(conversationId, null);
@@ -364,6 +388,7 @@ class AgenticStore {
                        if (signal?.aborted) {
                                this._pendingContinueRequests.set(conversationId, false);
                                resolve(false);
+
                                return;
                        }
 
@@ -376,6 +401,7 @@ class AgenticStore {
                                'abort',
                                () => {
                                        const resolver = this._continueResolvers.get(conversationId);
+
                                        if (resolver) {
                                                this._continueResolvers.delete(conversationId);
                                                this._pendingContinueRequests.set(conversationId, false);
@@ -388,7 +414,7 @@ class AgenticStore {
        }
 
        async runAgenticFlow(params: AgenticFlowParams): Promise<AgenticFlowResult> {
-               const { conversationId, messages, options = {}, callbacks, signal, perChatOverrides } = params;
+               const { callbacks, conversationId, messages, options = {}, perChatOverrides, signal } = params;
 
                // Clear any pending permissions/continue requests for this conversation when starting a new flow
                this._pendingPermissions.set(conversationId, null);
@@ -403,9 +429,11 @@ class AgenticStore {
                }
 
                const agenticConfig = this.getConfig(config(), perChatOverrides);
+
                if (!agenticConfig.enabled) return { handled: false };
 
                const hasMcpServers = mcpStore.hasEnabledServers(perChatOverrides);
+
                if (hasMcpServers) {
                        const initialized = await mcpStore.ensureInitialized(perChatOverrides);
 
@@ -415,6 +443,7 @@ class AgenticStore {
                }
 
                const tools = toolsStore.getEnabledToolsForLLM();
+
                if (tools.length === 0) {
                        return { handled: false };
                }
@@ -428,42 +457,48 @@ class AgenticStore {
                                                return ChatService.convertDbMessageToApiChatMessageData(
                                                        msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] }
                                                );
+
                                        return msg as ApiChatMessageData;
                                })
                        )
                ).filter((msg: { role: ChatRole; content: string | ApiChatMessageContentPart[] }) => {
                        if (msg.role === MessageRole.SYSTEM) {
                                const content = typeof msg.content === 'string' ? msg.content : '';
+
                                return content.trim().length > 0;
                        }
+
                        return true;
                });
 
                this.updateSession(conversationId, {
-                       isRunning: true,
                        currentTurn: 0,
-                       totalToolCalls: 0,
-                       lastError: null
+                       isRunning: true,
+                       lastError: null,
+                       totalToolCalls: 0
                });
 
                if (hasMcpServers) mcpStore.acquireConnection();
 
                try {
                        await this.executeAgenticLoop({
+                               agenticConfig,
+                               callbacks,
                                conversationId,
                                messages: normalizedMessages,
                                options,
-                               tools,
-                               agenticConfig,
-                               callbacks,
-                               signal
+                               signal,
+                               tools
                        });
+
                        return { handled: true };
                } catch (error) {
                        const normalizedError = error instanceof Error ? error : new Error(String(error));
+
                        this.updateSession(conversationId, { lastError: normalizedError });
                        callbacks.onError?.(normalizedError);
-                       return { handled: true, error: normalizedError };
+
+                       return { error: normalizedError, handled: true };
                } finally {
                        this.updateSession(conversationId, { isRunning: false });
 
@@ -486,40 +521,40 @@ class AgenticStore {
                callbacks: AgenticFlowCallbacks;
                signal?: AbortSignal;
        }): Promise<void> {
-               const { conversationId, messages, options, tools, agenticConfig, callbacks, signal } = params;
+               const { agenticConfig, callbacks, conversationId, messages, options, signal, tools } = params;
                const {
-                       onChunk,
-                       onReasoningChunk,
-                       onToolCallsStreaming,
+                       createAssistantMessage,
+                       createToolResultMessage,
+                       onAssistantTurnComplete,
                        onAttachments,
-                       onModel,
+                       onChunk,
                        onCompletionId,
-                       onAssistantTurnComplete,
-                       createToolResultMessage,
-                       updateToolResultMessage,
-                       createAssistantMessage,
                        onFlowComplete,
+                       onModel,
+                       onReasoningChunk,
                        onTimings,
-                       onTurnComplete
+                       onToolCallsStreaming,
+                       onTurnComplete,
+                       updateToolResultMessage
                } = callbacks;
-
                const sessionMessages: AgenticMessage[] = toAgenticMessages(messages);
+
                let capturedTimings: ChatMessageTimings | undefined;
                let totalToolCallCount = 0;
 
                const agenticTimings: ChatMessageAgenticTimings = {
-                       turns: 0,
+                       llm: { predicted_ms: 0, predicted_n: 0, prompt_ms: 0, prompt_n: 0 },
+                       perTurn: [],
+                       toolCalls: [],
                        toolCallsCount: 0,
                        toolsMs: 0,
-                       toolCalls: [],
-                       perTurn: [],
-                       llm: { predicted_n: 0, predicted_ms: 0, prompt_n: 0, prompt_ms: 0 }
+                       turns: 0
                };
                const maxTurns = agenticConfig.maxTurns;
-
                const effectiveModel = options.model || modelsStore.models[0]?.model || '';
 
                let turn = 0;
+
                while (true) {
                        if (turn >= maxTurns) {
                                // Turn limit reached - ask user whether to continue
@@ -530,6 +565,7 @@ class AgenticStore {
 
                                if (!shouldContinue || signal?.aborted) {
                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                        return;
                                }
 
@@ -542,6 +578,7 @@ class AgenticStore {
 
                        if (signal?.aborted) {
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                return;
                        }
 
@@ -558,10 +595,10 @@ class AgenticStore {
                        let turnTimings: ChatMessageTimings | undefined;
 
                        const turnStats: ChatMessageAgenticTurnStats = {
-                               turn: turn + 1,
-                               llm: { predicted_n: 0, predicted_ms: 0, prompt_n: 0, prompt_ms: 0 },
+                               llm: { predicted_ms: 0, predicted_n: 0, prompt_ms: 0, prompt_n: 0 },
                                toolCalls: [],
-                               toolsMs: 0
+                               toolsMs: 0,
+                               turn: turn + 1
                        };
 
                        try {
@@ -569,16 +606,30 @@ class AgenticStore {
                                        sessionMessages as ApiChatMessageData[],
                                        {
                                                ...options,
-                                               stream: true,
-                                               tools: tools.length > 0 ? tools : undefined,
                                                onChunk: (chunk: string) => {
                                                        turnContent += chunk;
                                                        onChunk?.(chunk);
                                                },
+                                               onComplete: () => {
+                                                       /* Completion handled after sendMessage resolves */
+                                               },
+                                               onCompletionId,
+                                               onError: (error: Error) => {
+                                                       throw error;
+                                               },
+                                               onModel,
                                                onReasoningChunk: (chunk: string) => {
                                                        turnReasoningContent += chunk;
                                                        onReasoningChunk?.(chunk);
                                                },
+                                               onTimings: (timings?: ChatMessageTimings, progress?: ChatMessagePromptProgress) => {
+                                                       onTimings?.(timings, progress);
+
+                                                       if (timings) {
+                                                               capturedTimings = timings;
+                                                               turnTimings = timings;
+                                                       }
+                                               },
                                                onToolCallChunk: (serialized: string) => {
                                                        try {
                                                                turnToolCalls = JSON.parse(serialized) as ApiChatCompletionToolCall[];
@@ -589,6 +640,7 @@ class AgenticStore {
                                                                        const name = turnToolCalls[0].function.name || '';
                                                                        const args = turnToolCalls[0].function.arguments || '';
                                                                        const argsLengthBucket = Math.floor(args.length / 100);
+
                                                                        if (
                                                                                name !== lastStreamingToolCallName ||
                                                                                argsLengthBucket !== lastStreamingToolCallArgsLength
@@ -596,7 +648,7 @@ class AgenticStore {
                                                                                lastStreamingToolCallName = name;
                                                                                lastStreamingToolCallArgsLength = argsLengthBucket;
                                                                                this.updateSession(conversationId, {
-                                                                                       streamingToolCall: { name, arguments: args }
+                                                                                       streamingToolCall: { arguments: args, name }
                                                                                });
                                                                        }
                                                                }
@@ -604,21 +656,8 @@ class AgenticStore {
                                                                /* Ignore parse errors during streaming */
                                                        }
                                                },
-                                               onModel,
-                                               onCompletionId,
-                                               onTimings: (timings?: ChatMessageTimings, progress?: ChatMessagePromptProgress) => {
-                                                       onTimings?.(timings, progress);
-                                                       if (timings) {
-                                                               capturedTimings = timings;
-                                                               turnTimings = timings;
-                                                       }
-                                               },
-                                               onComplete: () => {
-                                                       /* Completion handled after sendMessage resolves */
-                                               },
-                                               onError: (error: Error) => {
-                                                       throw error;
-                                               }
+                                               stream: true,
+                                               tools: tools.length > 0 ? tools : undefined
                                        },
                                        conversationId,
                                        signal
@@ -646,9 +685,12 @@ class AgenticStore {
                                                undefined
                                        );
                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                        return;
                                }
+
                                const normalizedError = error instanceof Error ? error : new Error('LLM stream error');
+
                                // preserve partial output as is, the outer error dialog informs the user separately
                                await onAssistantTurnComplete?.(
                                        turnContent,
@@ -657,6 +699,7 @@ class AgenticStore {
                                        undefined
                                );
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                throw normalizedError;
                        }
 
@@ -672,6 +715,7 @@ class AgenticStore {
                                        undefined
                                );
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                return;
                        }
 
@@ -686,6 +730,7 @@ class AgenticStore {
                                        turnToolCalls.length > 0 ? this.normalizeToolCalls(turnToolCalls) : undefined
                                );
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                return;
                        }
 
@@ -711,6 +756,7 @@ class AgenticStore {
 
                        // Normalize and save assistant turn with tool calls
                        const normalizedCalls = this.normalizeToolCalls(turnToolCalls);
+
                        if (normalizedCalls.length === 0) {
                                await onAssistantTurnComplete?.(
                                        turnContent,
@@ -719,6 +765,7 @@ class AgenticStore {
                                        undefined
                                );
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                return;
                        }
 
@@ -735,9 +782,9 @@ class AgenticStore {
 
                        // Add assistant message to session history
                        sessionMessages.push({
-                               role: MessageRole.ASSISTANT,
                                content: turnContent || undefined,
                                reasoning_content: turnReasoningContent || undefined,
+                               role: MessageRole.ASSISTANT,
                                tool_calls: normalizedCalls
                        });
 
@@ -747,6 +794,7 @@ class AgenticStore {
 
                                if (signal?.aborted) {
                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                        return;
                                }
 
@@ -758,21 +806,23 @@ class AgenticStore {
                                        for (let j = i; j < normalizedCalls.length; j++) {
                                                const remainingCall = normalizedCalls[j];
                                                const interruptedContent = 'Tool execution was interrupted by a new user message.';
+
                                                if (createToolResultMessage) {
                                                        await createToolResultMessage(remainingCall.id, interruptedContent);
                                                }
+
                                                sessionMessages.push({
+                                                       content: interruptedContent,
                                                        role: MessageRole.TOOL,
-                                                       tool_call_id: remainingCall.id,
-                                                       content: interruptedContent
+                                                       tool_call_id: remainingCall.id
                                                });
                                        }
+
                                        break;
                                }
 
                                const toolName = toolCall.function.name;
                                const serverLabel = toolsStore.getToolServerLabel(toolName);
-
                                // Ask for permission before executing the tool
                                const permission = await this.requestPermission(
                                        conversationId,
@@ -786,6 +836,7 @@ class AgenticStore {
 
                                if (signal?.aborted) {
                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                        return;
                                }
 
@@ -815,14 +866,17 @@ class AgenticStore {
                                                        const args = this.parseToolArguments(toolCall.function.arguments);
                                                        const cwd = conversationsStore.activeConversation?.cwd;
                                                        const msg = await createToolResultMessage(toolCall.id, '', undefined, cwd);
+
                                                        createdToolResultMessageId = msg.id;
 
                                                        let accumulated = '';
+
                                                        for await (const ev of ToolsService.streamTool(toolName, args, signal, cwd)) {
                                                                if (ev.chunk !== null) {
                                                                        accumulated += ev.chunk;
                                                                        await updateToolResultMessage(msg.id, accumulated);
                                                                }
+
                                                                if (ev.done) {
                                                                        if (ev.error) {
                                                                                accumulated = accumulated
@@ -831,6 +885,7 @@ class AgenticStore {
                                                                                await updateToolResultMessage(msg.id, accumulated);
                                                                                toolSuccess = false;
                                                                        }
+
                                                                        break;
                                                                }
                                                        }
@@ -852,8 +907,8 @@ class AgenticStore {
                                                        if (executionResult.isError) toolSuccess = false;
                                                } else {
                                                        const mcpCall: MCPToolCall = {
-                                                               id: toolCall.id,
-                                                               function: { name: toolName, arguments: toolCall.function.arguments }
+                                                               function: { arguments: toolCall.function.arguments, name: toolName },
+                                                               id: toolCall.id
                                                        };
                                                        const executionResult = await mcpStore.executeTool(mcpCall, signal);
 
@@ -863,14 +918,17 @@ class AgenticStore {
                                                if (isAbortError(error)) {
                                                        this.updateSession(conversationId, { executingToolCallId: null });
                                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                                        return;
                                                }
+
                                                // Carry the partial stream contents already mirrored to the UI -
                                                // they show up as live output even if the stream broke off mid-run.
                                                result = result
                                                        ? `${result}\nError: ${error instanceof Error ? error.message : String(error)}`
                                                        : `Error: ${error instanceof Error ? error.message : String(error)}`;
                                                toolSuccess = false;
+
                                                if (createdToolResultMessageId && updateToolResultMessage) {
                                                        await updateToolResultMessage(createdToolResultMessageId, result);
                                                }
@@ -881,8 +939,8 @@ class AgenticStore {
 
                                const toolDurationMs = performance.now() - toolStartTime;
                                const toolTiming: ChatMessageToolCallTiming = {
-                                       name: toolCall.function.name,
                                        duration_ms: Math.round(toolDurationMs),
+                                       name: toolCall.function.name,
                                        success: toolSuccess
                                };
 
@@ -894,10 +952,11 @@ class AgenticStore {
 
                                if (signal?.aborted) {
                                        onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                        return;
                                }
 
-                               const { cleanedResult, attachments } = this.extractBase64Attachments(result);
+                               const { attachments, cleanedResult } = this.extractBase64Attachments(result);
 
                                // For streaming tools the result message was created empty
                                // at the start of execution and updated in place as chunks
@@ -906,8 +965,10 @@ class AgenticStore {
                                // the final accumulator (rare, since chunks usually don't
                                // carry image data URIs) and emit the attachments callback.
                                let toolResultMessage: DatabaseMessage | undefined;
+
                                if (createdToolResultMessageId) {
                                        toolResultMessage = { id: createdToolResultMessageId } as DatabaseMessage;
+
                                        if (attachments.length > 0 && updateToolResultMessage) {
                                                await updateToolResultMessage(createdToolResultMessageId, cleanedResult, attachments);
                                        }
@@ -925,16 +986,17 @@ class AgenticStore {
 
                                // Build content parts for session history (including images for vision models)
                                const contentParts: ApiChatMessageContentPart[] = [
-                                       { type: ContentPartType.TEXT, text: cleanedResult }
+                                       { text: cleanedResult, type: ContentPartType.TEXT }
                                ];
+
                                for (const attachment of attachments) {
                                        if (attachment.type === AttachmentType.IMAGE) {
                                                if (modelsStore.modelSupportsVision(effectiveModel)) {
                                                        contentParts.push({
-                                                               type: ContentPartType.IMAGE_URL,
                                                                image_url: {
                                                                        url: (attachment as DatabaseMessageExtraImageFile).base64Url
-                                                               }
+                                                               },
+                                                               type: ContentPartType.IMAGE_URL
                                                        });
                                                } else {
                                                        console.info(
@@ -945,9 +1007,9 @@ class AgenticStore {
                                }
 
                                sessionMessages.push({
+                                       content: contentParts.length === 1 ? cleanedResult : contentParts,
                                        role: MessageRole.TOOL,
-                                       tool_call_id: toolCall.id,
-                                       content: contentParts.length === 1 ? cleanedResult : contentParts
+                                       tool_call_id: toolCall.id
                                });
                        }
 
@@ -955,6 +1017,7 @@ class AgenticStore {
                                agenticTimings.perTurn!.push(turnStats);
 
                                const intermediateTimings = this.buildFinalTimings(capturedTimings, agenticTimings);
+
                                if (intermediateTimings) onTurnComplete?.(intermediateTimings);
                        }
 
@@ -964,6 +1027,7 @@ class AgenticStore {
                                        '[AgenticStore] Steering message detected after tool execution, exiting agentic flow'
                                );
                                onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
+
                                return;
                        }
 
@@ -976,25 +1040,27 @@ class AgenticStore {
                agenticTimings: ChatMessageAgenticTimings
        ): ChatMessageTimings | undefined {
                if (agenticTimings.toolCallsCount === 0) return capturedTimings;
+
                return {
-                       predicted_n: capturedTimings?.predicted_n,
+                       agentic: agenticTimings,
+                       cache_n: capturedTimings?.cache_n,
                        predicted_ms: capturedTimings?.predicted_ms,
-                       prompt_n: capturedTimings?.prompt_n,
+                       predicted_n: capturedTimings?.predicted_n,
                        prompt_ms: capturedTimings?.prompt_ms,
-                       cache_n: capturedTimings?.cache_n,
-                       agentic: agenticTimings
+                       prompt_n: capturedTimings?.prompt_n
                };
        }
 
        private normalizeToolCalls(toolCalls: ApiChatCompletionToolCall[]): AgenticToolCallList {
                if (!toolCalls) return [];
+
                return toolCalls.map((call, index) => ({
-                       id: call?.id ?? `tool_${index}`,
-                       type: (call?.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION,
                        function: {
-                               name: call?.function?.name ?? '',
-                               arguments: call?.function?.arguments ?? ''
-                       }
+                               arguments: call?.function?.arguments ?? '',
+                               name: call?.function?.name ?? ''
+                       },
+                       id: call?.id ?? `tool_${index}`,
+                       type: (call?.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION
                }));
        }
 
@@ -1003,17 +1069,18 @@ class AgenticStore {
                attachments: DatabaseMessageExtra[];
        } {
                if (!result.trim()) {
-                       return { cleanedResult: result, attachments: [] };
+                       return { attachments: [], cleanedResult: result };
                }
 
                const lines = result.split(NEWLINE);
                const attachments: DatabaseMessageExtra[] = [];
+
                let attachmentIndex = 0;
 
                const cleanedLines = lines.map((line) => {
                        const trimmedLine = line.trim();
-
                        const match = trimmedLine.match(DATA_URI_BASE64_REGEX);
+
                        if (!match) {
                                return line;
                        }
@@ -1029,7 +1096,7 @@ class AgenticStore {
                        const name = this.buildAttachmentName(mimeType, attachmentIndex);
 
                        if (mimeType.startsWith(MimeTypePrefix.IMAGE)) {
-                               attachments.push({ type: AttachmentType.IMAGE, name, base64Url: trimmedLine });
+                               attachments.push({ base64Url: trimmedLine, name, type: AttachmentType.IMAGE });
 
                                return `[Attachment saved: ${name}]`;
                        }
@@ -1037,7 +1104,7 @@ class AgenticStore {
                        return line;
                });
 
-               return { cleanedResult: cleanedLines.join(NEWLINE), attachments };
+               return { attachments, cleanedResult: cleanedLines.join(NEWLINE) };
        }
 
        private buildAttachmentName(mimeType: string, index: number): string {
index a137be2367ec5b23bcd4d964d332445d7a7d1f98..d80de730d17159a43490dede82036501c776bbd4 100644 (file)
@@ -19,13 +19,16 @@ async function loadBuild() {
 
        if (import.meta.env.DEV) {
                build = 'dev';
+
                return;
        }
 
        try {
                const res = await fetch(`${base}/build.json`, { cache: 'no-store' });
+
                if (res.ok) {
                        const data = await res.json();
+
                        build = data.version ?? '';
                }
        } catch {
index 75c4dd84885d573b3fdf2f550a947ee412eae9ed..edbb8e621b7cca3a15f3717dcf7d8cf3673afab2 100644 (file)
  * @see ChatService in services/chat.service.ts for API operations
  */
 
-import { SvelteMap, SvelteSet } from 'svelte/reactivity';
-import { DatabaseService } from '$lib/services/database.service';
-import { ChatService } from '$lib/services/chat.service';
-import { STREAM_RESUME_RETRY_MS } from '$lib/constants/api-endpoints';
-import { streamIdentity } from '$lib/utils/stream-identity';
-import { getAuthHeaders } from '$lib/utils/api-headers';
 import { CONTENT_TYPE_HEADER } from '$lib/constants';
+import {
+       INACTIVE_CONVERSATION_STATE_MAX_AGE_MS,
+       MAX_INACTIVE_CONVERSATION_STATES,
+       SYSTEM_MESSAGE_PLACEHOLDER,
+       TITLE_GENERATION
+} from '$lib/constants';
+import { STREAM_RESUME_RETRY_MS } from '$lib/constants/api-endpoints';
 import { MimeTypeApplication } from '$lib/enums';
-import { conversationsStore } from '$lib/stores/conversations.svelte';
-import { config } from '$lib/stores/settings.svelte';
+import {
+       ContinueIntentKind,
+       ErrorDialogType,
+       MessageRole,
+       MessageType,
+       ReasoningEffort,
+       StreamConnectionState
+} from '$lib/enums';
+import { ChatService } from '$lib/services/chat.service';
+import { DatabaseService } from '$lib/services/database.service';
 import { agenticStore } from '$lib/stores/agentic.svelte';
+import { conversationsStore } from '$lib/stores/conversations.svelte';
 import { mcpStore } from '$lib/stores/mcp.svelte';
-import { contextSize, isRouterMode } from '$lib/stores/server.svelte';
 import {
-       selectedModelName,
        modelsStore,
-       selectedModelContextSize
+       selectedModelContextSize,
+       selectedModelName
 } from '$lib/stores/models.svelte';
-import {
-       normalizeModelName,
-       filterByLeafNodeId,
-       findDescendantMessages,
-       findLeafNode,
-       findMessageById,
-       formatCwdMessage,
-       isAbortError,
-       generateConversationTitle,
-       CWD_CLEARED_TEXT
-} from '$lib/utils';
+import { contextSize, isRouterMode } from '$lib/stores/server.svelte';
+import { config } from '$lib/stores/settings.svelte';
 import { toolsStore } from '$lib/stores/tools.svelte';
-import { classifyContinueIntent } from '$lib/utils/agentic';
-import {
-       MAX_INACTIVE_CONVERSATION_STATES,
-       INACTIVE_CONVERSATION_STATE_MAX_AGE_MS,
-       SYSTEM_MESSAGE_PLACEHOLDER,
-       TITLE_GENERATION
-} from '$lib/constants';
-import type {
-       ChatMessageTimings,
-       ChatMessagePromptProgress,
-       ChatStreamCallbacks,
-       ErrorDialogState
-} from '$lib/types/chat';
 import type {
        ApiChatMessageData,
        ApiProcessingState,
@@ -61,14 +48,27 @@ import type {
        DatabaseMessage,
        DatabaseMessageExtra
 } from '$lib/types';
+import type {
+       ChatMessagePromptProgress,
+       ChatMessageTimings,
+       ChatStreamCallbacks,
+       ErrorDialogState
+} from '$lib/types/chat';
 import {
-       ContinueIntentKind,
-       ErrorDialogType,
-       MessageRole,
-       MessageType,
-       ReasoningEffort,
-       StreamConnectionState
-} from '$lib/enums';
+       CWD_CLEARED_TEXT,
+       filterByLeafNodeId,
+       findDescendantMessages,
+       findLeafNode,
+       findMessageById,
+       formatCwdMessage,
+       generateConversationTitle,
+       isAbortError,
+       normalizeModelName
+} from '$lib/utils';
+import { classifyContinueIntent } from '$lib/utils/agentic';
+import { getAuthHeaders } from '$lib/utils/api-headers';
+import { streamIdentity } from '$lib/utils/stream-identity';
+import { SvelteMap, SvelteSet } from 'svelte/reactivity';
 
 interface ConversationStateEntry {
        lastAccessed: number;
@@ -128,12 +128,16 @@ class ChatStore {
 
        private setChatLoading(convId: string, loading: boolean): void {
                this.touchConversationState(convId);
+
                if (loading) {
                        this.chatLoadingStates.set(convId, true);
+
                        if (convId === conversationsStore.activeConversation?.id) this.isLoading = true;
                } else {
                        this.chatLoadingStates.delete(convId);
+
                        if (convId === conversationsStore.activeConversation?.id) this.isLoading = false;
+
                        this.setChatReasoning(convId, false);
                        // the local pipe is the authoritative observer of session end: when it finishes (clean
                        // onComplete or explicit Stop), the backend session is finalized too, so we drop the
@@ -146,9 +150,11 @@ class ChatStore {
        private setChatReasoning(convId: string, reasoning: boolean): void {
                if (reasoning) {
                        this.chatReasoningStates.set(convId, true);
+
                        if (convId === conversationsStore.activeConversation?.id) this.isReasoning = true;
                } else {
                        this.chatReasoningStates.delete(convId);
+
                        if (convId === conversationsStore.activeConversation?.id) this.isReasoning = false;
                }
        }
@@ -160,10 +166,11 @@ class ChatStore {
        ): void {
                this.touchConversationState(convId);
                this.chatStreamingStates.set(convId, {
-                       response,
                        messageId,
-                       model: model ?? this.chatStreamingStates.get(convId)?.model
+                       model: model ?? this.chatStreamingStates.get(convId)?.model,
+                       response
                });
+
                if (convId === conversationsStore.activeConversation?.id) this.currentResponse = response;
        }
        private clearChatStreaming(convId: string, messageId?: string): void {
@@ -171,9 +178,12 @@ class ChatStore {
                // same conversation, that would drop the frozen stop identity and stop the wrong session
                if (messageId !== undefined) {
                        const cur = this.chatStreamingStates.get(convId);
+
                        if (cur && cur.messageId !== messageId) return;
                }
+
                this.chatStreamingStates.delete(convId);
+
                if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
        }
        private getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
@@ -183,12 +193,15 @@ class ChatStore {
                this.isLoading = this.chatLoadingStates.get(convId) || false;
                this.isReasoning = this.chatReasoningStates.get(convId) || false;
                const s = this.chatStreamingStates.get(convId);
+
                this.currentResponse = s?.response || '';
                this.isStreamingActive = s !== undefined;
                this.setActiveProcessingConversation(convId);
+
                // Sync streaming content to activeMessages so UI displays current content
                if (s?.response && s?.messageId) {
                        const idx = conversationsStore.findMessageIndex(s.messageId);
+
                        if (idx !== -1) {
                                conversationsStore.updateMessageAtIndex(idx, { content: s.response });
                        }
@@ -214,34 +227,44 @@ class ChatStore {
         */
        async probeServerStream(convId: string): Promise<ApiStreamSession | null> {
                if (!convId) return null;
+
                let listResp: Response;
+
                try {
                        // POST the one conv id we are probing
                        listResp = await fetch(`./v1/streams/lookup`, {
-                               method: 'POST',
+                               body: JSON.stringify({ conversation_ids: [convId] }),
                                headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON },
-                               body: JSON.stringify({ conversation_ids: [convId] })
+                               method: 'POST'
                        });
                } catch (e) {
                        console.warn('probeServerStream fetch failed:', e);
+
                        return null;
                }
+
                if (!listResp.ok) {
                        console.warn(`probeServerStream got HTTP ${listResp.status} for conv ${convId}`);
+
                        return null;
                }
+
                let sessions: ApiStreamSession[];
+
                try {
                        sessions = (await listResp.json()) as ApiStreamSession[];
                } catch (e) {
                        console.warn('probeServerStream JSON parse failed:', e);
+
                        return null;
                }
+
                return ChatService.selectActiveStream(sessions);
        }
 
        async attachServerStream(convId: string, streamId?: string): Promise<void> {
                if (!convId) return;
+
                if (this.chatStreamingStates.has(convId)) return;
 
                // flip the spinner immediately, the user sees activity as soon as the conv becomes active.
@@ -250,6 +273,7 @@ class ChatStore {
                this.setChatLoading(convId, true);
                this.attachingConvs.add(convId);
                this.setStreamingActive(true);
+
                // only set the active processing conv if we are looking at it, otherwise a background
                // attach would steal the indicator from the conv the user is currently viewing
                if (convId === conversationsStore.activeConversation?.id) {
@@ -258,19 +282,22 @@ class ChatStore {
 
                const unlock = () => {
                        this.attachingConvs.delete(convId);
+
                        // flip the global flag off only when no other conv is still attaching
                        if (this.attachingConvs.size === 0) {
                                this.setStreamingActive(false);
                        }
+
                        this.setChatLoading(convId, false);
                        this.clearChatStreaming(convId);
                };
-
                // fetch the replay stream from byte 0, rebuild the assistant message from scratch.
                // resolve the server side identity, fall back to streamIdentity when the caller does not
                // pass a streamId. probeServerStream returns the full id (with ::model suffix when present)
                const id = streamId || streamIdentity(convId, selectedModelName());
+
                let response: Response;
+
                try {
                        response = await fetch(`./v1/stream?conv_id=${encodeURIComponent(id)}&from=0`, {
                                headers: getAuthHeaders()
@@ -278,11 +305,14 @@ class ChatStore {
                } catch (e) {
                        console.error('attachServerStream replay fetch failed:', e);
                        unlock();
+
                        return;
                }
+
                if (!response.ok) {
                        console.warn(`attachServerStream replay got HTTP ${response.status} for conv ${convId}`);
                        unlock();
+
                        return;
                }
 
@@ -292,42 +322,50 @@ class ChatStore {
                // DB we stay isolated, and only mirror into the active store when the attached conv is
                // the one currently displayed
                let messages: DatabaseMessage[];
+
                try {
                        messages = await DatabaseService.getConversationMessages(convId);
                } catch (e) {
                        console.error('attachServerStream load messages failed:', e);
                        unlock();
+
                        return;
                }
 
                // locate the slot to splice into, create a placeholder assistant message if there is none.
                // we use the conv-scoped findLastAssistantIdx helpers, they only depend on the array
                let targetIdx = this.findLastAssistantIdx(messages);
+
                if (targetIdx === -1) {
                        const lastUserIdx = this.findLastUserIdx(messages);
+
                        if (lastUserIdx === -1) {
                                console.warn(
                                        `attachServerStream: conv ${convId} has no user or assistant message, cannot splice`
                                );
                                unlock();
+
                                return;
                        }
+
                        try {
                                const placeholder = await DatabaseService.createMessageBranch(
                                        {
+                                               children: [],
+                                               content: '',
                                                convId,
+                                               parent: messages[lastUserIdx].id,
                                                role: MessageRole.ASSISTANT,
-                                               content: '',
-                                               type: MessageType.TEXT,
                                                timestamp: Date.now(),
-                                               parent: messages[lastUserIdx].id,
-                                               children: [],
-                                               toolCalls: ''
+                                               toolCalls: '',
+                                               type: MessageType.TEXT
                                        } as Omit<DatabaseMessage, 'id'>,
                                        messages[lastUserIdx].id
                                );
+
                                messages = [...messages, placeholder];
                                targetIdx = messages.length - 1;
+
                                // only push into the active store when this conv is the one displayed right now
                                if (convId === conversationsStore.activeConversation?.id) {
                                        conversationsStore.addMessageToActive(placeholder);
@@ -335,13 +373,17 @@ class ChatStore {
                        } catch (e) {
                                console.error('attachServerStream placeholder creation failed:', e);
                                unlock();
+
                                return;
                        }
                }
+
                if (targetIdx === -1) {
                        unlock();
+
                        return;
                }
+
                const targetMessage = messages[targetIdx];
                const targetMessageId = targetMessage.id;
                // when the assistant slot already has content, the running session is a continue or
@@ -351,7 +393,6 @@ class ChatStore {
                const existingContent = targetMessage.content ?? '';
                const existingReasoning = targetMessage.reasoningContent ?? '';
                const isAppendMode = existingContent.length > 0;
-
                // helper: write to the active store only when the attached conv is currently displayed.
                // the lookup by message id is robust to reordering of activeMessages, two parallel attaches
                // can no longer step on each other's indices
@@ -359,8 +400,11 @@ class ChatStore {
                        if (convId !== conversationsStore.activeConversation?.id) {
                                return;
                        }
+
                        const liveIdx = conversationsStore.findMessageIndex(targetMessageId);
+
                        if (liveIdx === -1) return;
+
                        conversationsStore.updateMessageAtIndex(liveIdx, updates);
                };
 
@@ -372,6 +416,7 @@ class ChatStore {
                // the session was tagged with, not the live dropdown
                const sepIdx = id.indexOf('::');
                const attachedModel: string | null = sepIdx === -1 ? null : id.slice(sepIdx + 2);
+
                this.setChatStreaming(convId, existingContent, targetMessageId, attachedModel);
                const abortController = this.getOrCreateAbortController(convId);
 
@@ -389,6 +434,7 @@ class ChatStore {
                                (chunk: string) => {
                                        streamedContent += chunk;
                                        const displayed = isAppendMode ? existingContent + streamedContent : streamedContent;
+
                                        writeActive({ content: displayed });
                                        this.setChatStreaming(convId, displayed, targetMessageId);
                                },
@@ -402,13 +448,14 @@ class ChatStore {
                                        const streamedR = streamedReasoningContent || reasoningContent || '';
                                        const content = isAppendMode ? existingContent + streamed : streamed;
                                        const reasoning = isAppendMode ? existingReasoning + streamedR : streamedR;
+
                                        // the DB write is the source of truth, mirror to the active store only when
                                        // the conv is currently displayed
                                        await DatabaseService.updateMessage(targetMessageId, {
                                                content,
                                                reasoningContent: reasoning || undefined,
-                                               toolCalls: toolCalls || '',
-                                               timings
+                                               timings,
+                                               toolCalls: toolCalls || ''
                                        });
                                        writeActive({
                                                content,
@@ -426,6 +473,7 @@ class ChatStore {
                                        const displayed = isAppendMode
                                                ? existingReasoning + streamedReasoningContent
                                                : streamedReasoningContent;
+
                                        writeActive({ reasoningContent: displayed });
                                },
                                undefined,
@@ -458,12 +506,16 @@ class ChatStore {
 
        async discoverActiveStream(convId: string): Promise<void> {
                if (!convId) return;
+
                if (this.chatStreamingStates.has(convId)) return;
+
                if (this.chatLoadingStates.get(convId) && !this.resumePendingConvs.has(convId)) return;
+
                // concurrency guard: another discover may already be running for this conv (typical race
                // between mount and visibilitychange on tab switch). a second concurrent fetch on the same
                // /v1/stream would duplicate every byte into the DB message, this guard bounces it
                if (this.discoveringConvs.has(convId)) return;
+
                this.discoveringConvs.add(convId);
 
                try {
@@ -472,13 +524,14 @@ class ChatStore {
                        // model conv with no ::suffix, only guess from the dropdown with no persisted state
                        const localState = ChatService.getStreamState(convId);
                        const streamId = ChatService.resumeStreamIdentity(convId, localState, selectedModelName());
-
                        // primary path: ask the server which sessions exist for this identity
                        const serverTarget = await this.probeServerStream(streamId);
+
                        if (serverTarget) {
                                // pass the full server side identity (may carry a ::model suffix) so the GET routes
                                // straight to the owning session, no probe or fan out
                                await this.attachServerStream(convId, serverTarget.conversation_id);
+
                                return;
                        }
 
@@ -488,14 +541,17 @@ class ChatStore {
                        if (!localState) {
                                return;
                        }
+
                        // quiet status probe first: a full attach flips the loading UI on every try, probing
                        // keeps the retry loop invisible while the owning model is still loading (503)
                        const status = await ChatService.probeResumeStatus(streamId);
+
                        if (status === 503) {
                                // make the wait visible: the empty assistant row persisted at send time renders
                                // the processing info, whose model load percentage flows from the models feed
                                this.resumePendingConvs.add(convId);
                                this.setChatLoading(convId, true);
+
                                if (!this.resumeRetryTimers.has(convId)) {
                                        this.resumeRetryTimers.set(
                                                convId,
@@ -505,22 +561,29 @@ class ChatStore {
                                                }, STREAM_RESUME_RETRY_MS)
                                        );
                                }
+
                                return;
                        }
+
                        if (this.resumePendingConvs.delete(convId) && status !== 200) {
                                // the wait is over without a session to attach, drop the visible loading state
                                this.setChatLoading(convId, false);
                        }
+
                        if (status === 0) {
                                // transient network failure, the next mount or visibility change retries
                                return;
                        }
+
                        if (status !== 200) {
                                // the session is gone (stopped, TTL expired), nothing to resume anymore
                                ChatService.clearStreamState(convId);
+
                                return;
                        }
+
                        await this.attachServerStream(convId, streamId);
+
                        // if attachServerStream failed (session gone, TTL expired), clear the local state to avoid retrying forever
                        if (!this.chatStreamingStates.has(convId) && !this.chatLoadingStates.get(convId)) {
                                ChatService.clearStreamState(convId);
@@ -534,6 +597,7 @@ class ChatStore {
                for (let i = messages.length - 1; i >= 0; i--) {
                        if (messages[i].role === MessageRole.ASSISTANT) return i;
                }
+
                return -1;
        }
 
@@ -541,6 +605,7 @@ class ChatStore {
                for (let i = messages.length - 1; i >= 0; i--) {
                        if (messages[i].role === MessageRole.USER) return i;
                }
+
                return -1;
        }
 
@@ -564,11 +629,13 @@ class ChatStore {
        private setProcessingState(conversationId: string, state: ApiProcessingState | null): void {
                if (state === null) this.processingStates.delete(conversationId);
                else this.processingStates.set(conversationId, state);
+
                if (conversationId === this.activeConversationId) this.activeProcessingState = state;
        }
 
        clearProcessingState(conversationId: string): void {
                this.processingStates.delete(conversationId);
+
                if (conversationId === this.activeConversationId) this.activeProcessingState = null;
        }
 
@@ -590,16 +657,19 @@ class ChatStore {
 
        private getOrCreateAbortController(convId: string): AbortController {
                let c = this.abortControllers.get(convId);
+
                if (!c || c.signal.aborted) {
                        c = new AbortController();
                        this.abortControllers.set(convId, c);
                }
+
                return c;
        }
 
        private abortRequest(convId?: string): void {
                if (convId) {
                        const c = this.abortControllers.get(convId);
+
                        if (c) {
                                c.abort();
                                this.abortControllers.delete(convId);
@@ -623,6 +693,7 @@ class ChatStore {
        async abortCurrentFlow(convId: string): Promise<void> {
                await this.savePartialResponseIfNeeded(convId);
                const c = this.abortControllers.get(convId);
+
                if (c) {
                        c.abort();
                        this.abortControllers.delete(convId);
@@ -666,9 +737,12 @@ class ChatStore {
 
        consumePendingDraft(): { message: string; files: ChatUploadedFile[] } | null {
                if (!this._pendingDraftMessage && this._pendingDraftFiles.length === 0) return null;
-               const d = { message: this._pendingDraftMessage, files: [...this._pendingDraftFiles] };
+
+               const d = { files: [...this._pendingDraftFiles], message: this._pendingDraftMessage };
+
                this._pendingDraftMessage = '';
                this._pendingDraftFiles = [];
+
                return d;
        }
 
@@ -680,9 +754,11 @@ class ChatStore {
                // union of local (this browser is piping) and remote (backend reports a running session
                // for this conv but no local pipe yet) sources. the sidebar shows one spinner per entry
                const out = new SvelteSet<string>(this.chatLoadingStates.keys());
+
                for (const id of this.remoteRunningConvs) {
                        out.add(id);
                }
+
                return Array.from(out);
        }
 
@@ -701,46 +777,61 @@ class ChatStore {
                // on the store init race, and the sidebar spinners light up at first paint for every conv
                // the user owns even if it has not been hydrated into the store yet
                let ids: string[];
+
                try {
                        const all = await DatabaseService.getAllConversations();
+
                        ids = all.map((c) => c.id).filter((id) => !!id);
                } catch (e) {
                        console.warn('syncRemoteRunningStreams DB read failed:', e);
+
                        return;
                }
+
                // only ask about conv ids the user already owns
                if (ids.length === 0) {
                        for (const id of Array.from(this.remoteRunningConvs)) {
                                this.remoteRunningConvs.delete(id);
                        }
+
                        return;
                }
+
                // rebuild the frozen conv::model identity per conv so a session started with a model still
                // matches. the server response is mapped back to the bare id below for the sidebar set
                const lookupIds = ids.map((id) =>
                        ChatService.resumeStreamIdentity(id, ChatService.getStreamState(id), null)
                );
+
                let sessions: ApiStreamSession[];
+
                try {
                        const resp = await fetch('./v1/streams/lookup', {
-                               method: 'POST',
+                               body: JSON.stringify({ conversation_ids: lookupIds }),
                                headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON },
-                               body: JSON.stringify({ conversation_ids: lookupIds })
+                               method: 'POST'
                        });
+
                        if (!resp.ok) return;
+
                        const body = (await resp.json()) as unknown;
+
                        if (!Array.isArray(body)) return;
+
                        sessions = body as ApiStreamSession[];
                } catch (e) {
                        console.warn('syncRemoteRunningStreams fetch failed:', e);
+
                        return;
                }
                const running = new SvelteSet<string>();
+
                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 bareId = sepIdx === -1 ? s.conversation_id : s.conversation_id.slice(0, sepIdx);
+
                                running.add(bareId);
                        }
                }
@@ -794,8 +885,11 @@ class ChatStore {
                convId: string
        ): { content: string; extras?: DatabaseMessageExtra[] } | null {
                const msg = this._pendingMessages.get(convId);
+
                if (!msg) return null;
+
                this._pendingMessages.delete(convId);
+
                return msg;
        }
 
@@ -819,15 +913,21 @@ class ChatStore {
                        ])
                ];
                const cleanupCandidates: Array<{ convId: string; lastAccessed: number }> = [];
+
                for (const convId of allConvIds) {
                        if (preserveIds.includes(convId)) continue;
+
                        if (this.chatLoadingStates.get(convId)) continue;
+
                        if (this.chatStreamingStates.has(convId)) continue;
+
                        const ts = this.conversationStateTimestamps.get(convId);
+
                        cleanupCandidates.push({ convId, lastAccessed: ts?.lastAccessed ?? 0 });
                }
                cleanupCandidates.sort((a, b) => a.lastAccessed - b.lastAccessed);
                let cleanedUp = 0;
+
                for (const { convId, lastAccessed } of cleanupCandidates) {
                        if (
                                cleanupCandidates.length - cleanedUp > MAX_INACTIVE_CONVERSATION_STATES ||
@@ -837,11 +937,14 @@ class ChatStore {
                                cleanedUp++;
                        }
                }
+
                return cleanedUp;
        }
        private cleanupConversationState(convId: string): void {
                const c = this.abortControllers.get(convId);
+
                if (c && !c.signal.aborted) c.abort();
+
                this.chatLoadingStates.delete(convId);
                this.chatStreamingStates.delete(convId);
                this.abortControllers.delete(convId);
@@ -862,10 +965,14 @@ class ChatStore {
                expectedRole?: MessageRole
        ): { message: DatabaseMessage; index: number } | null {
                const index = conversationsStore.findMessageIndex(messageId);
+
                if (index === -1) return null;
+
                const message = conversationsStore.activeMessages[index];
+
                if (expectedRole && message.role !== expectedRole) return null;
-               return { message, index };
+
+               return { index, message };
        }
 
        async addMessage(
@@ -877,34 +984,42 @@ class ChatStore {
                isSynthetic?: boolean
        ): Promise<DatabaseMessage> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) throw new Error('No active conversation');
+
                let parentId: string | null = null;
+
                if (parent === '-1') {
                        const am = conversationsStore.activeMessages;
+
                        if (am.length > 0) parentId = am[am.length - 1].id;
                        else {
                                const all = await conversationsStore.getConversationMessages(activeConv.id);
                                const r = all.find((m) => m.parent === null && m.type === 'root');
+
                                parentId = r ? r.id : await DatabaseService.createRootMessage(activeConv.id);
                        }
                } else parentId = parent;
+
                const message = await DatabaseService.createMessageBranch(
                        {
+                               children: [],
+                               content,
                                convId: activeConv.id,
+                               extra: extras,
+                               isSynthetic,
                                role,
-                               content,
-                               type,
                                timestamp: Date.now(),
                                toolCalls: '',
-                               children: [],
-                               extra: extras,
-                               isSynthetic
+                               type
                        },
                        parentId
                );
+
                conversationsStore.addMessageToActive(message);
                await conversationsStore.updateCurrentNode(message.id);
                conversationsStore.updateConversationTimestamp();
+
                return message;
        }
 
@@ -919,16 +1034,17 @@ class ChatStore {
                const content = cwd
                        ? formatCwdMessage(cwd, await toolsStore.resolveServerHome())
                        : CWD_CLEARED_TEXT;
-
                // Reuse the trailing cwd row when it is already the last message, so
                // repeated picks update it in place instead of stacking another row.
                const last = conversationsStore.activeMessages[conversationsStore.activeMessages.length - 1];
+
                if (last && last.role === MessageRole.USER && last.isSynthetic === true) {
                        await DatabaseService.updateMessage(last.id, { content, isSynthetic: true });
                        conversationsStore.updateMessageAtIndex(conversationsStore.activeMessages.length - 1, {
                                content,
                                isSynthetic: true
                        });
+
                        return;
                }
 
@@ -937,11 +1053,14 @@ class ChatStore {
 
        async addSystemPrompt(): Promise<void> {
                let activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) {
                        await conversationsStore.createConversation();
                        activeConv = conversationsStore.activeConversation;
                }
+
                if (!activeConv) return;
+
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
@@ -951,12 +1070,16 @@ class ChatStore {
                        const existingSystemMessage = allMessages.find(
                                (m) => m.role === MessageRole.SYSTEM && m.parent === rootId
                        );
+
                        if (existingSystemMessage) {
                                this.pendingEditMessageId = existingSystemMessage.id;
+
                                if (!conversationsStore.activeMessages.some((m) => m.id === existingSystemMessage.id))
                                        conversationsStore.activeMessages.unshift(existingSystemMessage);
+
                                return;
                        }
+
                        const am = conversationsStore.activeMessages;
                        const firstActiveMessage = am.find((m) => m.parent === rootId);
                        const systemMessage = await DatabaseService.createSystemMessage(
@@ -964,6 +1087,7 @@ class ChatStore {
                                SYSTEM_MESSAGE_PLACEHOLDER,
                                rootId
                        );
+
                        if (firstActiveMessage) {
                                await DatabaseService.updateMessage(firstActiveMessage.id, {
                                        parent: systemMessage.id
@@ -974,6 +1098,7 @@ class ChatStore {
                                const updatedRootChildren = rootMessage
                                        ? rootMessage.children.filter((id: string) => id !== firstActiveMessage.id)
                                        : [];
+
                                await DatabaseService.updateMessage(rootId, {
                                        children: [
                                                ...updatedRootChildren.filter((id: string) => id !== systemMessage.id),
@@ -981,11 +1106,13 @@ class ChatStore {
                                        ]
                                });
                                const firstMsgIndex = conversationsStore.findMessageIndex(firstActiveMessage.id);
+
                                if (firstMsgIndex !== -1)
                                        conversationsStore.updateMessageAtIndex(firstMsgIndex, {
                                                parent: systemMessage.id
                                        });
                        }
+
                        conversationsStore.activeMessages.unshift(systemMessage);
                        this.pendingEditMessageId = systemMessage.id;
                        conversationsStore.updateConversationTimestamp();
@@ -996,20 +1123,29 @@ class ChatStore {
 
        async removeSystemPromptPlaceholder(messageId: string): Promise<boolean> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return false;
+
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const systemMessage = findMessageById(allMessages, messageId);
+
                        if (!systemMessage || systemMessage.role !== MessageRole.SYSTEM) return false;
+
                        const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
+
                        if (!rootMessage) return false;
+
                        if (allMessages.length === 2 && systemMessage.children.length === 0) {
                                await conversationsStore.deleteConversation(activeConv.id);
+
                                return true;
                        }
+
                        for (const childId of systemMessage.children) {
                                await DatabaseService.updateMessage(childId, { parent: rootMessage.id });
                                const childIndex = conversationsStore.findMessageIndex(childId);
+
                                if (childIndex !== -1)
                                        conversationsStore.updateMessageAtIndex(childIndex, { parent: rootMessage.id });
                        }
@@ -1021,28 +1157,34 @@ class ChatStore {
                        });
                        await DatabaseService.deleteMessage(messageId);
                        const systemIndex = conversationsStore.findMessageIndex(messageId);
+
                        if (systemIndex !== -1) conversationsStore.activeMessages.splice(systemIndex, 1);
+
                        conversationsStore.updateConversationTimestamp();
+
                        return false;
                } catch (error) {
                        console.error('Failed to remove system prompt placeholder:', error);
+
                        return false;
                }
        }
 
        private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) throw new Error('No active conversation');
+
                return await DatabaseService.createMessageBranch(
                        {
+                               children: [],
+                               content: '',
                                convId: activeConv.id,
-                               type: MessageType.TEXT,
+                               model: null,
                                role: MessageRole.ASSISTANT,
-                               content: '',
                                timestamp: Date.now(),
                                toolCalls: '',
-                               children: [],
-                               model: null
+                               type: MessageType.TEXT
                        },
                        parentId || null
                );
@@ -1050,17 +1192,20 @@ class ChatStore {
 
        async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> {
                if (!content.trim() && (!extras || extras.length === 0)) return;
+
                const activeConv = conversationsStore.activeConversation;
 
                // If agentic loop is running, inject as a steering message instead of starting a new flow
                if (activeConv && agenticStore.isRunning(activeConv.id)) {
                        agenticStore.injectSteeringMessage(activeConv.id, content, extras);
+
                        return;
                }
 
                // If non-agentic streaming is active, queue as a pending message to send after completion
                if (activeConv && this.isChatLoadingInternal(activeConv.id)) {
                        this.injectPendingMessage(activeConv.id, content, extras);
+
                        return;
                }
 
@@ -1072,31 +1217,40 @@ class ChatStore {
                const allExtras = resourceExtras.length > 0 ? [...(extras || []), ...resourceExtras] : extras;
 
                let isNewConversation = false;
+
                if (!activeConv) {
                        await conversationsStore.createConversation();
                        isNewConversation = true;
                }
+
                const currentConv = conversationsStore.activeConversation;
+
                if (!currentConv) return;
+
                this.showErrorDialog(null);
                this.setChatLoading(currentConv.id, true);
                this.clearChatStreaming(currentConv.id);
                try {
                        let parentIdForUserMessage: string | undefined;
+
                        if (isNewConversation) {
                                const rootId = await DatabaseService.createRootMessage(currentConv.id);
                                const currentConfig = config();
                                const systemPrompt = currentConfig.systemMessage?.toString().trim();
+
                                let sysOrRootId = rootId;
+
                                if (systemPrompt) {
                                        const systemMessage = await DatabaseService.createSystemMessage(
                                                currentConv.id,
                                                systemPrompt,
                                                rootId
                                        );
+
                                        conversationsStore.addMessageToActive(systemMessage);
                                        sysOrRootId = systemMessage.id;
                                }
+
                                // Reflect a working directory picked on the new-chat screen into
                                // chat history before the first user message, so the model sees
                                // it on its first turn. createConversation() has already threaded
@@ -1110,11 +1264,13 @@ class ChatStore {
                                                undefined,
                                                true
                                        );
+
                                        parentIdForUserMessage = cwdMessage.id;
                                } else {
                                        parentIdForUserMessage = sysOrRootId;
                                }
                        }
+
                        const userMessage = await this.addMessage(
                                MessageRole.USER,
                                content,
@@ -1122,12 +1278,15 @@ class ChatStore {
                                parentIdForUserMessage ?? '-1',
                                allExtras
                        );
+
                        if (isNewConversation && content)
                                await conversationsStore.updateConversationName(
                                        currentConv.id,
                                        generateConversationTitle(content, Boolean(config().titleGenerationUseFirstLine))
                                );
+
                        const assistantMessage = await this.createAssistantMessage(userMessage.id);
+
                        conversationsStore.addMessageToActive(assistantMessage);
                        await this.streamChatCompletion(
                                conversationsStore.activeMessages.slice(0, -1),
@@ -1140,8 +1299,10 @@ class ChatStore {
                } catch (error) {
                        if (isAbortError(error)) {
                                this.setChatLoading(currentConv.id, false);
+
                                return;
                        }
+
                        console.error('Failed to send message:', error);
                        this.setChatLoading(currentConv.id, false);
                        const dialogType =
@@ -1151,10 +1312,11 @@ class ChatStore {
                        const contextInfo = (
                                error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } }
                        ).contextInfo;
+
                        this.showErrorDialog({
-                               type: dialogType,
+                               contextInfo,
                                message: error instanceof Error ? error.message : 'Unknown error',
-                               contextInfo
+                               type: dialogType
                        });
                }
        }
@@ -1174,6 +1336,7 @@ class ChatStore {
 
                if (isRouterMode()) {
                        const conversationModel = this.getConversationModel(allMessages);
+
                        effectiveModel = modelOverride || selectedModelName() || conversationModel;
                }
 
@@ -1188,23 +1351,31 @@ class ChatStore {
                let streamedReasoningContent = '';
                let resolvedModel: string | null = null;
                let modelPersisted = false;
+
                const convId = assistantMessage.convId;
+
                // Tracks the last message created in this flow. Used as the parent for the next
                // turn's assistant message so createAssistantMessage does not have to read
                // conversationsStore.activeMessages, which may belong to a different conversation
                // after the user navigates while the loop is still running.
                let lastCreatedInFlow = currentMessageId;
+
                // freeze the POST identity from t0 so a stop cancels with the exact session key,
                // never a stale or empty model resolved later
                this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
 
                const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
                        if (!modelName) return;
+
                        const n = normalizeModelName(modelName);
+
                        if (!n || n === resolvedModel) return;
+
                        resolvedModel = n;
                        const idx = conversationsStore.findMessageIndex(currentMessageId);
+
                        conversationsStore.updateMessageAtIndex(idx, { model: n });
+
                        if (persistImmediately && !modelPersisted) {
                                modelPersisted = true;
                                DatabaseService.updateMessage(currentMessageId, { model: n }).catch(() => {
@@ -1215,22 +1386,24 @@ class ChatStore {
                };
 
                let completionIdRecorded = false;
+
                const recordCompletionId = (id: string): void => {
                        if (!id || completionIdRecorded) return;
+
                        completionIdRecorded = true;
                        const idx = conversationsStore.findMessageIndex(currentMessageId);
+
                        conversationsStore.updateMessageAtIndex(idx, { completionId: id });
                        DatabaseService.updateMessage(currentMessageId, { completionId: id }).catch(() => {
                                completionIdRecorded = false;
                        });
                };
-
                const updateStreamingUI = () => {
                        this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
                        const idx = conversationsStore.findMessageIndex(currentMessageId);
+
                        conversationsStore.updateMessageAtIndex(idx, { content: streamedContent });
                };
-
                const cleanupStreamingState = () => {
                        this.setStreamingActive(false);
                        this.setChatLoading(convId, false);
@@ -1241,93 +1414,34 @@ class ChatStore {
                this.setStreamingActive(true);
                this.setActiveProcessingConversation(convId);
                const abortController = this.getOrCreateAbortController(convId);
-
                const streamCallbacks: ChatStreamCallbacks = {
-                       onChunk: (chunk: string) => {
-                               streamedContent += chunk;
-                               updateStreamingUI();
-                               this.setChatReasoning(convId, false);
-                       },
-                       onReasoningChunk: (chunk: string) => {
-                               streamedReasoningContent += chunk;
-                               // mark streaming state so a stop mid-thinking can persist the partial reasoning
-                               this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
-                               const idx = conversationsStore.findMessageIndex(currentMessageId);
-                               conversationsStore.updateMessageAtIndex(idx, {
-                                       reasoningContent: streamedReasoningContent
-                               });
-                               this.setChatReasoning(convId, true);
-                       },
-                       onToolCallsStreaming: (toolCalls) => {
-                               const idx = conversationsStore.findMessageIndex(currentMessageId);
-                               conversationsStore.updateMessageAtIndex(idx, {
-                                       toolCalls: JSON.stringify(toolCalls)
-                               });
-                       },
-                       onAttachments: (messageId: string, extras: DatabaseMessageExtra[]) => {
-                               if (!extras.length) return;
-                               const idx = conversationsStore.findMessageIndex(messageId);
-                               if (idx === -1) return;
-                               const msg = conversationsStore.activeMessages[idx];
-                               const updatedExtras = [...(msg.extra || []), ...extras];
-                               conversationsStore.updateMessageAtIndex(idx, { extra: updatedExtras });
-                               DatabaseService.updateMessage(messageId, { extra: updatedExtras }).catch(console.error);
-                       },
-                       onModel: (modelName: string) => recordModel(modelName),
-                       onCompletionId: (id: string) => recordCompletionId(id),
-                       onTurnComplete: (intermediateTimings: ChatMessageTimings) => {
-                               // Update the first assistant message with cumulative agentic timings
-                               const idx = conversationsStore.findMessageIndex(assistantMessage.id);
-                               conversationsStore.updateMessageAtIndex(idx, { timings: intermediateTimings });
-                       },
-                       onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => {
-                               const tokensPerSecond =
-                                       timings?.predicted_ms && timings?.predicted_n
-                                               ? (timings.predicted_n / timings.predicted_ms) * 1000
-                                               : 0;
-                               this.updateProcessingStateFromTimings(
+                       createAssistantMessage: async () => {
+                               // Reset streaming state for new message
+                               streamedContent = '';
+                               streamedReasoningContent = '';
+
+                               const msg = await DatabaseService.createMessageBranch(
                                        {
-                                               prompt_n: timings?.prompt_n || 0,
-                                               prompt_ms: timings?.prompt_ms,
-                                               predicted_n: timings?.predicted_n || 0,
-                                               predicted_per_second: tokensPerSecond,
-                                               cache_n: timings?.cache_n || 0,
-                                               prompt_progress: promptProgress
+                                               children: [],
+                                               content: '',
+                                               convId,
+                                               model: resolvedModel,
+                                               role: MessageRole.ASSISTANT,
+                                               timestamp: Date.now(),
+                                               toolCalls: '',
+                                               type: MessageType.TEXT
                                        },
-                                       convId
+                                       lastCreatedInFlow
                                );
-                       },
-                       onAssistantTurnComplete: async (
-                               content: string,
-                               reasoningContent: string | undefined,
-                               timings: ChatMessageTimings | undefined,
-                               toolCalls: import('$lib/types/api').ApiChatCompletionToolCall[] | undefined
-                       ) => {
-                               const updateData: Record<string, unknown> = {
-                                       content,
-                                       reasoningContent: reasoningContent || undefined,
-                                       toolCalls: toolCalls ? JSON.stringify(toolCalls) : '',
-                                       timings
-                               };
-                               if (resolvedModel && !modelPersisted) updateData.model = resolvedModel;
-                               await DatabaseService.updateMessage(currentMessageId, updateData);
-                               const idx = conversationsStore.findMessageIndex(currentMessageId);
-                               const uiUpdate: Partial<DatabaseMessage> = {
-                                       content,
-                                       reasoningContent: reasoningContent || undefined,
-                                       toolCalls: toolCalls ? JSON.stringify(toolCalls) : ''
-                               };
-                               if (timings) uiUpdate.timings = timings;
-                               if (resolvedModel) uiUpdate.model = resolvedModel;
-                               // touch the active ui array and node pointer only when this conversation
-                               // is displayed; otherwise persist the node move straight to the db so a
-                               // foreign conv's currNode stays untouched
+
                                if (conversationsStore.activeConversation?.id === convId) {
-                                       conversationsStore.updateMessageAtIndex(idx, uiUpdate);
-                                       await conversationsStore.updateCurrentNode(currentMessageId);
-                               } else {
-                                       await DatabaseService.updateCurrentNode(convId, currentMessageId);
+                                       conversationsStore.addMessageToActive(msg);
                                }
+
+                               currentMessageId = msg.id;
+                               lastCreatedInFlow = msg.id;
+
+                               return msg;
                        },
                        createToolResultMessage: async (
                                toolCallId: string,
@@ -1337,19 +1451,20 @@ class ChatStore {
                        ) => {
                                const msg = await DatabaseService.createMessageBranch(
                                        {
+                                               children: [],
+                                               content,
                                                convId,
-                                               type: MessageType.TEXT,
+                                               extra: extras,
                                                role: MessageRole.TOOL,
-                                               content,
-                                               toolCallId,
-                                               toolCwd,
                                                timestamp: Date.now(),
+                                               toolCallId,
                                                toolCalls: '',
-                                               children: [],
-                                               extra: extras
+                                               toolCwd,
+                                               type: MessageType.TEXT
                                        },
                                        currentMessageId
                                );
+
                                // mirror into the active store and move the node pointer only when this
                                // conversation is displayed; otherwise persist the node move straight to
                                // the db for the owning conv so a foreign conv's currNode stays untouched
@@ -1359,55 +1474,99 @@ class ChatStore {
                                } else {
                                        await DatabaseService.updateCurrentNode(convId, msg.id);
                                }
+
                                lastCreatedInFlow = msg.id;
+
                                return msg;
                        },
-                       updateToolResultMessage: async (
-                               messageId: string,
+                       onAssistantTurnComplete: async (
                                content: string,
-                               extras?: DatabaseMessageExtra[]
+                               reasoningContent: string | undefined,
+                               timings: ChatMessageTimings | undefined,
+                               toolCalls: import('$lib/types/api').ApiChatCompletionToolCall[] | undefined
                        ) => {
-                               // Persist latest content + merged extras; mirror into the active
-                               // store so the chat view sees live updates for streaming tools
-                               // (e.g. exec_shell_command). The existing tool message node
-                               // pointer stays put - the renderer is already scoped to it.
-                               const updates: Partial<DatabaseMessage> = { content };
-                               if (extras) {
-                                       const idx = conversationsStore.findMessageIndex(messageId);
-                                       const existing = idx >= 0 ? (conversationsStore.activeMessages[idx]?.extra ?? []) : [];
-                                       const merged = [...existing, ...extras];
-                                       updates.extra = merged;
-                               }
+                               const updateData: Record<string, unknown> = {
+                                       content,
+                                       reasoningContent: reasoningContent || undefined,
+                                       timings,
+                                       toolCalls: toolCalls ? JSON.stringify(toolCalls) : ''
+                               };
+
+                               if (resolvedModel && !modelPersisted) updateData.model = resolvedModel;
+
+                               await DatabaseService.updateMessage(currentMessageId, updateData);
+                               const idx = conversationsStore.findMessageIndex(currentMessageId);
+                               const uiUpdate: Partial<DatabaseMessage> = {
+                                       content,
+                                       reasoningContent: reasoningContent || undefined,
+                                       toolCalls: toolCalls ? JSON.stringify(toolCalls) : ''
+                               };
+
+                               if (timings) uiUpdate.timings = timings;
+
+                               if (resolvedModel) uiUpdate.model = resolvedModel;
+
+                               // touch the active ui array and node pointer only when this conversation
+                               // is displayed; otherwise persist the node move straight to the db so a
+                               // foreign conv's currNode stays untouched
                                if (conversationsStore.activeConversation?.id === convId) {
-                                       const idx = conversationsStore.findMessageIndex(messageId);
-                                       if (idx >= 0) conversationsStore.updateMessageAtIndex(idx, updates);
+                                       conversationsStore.updateMessageAtIndex(idx, uiUpdate);
+                                       await conversationsStore.updateCurrentNode(currentMessageId);
+                               } else {
+                                       await DatabaseService.updateCurrentNode(convId, currentMessageId);
                                }
-                               await DatabaseService.updateMessage(messageId, updates);
                        },
-                       createAssistantMessage: async () => {
-                               // Reset streaming state for new message
-                               streamedContent = '';
-                               streamedReasoningContent = '';
+                       onAttachments: (messageId: string, extras: DatabaseMessageExtra[]) => {
+                               if (!extras.length) return;
 
-                               const msg = await DatabaseService.createMessageBranch(
-                                       {
-                                               convId,
-                                               type: MessageType.TEXT,
-                                               role: MessageRole.ASSISTANT,
-                                               content: '',
-                                               timestamp: Date.now(),
-                                               toolCalls: '',
-                                               children: [],
-                                               model: resolvedModel
-                                       },
-                                       lastCreatedInFlow
-                               );
-                               if (conversationsStore.activeConversation?.id === convId) {
-                                       conversationsStore.addMessageToActive(msg);
+                               const idx = conversationsStore.findMessageIndex(messageId);
+
+                               if (idx === -1) return;
+
+                               const msg = conversationsStore.activeMessages[idx];
+                               const updatedExtras = [...(msg.extra || []), ...extras];
+
+                               conversationsStore.updateMessageAtIndex(idx, { extra: updatedExtras });
+                               DatabaseService.updateMessage(messageId, { extra: updatedExtras }).catch(console.error);
+                       },
+                       onChunk: (chunk: string) => {
+                               streamedContent += chunk;
+                               updateStreamingUI();
+                               this.setChatReasoning(convId, false);
+                       },
+                       onCompletionId: (id: string) => recordCompletionId(id),
+                       onError: async (error: Error) => {
+                               this.setStreamingActive(false);
+
+                               if (isAbortError(error)) {
+                                       cleanupStreamingState();
+                                       // If aborted with a pending message (e.g. "Send immediately"), re-send it
+                                       const pending = this.consumePendingMessage(convId);
+
+                                       if (pending) {
+                                               this.sendMessage(pending.content, pending.extras);
+                                       }
+
+                                       return;
                                }
-                               currentMessageId = msg.id;
-                               lastCreatedInFlow = msg.id;
-                               return msg;
+
+                               console.error('Streaming error:', error);
+                               // keep whatever was streamed so far, the message stays in memory and in DB
+                               await this.savePartialResponseIfNeeded(convId);
+                               cleanupStreamingState();
+                               this.clearPendingMessage(convId);
+
+                               const contextInfo = (
+                                       error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } }
+                               ).contextInfo;
+
+                               this.showErrorDialog({
+                                       contextInfo,
+                                       message: error.message,
+                                       type: error.name === 'TimeoutError' ? ErrorDialogType.TIMEOUT : ErrorDialogType.SERVER
+                               });
+
+                               if (onError) onError(error);
                        },
                        onFlowComplete: (finalTimings?: ChatMessageTimings) => {
                                if (finalTimings) {
@@ -1422,7 +1581,9 @@ class ChatStore {
                                cleanupStreamingState();
 
                                if (onComplete) onComplete(streamedContent);
+
                                if (isRouterMode()) modelsStore.fetchRouterModels().catch(console.error);
+
                                // Pre-encode conversation in KV cache for faster next turn
                                if (config().preEncodeConversation) {
                                        this.triggerPreEncode(
@@ -1434,59 +1595,105 @@ class ChatStore {
                                        );
                                }
                        },
-                       onError: async (error: Error) => {
-                               this.setStreamingActive(false);
-                               if (isAbortError(error)) {
-                                       cleanupStreamingState();
-                                       // If aborted with a pending message (e.g. "Send immediately"), re-send it
-                                       const pending = this.consumePendingMessage(convId);
-                                       if (pending) {
-                                               this.sendMessage(pending.content, pending.extras);
-                                       }
-                                       return;
-                               }
-                               console.error('Streaming error:', error);
-                               // keep whatever was streamed so far, the message stays in memory and in DB
-                               await this.savePartialResponseIfNeeded(convId);
-                               cleanupStreamingState();
-                               this.clearPendingMessage(convId);
+                       onModel: (modelName: string) => recordModel(modelName),
+                       onReasoningChunk: (chunk: string) => {
+                               streamedReasoningContent += chunk;
+                               // mark streaming state so a stop mid-thinking can persist the partial reasoning
+                               this.setChatStreaming(convId, streamedContent, currentMessageId, effectiveModel);
+                               const idx = conversationsStore.findMessageIndex(currentMessageId);
 
-                               const contextInfo = (
-                                       error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } }
-                               ).contextInfo;
-                               this.showErrorDialog({
-                                       type: error.name === 'TimeoutError' ? ErrorDialogType.TIMEOUT : ErrorDialogType.SERVER,
-                                       message: error.message,
-                                       contextInfo
+                               conversationsStore.updateMessageAtIndex(idx, {
+                                       reasoningContent: streamedReasoningContent
                                });
-                               if (onError) onError(error);
+                               this.setChatReasoning(convId, true);
+                       },
+                       onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => {
+                               const tokensPerSecond =
+                                       timings?.predicted_ms && timings?.predicted_n
+                                               ? (timings.predicted_n / timings.predicted_ms) * 1000
+                                               : 0;
+
+                               this.updateProcessingStateFromTimings(
+                                       {
+                                               cache_n: timings?.cache_n || 0,
+                                               predicted_n: timings?.predicted_n || 0,
+                                               predicted_per_second: tokensPerSecond,
+                                               prompt_ms: timings?.prompt_ms,
+                                               prompt_n: timings?.prompt_n || 0,
+                                               prompt_progress: promptProgress
+                                       },
+                                       convId
+                               );
+                       },
+                       onToolCallsStreaming: (toolCalls) => {
+                               const idx = conversationsStore.findMessageIndex(currentMessageId);
+
+                               conversationsStore.updateMessageAtIndex(idx, {
+                                       toolCalls: JSON.stringify(toolCalls)
+                               });
+                       },
+                       onTurnComplete: (intermediateTimings: ChatMessageTimings) => {
+                               // Update the first assistant message with cumulative agentic timings
+                               const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+
+                               conversationsStore.updateMessageAtIndex(idx, { timings: intermediateTimings });
+                       },
+                       updateToolResultMessage: async (
+                               messageId: string,
+                               content: string,
+                               extras?: DatabaseMessageExtra[]
+                       ) => {
+                               // Persist latest content + merged extras; mirror into the active
+                               // store so the chat view sees live updates for streaming tools
+                               // (e.g. exec_shell_command). The existing tool message node
+                               // pointer stays put - the renderer is already scoped to it.
+                               const updates: Partial<DatabaseMessage> = { content };
+
+                               if (extras) {
+                                       const idx = conversationsStore.findMessageIndex(messageId);
+                                       const existing = idx >= 0 ? (conversationsStore.activeMessages[idx]?.extra ?? []) : [];
+                                       const merged = [...existing, ...extras];
+
+                                       updates.extra = merged;
+                               }
+
+                               if (conversationsStore.activeConversation?.id === convId) {
+                                       const idx = conversationsStore.findMessageIndex(messageId);
+
+                                       if (idx >= 0) conversationsStore.updateMessageAtIndex(idx, updates);
+                               }
+
+                               await DatabaseService.updateMessage(messageId, updates);
                        }
                };
-
                const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
 
                {
                        const agenticResult = await agenticStore.runAgenticFlow({
+                               callbacks: streamCallbacks,
                                conversationId: convId,
                                messages: allMessages,
                                options: {
                                        ...this.getApiOptions(),
                                        ...(effectiveModel ? { model: effectiveModel } : {})
                                },
-                               callbacks: streamCallbacks,
-                               signal: abortController.signal,
-                               perChatOverrides
+                               perChatOverrides,
+                               signal: abortController.signal
                        });
+
                        if (agenticResult.handled) {
                                // Generate LLM based title for new conversations after agentic flow completes
                                if (firstUserMessageContent) {
                                        await this.generateTitleWithLLM(firstUserMessageContent, streamedContent, convId);
                                }
+
                                // Check if there's a pending steering message to re-send
                                const pending = agenticStore.consumePendingSteeringMessage(convId);
+
                                if (pending) {
                                        await this.sendMessage(pending.content, pending.extras);
                                }
+
                                return;
                        }
                }
@@ -1496,17 +1703,7 @@ class ChatStore {
                        {
                                ...this.getApiOptions(),
                                ...(effectiveModel ? { model: effectiveModel } : {}),
-                               stream: true,
                                onChunk: streamCallbacks.onChunk,
-                               onReasoningChunk: streamCallbacks.onReasoningChunk,
-                               onModel: streamCallbacks.onModel,
-                               onCompletionId: streamCallbacks.onCompletionId,
-                               onTimings: streamCallbacks.onTimings,
-                               onConnectionState: (state: StreamConnectionState) => {
-                                       if (convId === conversationsStore.activeConversation?.id) {
-                                               this.streamConnectionState = state;
-                                       }
-                               },
                                onComplete: async (
                                        finalContent?: string,
                                        reasoningContent?: string,
@@ -1518,10 +1715,12 @@ class ChatStore {
                                        const updateData: Record<string, unknown> = {
                                                content,
                                                reasoningContent: reasoning || undefined,
-                                               toolCalls: toolCalls || '',
-                                               timings
+                                               timings,
+                                               toolCalls: toolCalls || ''
                                        };
+
                                        if (resolvedModel && !modelPersisted) updateData.model = resolvedModel;
+
                                        await DatabaseService.updateMessage(currentMessageId, updateData);
                                        const idx = conversationsStore.findMessageIndex(currentMessageId);
                                        const uiUpdate: Partial<DatabaseMessage> = {
@@ -1529,12 +1728,17 @@ class ChatStore {
                                                reasoningContent: reasoning || undefined,
                                                toolCalls: toolCalls || ''
                                        };
+
                                        if (timings) uiUpdate.timings = timings;
+
                                        if (resolvedModel) uiUpdate.model = resolvedModel;
+
                                        conversationsStore.updateMessageAtIndex(idx, uiUpdate);
                                        await conversationsStore.updateCurrentNode(currentMessageId);
                                        cleanupStreamingState();
+
                                        if (onComplete) await onComplete(content);
+
                                        if (isRouterMode()) modelsStore.fetchRouterModels().catch(console.error);
 
                                        // Generate LLM based title for new conversations (avoids stale reference
@@ -1545,11 +1749,22 @@ class ChatStore {
 
                                        // Check if there's a pending message queued during streaming
                                        const pending = this.consumePendingMessage(convId);
+
                                        if (pending) {
                                                await this.sendMessage(pending.content, pending.extras);
                                        }
                                },
-                               onError: streamCallbacks.onError
+                               onCompletionId: streamCallbacks.onCompletionId,
+                               onConnectionState: (state: StreamConnectionState) => {
+                                       if (convId === conversationsStore.activeConversation?.id) {
+                                               this.streamConnectionState = state;
+                                       }
+                               },
+                               onError: streamCallbacks.onError,
+                               onModel: streamCallbacks.onModel,
+                               onReasoningChunk: streamCallbacks.onReasoningChunk,
+                               onTimings: streamCallbacks.onTimings,
+                               stream: true
                        },
                        convId,
                        abortController.signal
@@ -1558,7 +1773,9 @@ class ChatStore {
 
        async stopGeneration(): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
+
                await this.stopGenerationForChat(activeConv.id);
        }
        async stopGenerationForChat(convId: string): Promise<void> {
@@ -1569,14 +1786,17 @@ class ChatStore {
                // captured when the session started, not the live dropdown
                const streamStateForStop = this.chatStreamingStates.get(convId);
                const modelForStop = streamStateForStop?.model ?? ChatService.getStreamState(convId)?.model;
+
                void ChatService.cancelServerStream(convId, modelForStop);
                // an explicit stop leaves nothing to resume and kills a pending resume retry
                ChatService.clearStreamState(convId);
                const retryTimer = this.resumeRetryTimers.get(convId);
+
                if (retryTimer !== undefined) {
                        clearTimeout(retryTimer);
                        this.resumeRetryTimers.delete(convId);
                }
+
                this.resumePendingConvs.delete(convId);
                this.abortRequest(convId);
                this.setChatLoading(convId, false);
@@ -1597,16 +1817,13 @@ class ChatStore {
                        configValue.titleGenerationPrompt.trim()
                                ? configValue.titleGenerationPrompt
                                : TITLE_GENERATION.DEFAULT_PROMPT;
-
                const titlePrompt = titlePromptTemplate
                        .replace('{{USER}}', String(userContent || ''))
                        .replace('{{ASSISTANT}}', String(assistantContent || ''));
-
                const titleMessage: ApiChatMessageData = {
-                       role: MessageRole.USER,
-                       content: titlePrompt
+                       content: titlePrompt,
+                       role: MessageRole.USER
                };
-
                const titleResponse = await ChatService.generateTitle(titleMessage, effectiveModel);
 
                if (!titleResponse) {
@@ -1614,14 +1831,18 @@ class ChatStore {
                }
 
                let cleanTitle = titleResponse.trim();
+
                cleanTitle = cleanTitle
                        .replace(TITLE_GENERATION.PREFIX_PATTERN, '')
                        .replace(TITLE_GENERATION.QUOTE_PATTERN, '')
                        .trim();
+
                if (!cleanTitle || cleanTitle.length < TITLE_GENERATION.MIN_LENGTH) {
                        const firstLine = userContent.split('\n').find((l) => l.trim().length > 0);
+
                        cleanTitle = firstLine ? firstLine.trim() : TITLE_GENERATION.FALLBACK;
                }
+
                if (cleanTitle && cleanTitle.length >= TITLE_GENERATION.MIN_LENGTH) {
                        await conversationsStore.updateConversationName(convId, cleanTitle);
                }
@@ -1629,15 +1850,22 @@ class ChatStore {
 
        private async savePartialResponseIfNeeded(convId?: string): Promise<void> {
                const conversationId = convId || conversationsStore.activeConversation?.id;
+
                if (!conversationId) return;
+
                const streamingState = this.getChatStreaming(conversationId);
+
                if (!streamingState) return;
+
                const messages =
                        conversationId === conversationsStore.activeConversation?.id
                                ? conversationsStore.activeMessages
                                : await conversationsStore.getConversationMessages(conversationId);
+
                if (!messages.length) return;
+
                const lastMessage = messages[messages.length - 1];
+
                if (lastMessage?.role !== MessageRole.ASSISTANT) return;
 
                const partialContent = streamingState.response;
@@ -1661,27 +1889,33 @@ class ChatStore {
                        } = {
                                toolCalls: ''
                        };
+
                        if (partialContent.trim()) updateData.content = partialContent;
+
                        if (partialReasoning.trim()) updateData.reasoningContent = partialReasoning;
+
                        const lastKnownState = this.getProcessingState(conversationId);
+
                        if (lastKnownState) {
                                updateData.timings = {
-                                       prompt_n: lastKnownState.promptTokens || 0,
-                                       prompt_ms: lastKnownState.promptMs,
-                                       predicted_n: lastKnownState.tokensDecoded || 0,
                                        cache_n: lastKnownState.cacheTokens || 0,
                                        predicted_ms:
                                                lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
                                                        ? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
-                                                       : undefined
+                                                       : undefined,
+                                       predicted_n: lastKnownState.tokensDecoded || 0,
+                                       prompt_ms: lastKnownState.promptMs,
+                                       prompt_n: lastKnownState.promptTokens || 0
                                };
                        }
+
                        await DatabaseService.updateMessage(lastMessage.id, updateData);
                        lastMessage.content = partialContent;
                        // mirror the drop into the in-memory message so the next request sent via
                        // sendMessage (queued pending, Send immediately, or manual follow-up) reads
                        // the cleared value, not whatever the streaming widget had been showing
                        lastMessage.toolCalls = '';
+
                        if (updateData.timings) lastMessage.timings = updateData.timings;
                } catch (error) {
                        lastMessage.content = partialContent;
@@ -1692,31 +1926,43 @@ class ChatStore {
 
        async updateMessage(messageId: string, newContent: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
+
                if (this.isChatLoadingInternal(activeConv.id)) await this.stopGeneration();
+
                const result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
+
                if (!result) return;
-               const { message: messageToUpdate, index: messageIndex } = result;
+
+               const { index: messageIndex, message: messageToUpdate } = result;
                const originalContent = messageToUpdate.content;
+
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
                        const isFirstUserMessage = rootMessage && messageToUpdate.parent === rootMessage.id;
+
                        conversationsStore.updateMessageAtIndex(messageIndex, { content: newContent });
                        await DatabaseService.updateMessage(messageId, { content: newContent });
+
                        if (isFirstUserMessage && newContent.trim())
                                await conversationsStore.updateConversationName(
                                        activeConv.id,
                                        generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
                                );
+
                        const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex + 1);
+
                        if (messagesToRemove.length > 0)
                                await DatabaseService.deleteMessageCascading(activeConv.id, messagesToRemove[0].id);
+
                        conversationsStore.sliceActiveMessages(messageIndex + 1);
                        conversationsStore.updateConversationTimestamp();
                        this.setChatLoading(activeConv.id, true);
                        this.clearChatStreaming(activeConv.id);
                        const assistantMessage = await this.createAssistantMessage();
+
                        conversationsStore.addMessageToActive(assistantMessage);
                        await conversationsStore.updateCurrentNode(assistantMessage.id);
                        await this.streamChatCompletion(
@@ -1736,13 +1982,19 @@ class ChatStore {
 
        async regenerateMessage(messageId: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
+
                this.cancelPreEncode();
                const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
+
                if (!result) return;
+
                const { index: messageIndex } = result;
+
                try {
                        const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex);
+
                        await DatabaseService.deleteMessageCascading(activeConv.id, messagesToRemove[0].id);
                        conversationsStore.sliceActiveMessages(messageIndex);
                        conversationsStore.updateConversationTimestamp();
@@ -1753,6 +2005,7 @@ class ChatStore {
                                        ? conversationsStore.activeMessages[conversationsStore.activeMessages.length - 1].id
                                        : undefined;
                        const assistantMessage = await this.createAssistantMessage(parentMessageId);
+
                        conversationsStore.addMessageToActive(assistantMessage);
                        await this.streamChatCompletion(
                                conversationsStore.activeMessages.slice(0, -1),
@@ -1760,37 +2013,47 @@ class ChatStore {
                        );
                } catch (error) {
                        if (!isAbortError(error)) console.error('Failed to regenerate message:', error);
+
                        this.setChatLoading(activeConv?.id || '', false);
                }
        }
 
        async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
+
                this.cancelPreEncode();
                try {
                        const idx = conversationsStore.findMessageIndex(messageId);
+
                        if (idx === -1) return;
+
                        const msg = conversationsStore.activeMessages[idx];
+
                        if (msg.role !== MessageRole.ASSISTANT) return;
+
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const parentMessage = findMessageById(allMessages, msg.parent);
+
                        if (!parentMessage) return;
+
                        this.setChatLoading(activeConv.id, true);
                        this.clearChatStreaming(activeConv.id);
                        const newAssistantMessage = await DatabaseService.createMessageBranch(
                                {
+                                       children: [],
+                                       content: '',
                                        convId: msg.convId,
-                                       type: msg.type,
-                                       timestamp: Date.now(),
+                                       model: null,
                                        role: msg.role,
-                                       content: '',
+                                       timestamp: Date.now(),
                                        toolCalls: '',
-                                       children: [],
-                                       model: null
+                                       type: msg.type
                                },
                                parentMessage.id
                        );
+
                        await conversationsStore.updateCurrentNode(newAssistantMessage.id);
                        conversationsStore.updateConversationTimestamp();
                        await conversationsStore.refreshActiveMessages();
@@ -1800,6 +2063,7 @@ class ChatStore {
                                false
                        ) as DatabaseMessage[];
                        const modelToUse = modelOverride || msg.model || undefined;
+
                        await this.streamChatCompletion(
                                conversationPath,
                                newAssistantMessage,
@@ -1810,6 +2074,7 @@ class ChatStore {
                } catch (error) {
                        if (!isAbortError(error))
                                console.error('Failed to regenerate message with branching:', error);
+
                        this.setChatLoading(activeConv?.id || '', false);
                }
        }
@@ -1821,54 +2086,66 @@ class ChatStore {
                messageTypes: string[];
        }> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv)
-                       return { totalCount: 0, userMessages: 0, assistantMessages: 0, messageTypes: [] };
+                       return { assistantMessages: 0, messageTypes: [], totalCount: 0, userMessages: 0 };
+
                const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                const messageToDelete = findMessageById(allMessages, messageId);
 
                // For system messages, don't count descendants as they will be preserved (reparented to root)
                if (messageToDelete?.role === MessageRole.SYSTEM) {
                        const messagesToDelete = allMessages.filter((m) => m.id === messageId);
-                       let userMessages = 0,
-                               assistantMessages = 0;
+
+                       let assistantMessages = 0,
+                               userMessages = 0;
+
                        const messageTypes: string[] = [];
 
                        for (const msg of messagesToDelete) {
                                if (msg.role === MessageRole.USER) {
                                        userMessages++;
+
                                        if (!messageTypes.includes('user message')) messageTypes.push('user message');
                                } else if (msg.role === MessageRole.ASSISTANT) {
                                        assistantMessages++;
+
                                        if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response');
                                }
                        }
 
-                       return { totalCount: 1, userMessages, assistantMessages, messageTypes };
+                       return { assistantMessages, messageTypes, totalCount: 1, userMessages };
                }
 
                const descendants = findDescendantMessages(allMessages, messageId);
                const allToDelete = [messageId, ...descendants];
                const messagesToDelete = allMessages.filter((m) => allToDelete.includes(m.id));
-               let userMessages = 0,
-                       assistantMessages = 0;
+
+               let assistantMessages = 0,
+                       userMessages = 0;
+
                const messageTypes: string[] = [];
 
                for (const msg of messagesToDelete) {
                        if (msg.role === MessageRole.USER) {
                                userMessages++;
+
                                if (!messageTypes.includes('user message')) messageTypes.push('user message');
                        } else if (msg.role === MessageRole.ASSISTANT) {
                                assistantMessages++;
+
                                if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response');
                        }
                }
 
-               return { totalCount: allToDelete.length, userMessages, assistantMessages, messageTypes };
+               return { assistantMessages, messageTypes, totalCount: allToDelete.length, userMessages };
        }
 
        async deleteMessage(messageId: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
+
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const messageToDelete = findMessageById(allMessages, messageId);
@@ -1915,32 +2192,40 @@ class ChatStore {
         */
        private async continueAsNextAgenticTurn(anchorIndex: number): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
+
                const anchor = conversationsStore.activeMessages[anchorIndex];
+
                if (!anchor) return;
+
                this.cancelPreEncode();
                this.setChatLoading(activeConv.id, true);
                this.clearChatStreaming(activeConv.id);
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const anchorMessage = findMessageById(allMessages, anchor.id);
+
                        if (!anchorMessage) {
                                this.setChatLoading(activeConv.id, false);
+
                                return;
                        }
+
                        const newAssistantMessage = await DatabaseService.createMessageBranch(
                                {
+                                       children: [],
+                                       content: '',
                                        convId: activeConv.id,
-                                       type: MessageType.TEXT,
-                                       timestamp: Date.now(),
+                                       model: null,
                                        role: MessageRole.ASSISTANT,
-                                       content: '',
+                                       timestamp: Date.now(),
                                        toolCalls: '',
-                                       children: [],
-                                       model: null
+                                       type: MessageType.TEXT
                                },
                                anchorMessage.id
                        );
+
                        await conversationsStore.updateCurrentNode(newAssistantMessage.id);
                        conversationsStore.updateConversationTimestamp();
                        await conversationsStore.refreshActiveMessages();
@@ -1949,30 +2234,35 @@ class ChatStore {
                                anchorMessage.id,
                                false
                        ) as DatabaseMessage[];
+
                        await this.streamChatCompletion(conversationPath, newAssistantMessage);
                } catch (error) {
                        if (!isAbortError(error)) console.error('Failed to continue agentic turn:', error);
+
                        this.setChatLoading(activeConv.id, false);
                }
        }
 
        async continueAssistantMessage(messageId: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
+
                const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
 
                if (!result) return;
 
-               const { message: msg, index: idx } = result;
-
+               const { index: idx, message: msg } = result;
                // Decide which resume path applies. tool_calls without tool results can
                // not be resumed mid sequence by continue_final_message, branch instead.
                // tool_calls already paired with tool results need a fresh next turn,
                // not a token level continuation of the target assistant.
                const intent = classifyContinueIntent(conversationsStore.activeMessages, idx);
+
                if (intent.kind === ContinueIntentKind.RERUN_TURN) {
                        return this.regenerateMessageWithBranching(messageId);
                }
+
                if (intent.kind === ContinueIntentKind.NEXT_TURN) {
                        return this.continueAsNextAgenticTurn(intent.truncateAfter);
                }
@@ -1987,6 +2277,7 @@ class ChatStore {
 
                        if (!dbMessage) {
                                this.setChatLoading(activeConv.id, false);
+
                                return;
                        }
 
@@ -2010,7 +2301,6 @@ class ChatStore {
                                        content: fullContent
                                });
                        };
-
                        const abortController = this.getOrCreateAbortController(msg.convId);
 
                        await ChatService.sendMessage(
@@ -2018,52 +2308,12 @@ class ChatStore {
                                {
                                        ...this.getApiOptions(),
                                        continueFinalMessage: true,
-                                       onConnectionState: (state: StreamConnectionState) => {
-                                               if (msg.convId === conversationsStore.activeConversation?.id) {
-                                                       this.streamConnectionState = state;
-                                               }
-                                       },
                                        onChunk: (chunk: string) => {
                                                appendedContent += chunk;
                                                hasReceivedContent = true;
                                                updateStreamingContent(originalContent + appendedContent);
                                                this.setChatReasoning(msg.convId, false);
                                        },
-                                       onCompletionId: (id: string) => {
-                                               if (!id) return;
-                                               // refresh the message id so a later skip targets the live slot after a continue
-                                               conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(msg.id), {
-                                                       completionId: id
-                                               });
-                                               DatabaseService.updateMessage(msg.id, { completionId: id }).catch(() => {});
-                                       },
-                                       onReasoningChunk: (chunk: string) => {
-                                               appendedReasoning += chunk;
-                                               hasReceivedContent = true;
-                                               // mark streaming state so a stop mid-thinking can persist the partial reasoning
-                                               this.setChatStreaming(msg.convId, originalContent + appendedContent, msg.id);
-                                               conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(msg.id), {
-                                                       reasoningContent: originalReasoning + appendedReasoning
-                                               });
-                                               this.setChatReasoning(msg.convId, true);
-                                       },
-                                       onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => {
-                                               const tokensPerSecond =
-                                                       timings?.predicted_ms && timings?.predicted_n
-                                                               ? (timings.predicted_n / timings.predicted_ms) * 1000
-                                                               : 0;
-                                               this.updateProcessingStateFromTimings(
-                                                       {
-                                                               prompt_n: timings?.prompt_n || 0,
-                                                               prompt_ms: timings?.prompt_ms,
-                                                               predicted_n: timings?.predicted_n || 0,
-                                                               predicted_per_second: tokensPerSecond,
-                                                               cache_n: timings?.cache_n || 0,
-                                                               prompt_progress: promptProgress
-                                                       },
-                                                       msg.convId
-                                               );
-                                       },
                                        onComplete: async (
                                                finalContent?: string,
                                                reasoningContent?: string,
@@ -2096,6 +2346,20 @@ class ChatStore {
                                                this.clearChatStreaming(msg.convId);
                                                this.setProcessingState(msg.convId, null);
                                        },
+                                       onCompletionId: (id: string) => {
+                                               if (!id) return;
+
+                                               // refresh the message id so a later skip targets the live slot after a continue
+                                               conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(msg.id), {
+                                                       completionId: id
+                                               });
+                                               DatabaseService.updateMessage(msg.id, { completionId: id }).catch(() => {});
+                                       },
+                                       onConnectionState: (state: StreamConnectionState) => {
+                                               if (msg.convId === conversationsStore.activeConversation?.id) {
+                                                       this.streamConnectionState = state;
+                                               }
+                                       },
                                        onError: async (error: Error) => {
                                                if (isAbortError(error)) {
                                                        if (hasReceivedContent && appendedContent) {
@@ -2139,10 +2403,37 @@ class ChatStore {
                                                this.clearChatStreaming(msg.convId);
                                                this.setProcessingState(msg.convId, null);
                                                this.showErrorDialog({
-                                                       type:
-                                                               error.name === 'TimeoutError' ? ErrorDialogType.TIMEOUT : ErrorDialogType.SERVER,
-                                                       message: error.message
+                                                       message: error.message,
+                                                       type: error.name === 'TimeoutError' ? ErrorDialogType.TIMEOUT : ErrorDialogType.SERVER
+                                               });
+                                       },
+                                       onReasoningChunk: (chunk: string) => {
+                                               appendedReasoning += chunk;
+                                               hasReceivedContent = true;
+                                               // mark streaming state so a stop mid-thinking can persist the partial reasoning
+                                               this.setChatStreaming(msg.convId, originalContent + appendedContent, msg.id);
+                                               conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(msg.id), {
+                                                       reasoningContent: originalReasoning + appendedReasoning
                                                });
+                                               this.setChatReasoning(msg.convId, true);
+                                       },
+                                       onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => {
+                                               const tokensPerSecond =
+                                                       timings?.predicted_ms && timings?.predicted_n
+                                                               ? (timings.predicted_n / timings.predicted_ms) * 1000
+                                                               : 0;
+
+                                               this.updateProcessingStateFromTimings(
+                                                       {
+                                                               cache_n: timings?.cache_n || 0,
+                                                               predicted_n: timings?.predicted_n || 0,
+                                                               predicted_per_second: tokensPerSecond,
+                                                               prompt_ms: timings?.prompt_ms,
+                                                               prompt_n: timings?.prompt_n || 0,
+                                                               prompt_progress: promptProgress
+                                                       },
+                                                       msg.convId
+                                               );
                                        }
                                },
 
@@ -2151,6 +2442,7 @@ class ChatStore {
                        );
                } catch (error) {
                        if (!isAbortError(error)) console.error('Failed to continue message:', error);
+
                        if (activeConv) this.setChatLoading(activeConv.id, false);
                }
        }
@@ -2161,25 +2453,27 @@ class ChatStore {
                shouldBranch: boolean
        ): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
 
                const result = this.getMessageByIdWithRole(messageId, MessageRole.ASSISTANT);
+
                if (!result) return;
 
-               const { message: msg, index: idx } = result;
+               const { index: idx, message: msg } = result;
 
                try {
                        if (shouldBranch) {
                                const newMessage = await DatabaseService.createMessageBranch(
                                        {
+                                               children: [],
+                                               content: newContent,
                                                convId: msg.convId,
-                                               type: msg.type,
-                                               timestamp: Date.now(),
+                                               model: msg.model,
                                                role: msg.role,
-                                               content: newContent,
+                                               timestamp: Date.now(),
                                                toolCalls: msg.toolCalls || '',
-                                               children: [],
-                                               model: msg.model
+                                               type: msg.type
                                        },
                                        msg.parent!
                                );
@@ -2204,12 +2498,15 @@ class ChatStore {
                newExtras?: DatabaseMessageExtra[]
        ): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
 
                const result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
+
                if (!result) return;
 
-               const { message: msg, index: idx } = result;
+               const { index: idx, message: msg } = result;
+
                try {
                        const updateData: Partial<DatabaseMessage> = { content: newContent };
 
@@ -2241,11 +2538,17 @@ class ChatStore {
                newExtras?: DatabaseMessageExtra[]
        ): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv || this.isChatLoadingInternal(activeConv.id)) return;
+
                let result = this.getMessageByIdWithRole(messageId, MessageRole.USER);
+
                if (!result) result = this.getMessageByIdWithRole(messageId, MessageRole.SYSTEM);
+
                if (!result) return;
-               const { message: msg, index: idx } = result;
+
+               const { index: idx, message: msg } = result;
+
                try {
                        const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
                        const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
@@ -2267,41 +2570,48 @@ class ChatStore {
                                // No responses after this message — update in place instead of branching
                                const updates: Partial<DatabaseMessage> = {
                                        content: newContent,
-                                       timestamp: Date.now(),
-                                       extra: extrasToUse
+                                       extra: extrasToUse,
+                                       timestamp: Date.now()
                                };
+
                                await DatabaseService.updateMessage(msg.id, updates);
                                conversationsStore.updateMessageAtIndex(idx, updates);
                                messageIdForResponse = msg.id;
                        } else {
                                // Has children — create a new branch as sibling
                                const parentId = msg.parent || rootMessage?.id;
+
                                if (!parentId) return;
+
                                const newMessage = await DatabaseService.createMessageBranch(
                                        {
+                                               children: [],
+                                               content: newContent,
                                                convId: msg.convId,
-                                               type: msg.type,
-                                               timestamp: Date.now(),
+                                               extra: extrasToUse,
+                                               model: msg.model,
                                                role: msg.role,
-                                               content: newContent,
+                                               timestamp: Date.now(),
                                                toolCalls: msg.toolCalls || '',
-                                               children: [],
-                                               extra: extrasToUse,
-                                               model: msg.model
+                                               type: msg.type
                                        },
                                        parentId
                                );
+
                                await conversationsStore.updateCurrentNode(newMessage.id);
                                messageIdForResponse = newMessage.id;
                        }
 
                        conversationsStore.updateConversationTimestamp();
+
                        if (isFirstUserMessage && newContent.trim())
                                await conversationsStore.updateConversationName(
                                        activeConv.id,
                                        generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
                                );
+
                        await conversationsStore.refreshActiveMessages();
+
                        if (msg.role === MessageRole.USER)
                                await this.generateResponseForMessage(messageIdForResponse);
                } catch (error) {
@@ -2311,6 +2621,7 @@ class ChatStore {
 
        private async generateResponseForMessage(userMessageId: string): Promise<void> {
                const activeConv = conversationsStore.activeConversation;
+
                if (!activeConv) return;
 
                this.showErrorDialog(null);
@@ -2326,14 +2637,14 @@ class ChatStore {
                        ) as DatabaseMessage[];
                        const assistantMessage = await DatabaseService.createMessageBranch(
                                {
+                                       children: [],
+                                       content: '',
                                        convId: activeConv.id,
-                                       type: MessageType.TEXT,
-                                       timestamp: Date.now(),
+                                       model: null,
                                        role: MessageRole.ASSISTANT,
-                                       content: '',
+                                       timestamp: Date.now(),
                                        toolCalls: '',
-                                       children: [],
-                                       model: null
+                                       type: MessageType.TEXT
                                },
                                userMessageId
                        );
@@ -2386,21 +2697,23 @@ class ChatStore {
 
                if (processingState === null) {
                        console.warn('Failed to parse timing data - skipping update');
+
                        return;
                }
 
                const targetId = conversationId || this.activeConversationId;
+
                if (targetId) {
                        this.setProcessingState(targetId, processingState);
                }
        }
 
        private parseTimingData(timingData: Record<string, unknown>): ApiProcessingState | null {
-               const promptTokens = (timingData.prompt_n as number) || 0,
-                       promptMs = (timingData.prompt_ms as number) || undefined,
+               const cacheTokens = (timingData.cache_n as number) || 0,
                        predictedTokens = (timingData.predicted_n as number) || 0,
-                       tokensPerSecond = (timingData.predicted_per_second as number) || 0,
-                       cacheTokens = (timingData.cache_n as number) || 0;
+                       promptMs = (timingData.prompt_ms as number) || undefined,
+                       promptTokens = (timingData.prompt_n as number) || 0,
+                       tokensPerSecond = (timingData.predicted_per_second as number) || 0;
                const promptProgress = timingData.prompt_progress as
                        | { total: number; cache: number; processed: number; time_ms: number }
                        | undefined;
@@ -2415,43 +2728,47 @@ class ChatStore {
                const progressPercent = promptProgress
                        ? Math.round((progressActualDone / progressActualTotal) * 100)
                        : undefined;
+
                return {
-                       status: predictedTokens > 0 ? 'generating' : promptProgress ? 'preparing' : 'idle',
-                       tokensDecoded: predictedTokens,
-                       tokensRemaining: outputTokensMax - predictedTokens,
-                       contextUsed,
+                       cacheTokens,
                        contextTotal,
-                       outputTokensUsed,
-                       outputTokensMax,
+                       contextUsed,
                        hasNextToken: predictedTokens > 0,
-                       tokensPerSecond,
-                       temperature: currentConfig.temperature ?? 0.8,
-                       topP: currentConfig.top_p ?? 0.95,
-                       speculative: false,
+                       outputTokensMax,
+                       outputTokensUsed,
                        progressPercent,
+                       promptMs,
                        promptProgress,
                        promptTokens,
-                       promptMs,
-                       cacheTokens
+                       speculative: false,
+                       status: predictedTokens > 0 ? 'generating' : promptProgress ? 'preparing' : 'idle',
+                       temperature: currentConfig.temperature ?? 0.8,
+                       tokensDecoded: predictedTokens,
+                       tokensPerSecond,
+                       tokensRemaining: outputTokensMax - predictedTokens,
+                       topP: currentConfig.top_p ?? 0.95
                };
        }
 
        restoreProcessingStateFromMessages(messages: DatabaseMessage[], conversationId: string): void {
                for (let i = messages.length - 1; i >= 0; i--) {
                        const message = messages[i];
+
                        if (message.role === MessageRole.ASSISTANT && message.timings) {
                                const restoredState = this.parseTimingData({
-                                       prompt_n: message.timings.prompt_n || 0,
-                                       prompt_ms: message.timings.prompt_ms,
+                                       cache_n: message.timings.cache_n || 0,
                                        predicted_n: message.timings.predicted_n || 0,
                                        predicted_per_second:
                                                message.timings.predicted_n && message.timings.predicted_ms
                                                        ? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
                                                        : 0,
-                                       cache_n: message.timings.cache_n || 0
+                                       prompt_ms: message.timings.prompt_ms,
+                                       prompt_n: message.timings.prompt_n || 0
                                });
+
                                if (restoredState) {
                                        this.setProcessingState(conversationId, restoredState);
+
                                        return;
                                }
                        }
@@ -2461,8 +2778,10 @@ class ChatStore {
        getConversationModel(messages: DatabaseMessage[]): string | null {
                for (let i = messages.length - 1; i >= 0; i--) {
                        const message = messages[i];
+
                        if (message.role === MessageRole.ASSISTANT && message.model) return message.model;
                }
+
                return null;
        }
 
@@ -2474,6 +2793,7 @@ class ChatStore {
 
                if (isRouterMode()) {
                        const modelName = selectedModelName();
+
                        if (modelName) apiOptions.model = modelName;
                }
 
@@ -2485,8 +2805,10 @@ class ChatStore {
 
                // an explicit reasoning choice overrides the server default, DEFAULT sends nothing
                const effort = conversationsStore.getReasoningEffort();
+
                if (effort !== ReasoningEffort.DEFAULT) {
                        apiOptions.enableThinking = effort !== ReasoningEffort.OFF;
+
                        if (effort !== ReasoningEffort.OFF) apiOptions.reasoningEffort = effort;
                }
 
@@ -2570,6 +2892,7 @@ class ChatStore {
 
                try {
                        const allIdle = await ChatService.areAllSlotsIdle(model, signal);
+
                        if (!allIdle || signal.aborted) return;
 
                        const messagesWithAssistant: DatabaseMessage[] = [
index 441edb3acfcaa25128bda0da1a22b924d5a3fdac..654dc9f7787b620050bdeb31d531511ebe76e35f 100644 (file)
@@ -16,20 +16,23 @@ import {
 let closeTimer: ReturnType<typeof setTimeout> | undefined;
 let lastPointerType = '';
 
-export const gaugePopup = $state({ open: false, centerX: 0, bottom: 0 });
+export const gaugePopup = $state({ bottom: 0, centerX: 0, open: false });
 
 function openFrom(trigger: HTMLElement): void {
        clearTimeout(closeTimer);
        const frame = trigger.closest('form');
+
        if (frame) {
                const frameRect = frame.getBoundingClientRect();
                const triggerRect = trigger.getBoundingClientRect();
                const centerX = triggerRect.left + triggerRect.width / 2 - frameRect.left;
                const min = CONTEXT_GAUGE_CARD_HALF_WIDTH_PX + CONTEXT_GAUGE_EDGE_MARGIN_PX;
                const max = frameRect.width - CONTEXT_GAUGE_CARD_HALF_WIDTH_PX - CONTEXT_GAUGE_EDGE_MARGIN_PX;
+
                gaugePopup.centerX = Math.min(Math.max(centerX, min), Math.max(min, max));
                gaugePopup.bottom = frameRect.bottom - triggerRect.top + CONTEXT_GAUGE_DIAL_GAP_PX;
        }
+
        gaugePopup.open = true;
 }
 
@@ -53,32 +56,38 @@ export function gaugeTriggerPointerDown(event: PointerEvent): void {
 
 export function gaugeTriggerClick(event: MouseEvent): void {
        if (lastPointerType !== 'touch') return;
+
        toggleFrom(event.currentTarget as HTMLElement);
 }
 
 export function gaugeTriggerKeydown(event: KeyboardEvent): void {
        if (event.key !== 'Enter' && event.key !== ' ') return;
+
        event.preventDefault();
        toggleFrom(event.currentTarget as HTMLElement);
 }
 
 export function gaugeTriggerEnter(event: PointerEvent): void {
        if (event.pointerType !== 'mouse') return;
+
        openFrom(event.currentTarget as HTMLElement);
 }
 
 export function gaugeTriggerLeave(event: PointerEvent): void {
        if (event.pointerType !== 'mouse') return;
+
        scheduleClose();
 }
 
 export function gaugeCardEnter(event: PointerEvent): void {
        if (event.pointerType !== 'mouse') return;
+
        clearTimeout(closeTimer);
 }
 
 export function gaugeCardLeave(event: PointerEvent): void {
        if (event.pointerType !== 'mouse') return;
+
        scheduleClose();
 }
 
index c8b292039811598293686096553c817cff44a8e6..b1fcac4bedd13cfead241bdb0bed8a17df2e5425 100644 (file)
  * @see DatabaseService in services/database.ts for IndexedDB operations
  */
 
-import { goto } from '$app/navigation';
 import { browser } from '$app/environment';
-import { toast } from 'svelte-sonner';
-import { DatabaseService } from '$lib/services/database.service';
-import { MigrationService } from '$lib/services/migration.service';
-import { config } from '$lib/stores/settings.svelte';
-import { mcpStore } from '$lib/stores/mcp.svelte';
-import { filterByLeafNodeId, findLeafNode, generateConversationTitle } from '$lib/utils';
-import type { McpServerOverride } from '$lib/types/database';
-import { zipSync, unzipSync, strToU8, strFromU8 } from 'fflate';
-import {
-       MessageRole,
-       FileExtensionText,
-       MimeTypeText,
-       MimeTypeApplication,
-       ReasoningEffort,
-       SessionRecordType
-} from '$lib/enums';
+import { goto } from '$app/navigation';
 import {
-       ISO_DATE_TIME_SEPARATOR,
-       ISO_DATE_TIME_SEPARATOR_REPLACEMENT,
-       ISO_TIMESTAMP_SLICE_LENGTH,
        EXPORT_CONV_ID_TRIM_LENGTH,
-       EXPORT_CONV_NONALNUM_REPLACEMENT,
        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,
-       NON_ALPHANUMERIC_REGEX,
+       ISO_TIMESTAMP_SLICE_LENGTH,
        MULTIPLE_UNDERSCORE_REGEX,
-       REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY,
        NEWLINE,
+       NON_ALPHANUMERIC_REGEX,
+       REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY,
        SESSION_HARNESS,
        ZIP_MAGIC
 } from '$lib/constants';
-
 import { ROUTES } from '$lib/constants/routes';
+import {
+       FileExtensionText,
+       MessageRole,
+       MimeTypeApplication,
+       MimeTypeText,
+       ReasoningEffort,
+       SessionRecordType
+} from '$lib/enums';
+import { DatabaseService } from '$lib/services/database.service';
+import { MigrationService } from '$lib/services/migration.service';
 import { RouterService } from '$lib/services/router.service';
+import { mcpStore } from '$lib/stores/mcp.svelte';
+import { config } from '$lib/stores/settings.svelte';
+import type { McpServerOverride } from '$lib/types/database';
+import { filterByLeafNodeId, findLeafNode, generateConversationTitle } from '$lib/utils';
+import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
 import { SvelteMap, SvelteSet } from 'svelte/reactivity';
+import { toast } from 'svelte-sonner';
 
 export interface ConversationTreeItem {
        conversation: DatabaseConversation;
@@ -98,8 +97,10 @@ class ConversationsStore {
        /** Load reasoning effort default from localStorage, DEFAULT defers to the server */
        private static loadReasoningEffortDefault(): ReasoningEffort {
                if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.DEFAULT;
+
                try {
                        const raw = localStorage.getItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY);
+
                        return (raw as ReasoningEffort) || ReasoningEffort.DEFAULT;
                } catch {
                        return ReasoningEffort.DEFAULT;
@@ -109,6 +110,7 @@ class ConversationsStore {
        /** Persist reasoning effort default to localStorage */
        private saveReasoningEffortDefaults(): void {
                if (typeof globalThis.localStorage === 'undefined') return;
+
                localStorage.setItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY, this.pendingReasoningEffort);
        }
 
@@ -138,6 +140,7 @@ class ConversationsStore {
         */
        init(): Promise<void> {
                if (!browser) return Promise.resolve();
+
                if (this.initPromise) return this.initPromise;
 
                this.initPromise = (async () => {
@@ -229,6 +232,7 @@ class ConversationsStore {
                if (index !== -1) {
                        return this.activeMessages.splice(index, 1)[0];
                }
+
                return undefined;
        }
 
@@ -245,6 +249,7 @@ class ConversationsStore {
         */
        async loadConversations(): Promise<void> {
                const conversations = await DatabaseService.getAllConversations();
+
                this.conversations = conversations;
        }
 
@@ -255,16 +260,16 @@ class ConversationsStore {
         */
        async createConversation(name?: string): Promise<string> {
                const conversationName = name || `Chat ${new Date().toLocaleString()}`;
-
                // No MCP override list is seeded: getAllMcpServerOverrides resolves
                // servers without a per-conversation override to `mcpServers[i].enabled`,
                // and only explicit toggles are stored on the conversation.
                // Working directory picked on the new-chat screen gets threaded in
                // here too, then cleared so it doesn't bleed onto subsequent new chats.
                const conversation = await DatabaseService.createConversation(conversationName, {
-                       reasoningEffort: this.pendingReasoningEffort,
-                       cwd: this.pendingCwd ?? undefined
+                       cwd: this.pendingCwd ?? undefined,
+                       reasoningEffort: this.pendingReasoningEffort
                });
+
                this.pendingCwd = null;
 
                this.conversations = [conversation, ...this.conversations];
@@ -302,15 +307,18 @@ class ConversationsStore {
                                        conversation.currNode,
                                        false
                                ) as DatabaseMessage[];
+
                                this.activeMessages = filteredMessages;
                        } else {
                                const messages = await DatabaseService.getConversationMessages(convId);
+
                                this.activeMessages = messages;
                        }
 
                        return true;
                } catch (error) {
                        console.error('Failed to load conversation:', error);
+
                        return false;
                }
        }
@@ -338,8 +346,10 @@ class ConversationsStore {
                                // Collect all descendants recursively
                                const idsToRemove = new SvelteSet([convId]);
                                const queue = [convId];
+
                                while (queue.length > 0) {
                                        const parentId = queue.pop()!;
+
                                        for (const c of this.conversations) {
                                                if (c.forkedFromConversationId === parentId && !idsToRemove.has(c.id)) {
                                                        idsToRemove.add(c.id);
@@ -357,6 +367,7 @@ class ConversationsStore {
                                // Reparent direct children to deleted conv's parent (or promote to top-level)
                                const deletedConv = this.conversations.find((c) => c.id === convId);
                                const newParent = deletedConv?.forkedFromConversationId;
+
                                this.conversations = this.conversations
                                        .filter((c) => c.id !== convId)
                                        .map((c) =>
@@ -381,6 +392,7 @@ class ConversationsStore {
        async deleteAll(): Promise<void> {
                try {
                        const allConversations = await DatabaseService.getAllConversations();
+
                        await DatabaseService.bulkDeleteConversations(allConversations.map((c) => c.id));
 
                        this.clearActiveConversation();
@@ -409,8 +421,10 @@ class ConversationsStore {
                        // Collect all descendants recursively so the local cache stays consistent
                        // even when deleteWithForks is omitted.
                        const queue = [...convIds];
+
                        while (queue.length > 0) {
                                const parentId = queue.pop()!;
+
                                for (const c of this.conversations) {
                                        if (c.forkedFromConversationId === parentId && !idsToRemove.has(c.id)) {
                                                idsToRemove.add(c.id);
@@ -453,16 +467,18 @@ class ConversationsStore {
 
                try {
                        const updates = await DatabaseService.bulkToggleConversationPins(convIds);
-
                        const activeId = this.activeConversation?.id;
+
                        if (activeId && updates.has(activeId)) {
                                this.activeConversation = {
                                        ...this.activeConversation!,
                                        pinned: updates.get(activeId)!
                                };
                        }
+
                        for (let i = 0; i < this.conversations.length; i++) {
                                const newPinned = updates.get(this.conversations[i].id);
+
                                if (newPinned !== undefined) this.conversations[i].pinned = newPinned;
                        }
 
@@ -487,16 +503,18 @@ class ConversationsStore {
 
                try {
                        const fetched = await DatabaseService.getConversationsWithMessages(convIds);
-
                        const activeId = this.activeConversation?.id;
                        const overridden = fetched.get(activeId ?? '');
+
                        if (overridden && activeId) {
                                overridden.conv = { ...this.activeConversation! };
                        }
 
                        const exported = [...fetched.values()];
+
                        if (exported.length === 0) {
                                toast.error('No conversations to export');
+
                                return;
                        }
 
@@ -531,13 +549,13 @@ class ConversationsStore {
 
                if (allMessages.length === 0) {
                        this.activeMessages = [];
+
                        return;
                }
 
                const leafNodeId =
                        this.activeConversation.currNode ||
                        allMessages.reduce((latest, msg) => (msg.timestamp > latest.timestamp ? msg : latest)).id;
-
                const currentPath = filterByLeafNodeId(allMessages, leafNodeId, false) as DatabaseMessage[];
 
                this.activeMessages = currentPath;
@@ -591,7 +609,6 @@ class ConversationsStore {
        async toggleConversationPin(convId: string): Promise<boolean> {
                try {
                        const newPinnedState = await DatabaseService.toggleConversationPin(convId);
-
                        const convIndex = this.conversations.findIndex((c) => c.id === convId);
 
                        if (convIndex !== -1) {
@@ -605,6 +622,7 @@ class ConversationsStore {
                        return newPinnedState;
                } catch (error) {
                        console.error('Failed to toggle conversation pin:', error);
+
                        return false;
                }
        }
@@ -618,15 +636,16 @@ class ConversationsStore {
         */
        updateConversationTimestamp(convId?: string): void {
                const targetId = convId ?? this.activeConversation?.id;
+
                if (!targetId) return;
 
                const now = Date.now();
-
                const chatIndex = this.conversations.findIndex((c) => c.id === targetId);
 
                if (chatIndex !== -1) {
                        this.conversations[chatIndex].lastModified = now;
                        const updatedConv = this.conversations.splice(chatIndex, 1)[0];
+
                        this.conversations = [updatedConv, ...this.conversations];
                }
 
@@ -670,7 +689,6 @@ class ConversationsStore {
                const currentFirstUserMessage = this.activeMessages.find(
                        (m) => m.role === MessageRole.USER && m.parent === rootMessage?.id
                );
-
                const currentLeafNodeId = findLeafNode(allMessages, siblingId);
 
                await DatabaseService.updateCurrentNode(this.activeConversation.id, currentLeafNodeId);
@@ -714,8 +732,10 @@ class ConversationsStore {
         */
        #getDefaultOverride(serverId: string): McpServerOverride | undefined {
                const server = mcpStore.getServers().find((s) => s.id === serverId);
+
                if (!server) return undefined;
-               return { serverId, enabled: server.enabled };
+
+               return { enabled: server.enabled, serverId };
        }
 
        /**
@@ -729,7 +749,9 @@ class ConversationsStore {
                const override = this.activeConversation?.mcpServerOverrides?.find(
                        (o: McpServerOverride) => o.serverId === serverId
                );
+
                if (override) return override;
+
                return this.#getDefaultOverride(serverId);
        }
 
@@ -740,9 +762,11 @@ class ConversationsStore {
         */
        getAllMcpServerOverrides(): McpServerOverride[] {
                const overrides = this.activeConversation?.mcpServerOverrides;
+
                return mcpStore.getServers().map((s) => {
                        const override = overrides?.find((o: McpServerOverride) => o.serverId === s.id);
-                       return { serverId: s.id, enabled: override?.enabled ?? s.enabled };
+
+                       return { enabled: override?.enabled ?? s.enabled, serverId: s.id };
                });
        }
 
@@ -753,6 +777,7 @@ class ConversationsStore {
         */
        isMcpServerEnabledForChat(serverId: string): boolean {
                const override = this.getMcpServerOverride(serverId);
+
                return override?.enabled ?? false;
        }
 
@@ -768,16 +793,18 @@ class ConversationsStore {
                        if (enabled !== undefined) {
                                mcpStore.updateServer(serverId, { enabled });
                        }
+
                        return;
                }
 
                // Clone to plain objects to avoid Proxy serialization issues with IndexedDB
                const currentOverrides = (this.activeConversation.mcpServerOverrides || []).map(
                        (o: McpServerOverride) => ({
-                               serverId: o.serverId,
-                               enabled: o.enabled
+                               enabled: o.enabled,
+                               serverId: o.serverId
                        })
                );
+
                let newOverrides: McpServerOverride[];
 
                if (enabled === undefined) {
@@ -786,11 +813,12 @@ class ConversationsStore {
                        const existingIndex = currentOverrides.findIndex(
                                (o: McpServerOverride) => o.serverId === serverId
                        );
+
                        if (existingIndex >= 0) {
                                newOverrides = [...currentOverrides];
-                               newOverrides[existingIndex] = { serverId, enabled };
+                               newOverrides[existingIndex] = { enabled, serverId };
                        } else {
-                               newOverrides = [...currentOverrides, { serverId, enabled }];
+                               newOverrides = [...currentOverrides, { enabled, serverId }];
                        }
                }
 
@@ -804,6 +832,7 @@ class ConversationsStore {
                };
 
                const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
+
                if (convIndex !== -1) {
                        this.conversations[convIndex].mcpServerOverrides =
                                newOverrides.length > 0 ? newOverrides : undefined;
@@ -816,6 +845,7 @@ class ConversationsStore {
         */
        async toggleMcpServerForChat(serverId: string): Promise<void> {
                const currentEnabled = this.isMcpServerEnabledForChat(serverId);
+
                await this.setMcpServerOverride(serverId, !currentEnabled);
        }
 
@@ -837,12 +867,14 @@ class ConversationsStore {
                        if (this.activeConversation.reasoningEffort !== undefined) {
                                return this.activeConversation.reasoningEffort;
                        }
+
                        // conversations created before the tri-state store an explicit
                        // opt-out only as thinkingEnabled = false
                        if (this.activeConversation.thinkingEnabled === false) {
                                return ReasoningEffort.OFF;
                        }
                }
+
                return this.pendingReasoningEffort;
        }
 
@@ -855,6 +887,7 @@ class ConversationsStore {
                if (!this.activeConversation) {
                        this.pendingReasoningEffort = effort;
                        this.saveReasoningEffortDefaults();
+
                        return;
                }
 
@@ -868,6 +901,7 @@ class ConversationsStore {
                });
 
                const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
+
                if (convIndex !== -1) {
                        this.conversations[convIndex].reasoningEffort = effort;
                }
@@ -889,6 +923,7 @@ class ConversationsStore {
                // No chat yet - buffer for the first chat the user creates.
                if (!this.activeConversation) {
                        this.pendingCwd = trimmed ?? null;
+
                        return;
                }
 
@@ -902,10 +937,12 @@ class ConversationsStore {
                });
 
                const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id);
+
                if (convIndex !== -1) {
                        this.conversations[convIndex].cwd = trimmed;
                        this.conversations = [...this.conversations];
                }
+
                this.pendingCwd = null;
        }
 
@@ -964,22 +1001,20 @@ class ConversationsStore {
                msgs?: DatabaseMessage[]
        ): 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);
-
                // 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 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) ?? '';
+
                return `${formattedDate}_conv_${trimmedConvId}_${sanitizedName}${FileExtensionText.JSONL}`;
        }
 
@@ -992,10 +1027,9 @@ class ConversationsStore {
         */
        serializeSessionToJsonl(data: ExportedConversation): string {
                const { conv, messages } = data;
-
                const sessionLine = JSON.stringify({
-                       type: SessionRecordType.SESSION,
                        harness: SESSION_HARNESS,
+                       type: SessionRecordType.SESSION,
                        ...conv
                });
                const messageLines = messages.map((message: DatabaseMessage) => {
@@ -1003,7 +1037,7 @@ class ConversationsStore {
                        const { toolCalls, ...rest } = message;
                        const normalized = toolCalls ? { ...rest, toolCalls: JSON.parse(toolCalls) } : rest;
 
-                       return JSON.stringify({ type: SessionRecordType.MESSAGE, message: normalized });
+                       return JSON.stringify({ message: normalized, type: SessionRecordType.MESSAGE });
                });
 
                return [sessionLine, ...messageLines].join(NEWLINE);
@@ -1019,10 +1053,12 @@ class ConversationsStore {
         */
        parseSessionsJsonl(text: string): ExportedConversation[] {
                const sessions: ExportedConversation[] = [];
+
                let current: ExportedConversation | null = null;
 
                for (const line of text.split(NEWLINE)) {
                        const trimmed = line.trim();
+
                        if (!trimmed) continue;
 
                        const record = JSON.parse(trimmed);
@@ -1030,6 +1066,7 @@ class ConversationsStore {
                        if (record.type === SessionRecordType.SESSION) {
                                // Drop the discriminator and harness marker; the rest is the conversation.
                                const conv = { ...record };
+
                                delete conv.type;
                                delete conv.harness;
                                current = { conv: conv as DatabaseConversation, messages: [] };
@@ -1040,10 +1077,12 @@ class ConversationsStore {
                                }
 
                                const message = record.message as DatabaseMessage;
+
                                // `toolCalls` is parsed to an array on export; the DB stores it as a string.
                                if (message.toolCalls !== undefined && typeof message.toolCalls !== 'string') {
                                        message.toolCalls = JSON.stringify(message.toolCalls);
                                }
+
                                current.messages.push(message);
                        }
                        // Ignore unknown record types for forward compatibility.
@@ -1084,10 +1123,13 @@ class ConversationsStore {
                if (ZIP_MAGIC.every((byte, index) => bytes[index] === byte)) {
                        const entries = unzipSync(bytes);
                        const sessions: ExportedConversation[] = [];
+
                        for (const [entryName, entryBytes] of Object.entries(entries)) {
                                if (!entryName.toLowerCase().endsWith(FileExtensionText.JSONL)) continue;
+
                                sessions.push(...this.parseSessionsJsonl(strFromU8(entryBytes)));
                        }
+
                        return sessions;
                }
 
@@ -1099,12 +1141,15 @@ class ConversationsStore {
 
                // Legacy JSON format: an array of conversations or a single conversation object.
                const parsed = JSON.parse(text);
+
                if (Array.isArray(parsed)) {
                        return parsed;
                }
+
                if (parsed && typeof parsed === 'object' && 'conv' in parsed && 'messages' in parsed) {
                        return [parsed];
                }
+
                throw new Error(
                        'Invalid file format: expected array of conversations or single conversation object'
                );
@@ -1120,13 +1165,14 @@ class ConversationsStore {
 
                if (!conversation) {
                        console.error('Invalid data: missing conversation');
+
                        return;
                }
 
                const downloadFilename = filename ?? this.generateConversationFilename(conversation, msgs);
-
                const jsonl = this.serializeSessionToJsonl(data);
                const blob = new Blob([jsonl], { type: MimeTypeText.JSONL });
+
                this.triggerDownload(blob, downloadFilename);
        }
 
@@ -1138,6 +1184,7 @@ class ConversationsStore {
        downloadConversationsArchive(data: ExportedConversation[]): void {
                if (data.length === 0) {
                        console.error('Invalid data: no conversations to export');
+
                        return;
                }
 
@@ -1150,6 +1197,7 @@ class ConversationsStore {
                        // Disambiguate any duplicate filenames within the archive.
                        let entryName = baseName;
                        let suffix = 1;
+
                        while (usedNames.has(entryName)) {
                                entryName = baseName.replace(
                                        new RegExp(`${FileExtensionText.JSONL}$`),
@@ -1162,9 +1210,9 @@ class ConversationsStore {
                }
 
                const archiveName = `${new Date().toISOString().split(ISO_DATE_TIME_SEPARATOR)[0]}_conversations${FileExtensionText.ZIP}`;
-
                const zipped = zipSync(files);
                const blob = new Blob([zipped], { type: MimeTypeApplication.ZIP });
+
                this.triggerDownload(blob, archiveName);
        }
 
@@ -1174,6 +1222,7 @@ class ConversationsStore {
        private triggerDownload(blob: Blob, filename: string): void {
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
+
                a.href = url;
                a.download = filename;
                document.body.appendChild(a);
@@ -1208,7 +1257,9 @@ class ConversationsStore {
                data: ExportedConversations
        ): Promise<{ imported: DatabaseConversation[]; skipped: DatabaseConversation[] }> {
                const result = await DatabaseService.importConversations(data);
+
                await this.loadConversations();
+
                return result;
        }
 }
@@ -1238,7 +1289,9 @@ export const isConversationsInitialized = () => conversationsStore.isInitialized
 // Pinned conversations first, then by lastModified descending
 const comparePinnedThenRecent = (a: DatabaseConversation, b: DatabaseConversation) => {
        if (a.pinned && !b.pinned) return -1;
+
        if (!a.pinned && b.pinned) return 1;
+
        return b.lastModified - a.lastModified;
 };
 
@@ -1265,6 +1318,7 @@ export function buildConversationTree(convs: DatabaseConversation[]): Conversati
                result.push({ conversation: conv, depth });
 
                const children = childrenByParent.get(conv.id);
+
                if (children) {
                        children.sort(comparePinnedThenRecent);
 
@@ -1275,6 +1329,7 @@ export function buildConversationTree(convs: DatabaseConversation[]): Conversati
        }
 
        const roots = convs.filter((c) => !forkIds.has(c.id)).sort(comparePinnedThenRecent);
+
        for (const root of roots) {
                walk(root, 0);
        }
index d0f04b437a3f7e7ae3297d5e6cf9542fa5a0ce9d..fef4865a9993b4a09182fc2fe36c83abacf631c0 100644 (file)
@@ -32,8 +32,8 @@ interface DeviceContext {
 const SERVER_DEFAULT: DeviceContext = {
        isIOSDevice: false,
        isIOSSafari: false,
-       isWKWebView: false,
-       isStandalone: false
+       isStandalone: false,
+       isWKWebView: false
 };
 
 function detect(): DeviceContext {
@@ -41,22 +41,19 @@ function detect(): DeviceContext {
 
        const ua = navigator.userAgent;
        const isTouch = navigator.maxTouchPoints > 0;
-
        const isIOSDevice = UA_PATTERNS.IOS_PHONE.test(ua) || (UA_PATTERNS.MACINTOSH.test(ua) && isTouch);
-
        // Safari keeps 'Safari/' in the UA; non-Safari iOS browsers emit their own
        // token instead. WKWebView typically omits 'Safari/' entirely.
        const hasSafariToken = UA_PATTERNS.SAFARI.test(ua) && !UA_PATTERNS.WEBVIEW_IOS.test(ua);
        const isIOSSafari = isIOSDevice && hasSafariToken;
        const isWKWebView = isIOSDevice && !hasSafariToken;
-
        // navigator.standalone is the legacy iOS-only flag (deprecated but still
        // present); display-mode: standalone is the modern standard (Safari 16.4+).
        const isStandalone =
                window.matchMedia(MEDIA_QUERIES.DISPLAY_MODE_STANDALONE).matches ||
                (navigator as Navigator & { standalone?: boolean }).standalone === true;
 
-       return { isIOSDevice, isIOSSafari, isWKWebView, isStandalone };
+       return { isIOSDevice, isIOSSafari, isStandalone, isWKWebView };
 }
 
 export const device = $state<DeviceContext>(detect());
index 7ee814d840eab543d3d38101edc29e1072887226..235a59122e7fc7b7e5197040d505324716011f10 100644 (file)
@@ -10,13 +10,15 @@ class DraftMessagesStore {
 
        getDraftMessage(chatId: string | undefined): DraftMessage {
                const key = chatId ?? NEW_CHAT_DRAFT_KEY;
-               return this.drafts.get(key) ?? { message: '', files: [] };
+
+               return this.drafts.get(key) ?? { files: [], message: '' };
        }
 
        saveDraftMessage(chatId: string | undefined, message: string, files: ChatUploadedFile[]): void {
                const key = chatId ?? NEW_CHAT_DRAFT_KEY;
+
                if (message || files.length > 0) {
-                       this.drafts.set(key, { message, files: [...files] });
+                       this.drafts.set(key, { files: [...files], message });
                } else {
                        this.drafts.delete(key);
                }
@@ -24,6 +26,7 @@ class DraftMessagesStore {
 
        clearDraftMessage(chatId: string | undefined): void {
                const key = chatId ?? NEW_CHAT_DRAFT_KEY;
+
                this.drafts.delete(key);
        }
 }
index 81fb86d972d47d3570c1f48ee03bff4e19287b43..6f0f8d7c80c5a9ed7720a9fddaec468ff1c7d218 100644 (file)
  * @see MCP Protocol Specification: https://modelcontextprotocol.io/specification/2025-06-18/server/resources
  */
 
-import { SvelteMap } from 'svelte/reactivity';
-import { AttachmentType } from '$lib/enums';
 import {
+       BINARY_CONTENT_LABEL,
        MCP_RESOURCE_ATTACHMENT_ID_PREFIX,
        MCP_RESOURCE_CACHE_MAX_ENTRIES,
        MCP_RESOURCE_CACHE_TTL_MS,
        NEWLINE,
-       RESOURCE_UNKNOWN_TYPE,
-       BINARY_CONTENT_LABEL
+       RESOURCE_UNKNOWN_TYPE
 } from '$lib/constants';
-import { normalizeResourceUri } from '$lib/utils';
+import { AttachmentType } from '$lib/enums';
 import type {
+       DatabaseMessageExtraMcpResource,
+       MCPCachedResource,
        MCPResource,
-       MCPResourceTemplate,
+       MCPResourceAttachment,
        MCPResourceContent,
        MCPResourceInfo,
-       MCPResourceTemplateInfo,
-       MCPCachedResource,
-       MCPResourceAttachment,
        MCPResourceSubscription,
-       MCPServerResources,
-       DatabaseMessageExtraMcpResource
+       MCPResourceTemplate,
+       MCPResourceTemplateInfo,
+       MCPServerResources
 } from '$lib/types';
+import { normalizeResourceUri } from '$lib/utils';
+import { SvelteMap } from 'svelte/reactivity';
 
 function generateAttachmentId(): string {
        return `${MCP_RESOURCE_ATTACHMENT_ID_PREFIX}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
@@ -67,6 +67,7 @@ class MCPResourceStore {
 
        get totalResourceCount(): number {
                let count = 0;
+
                for (const serverRes of this._serverResources.values()) {
                        count += serverRes.resources.length;
                }
@@ -76,6 +77,7 @@ class MCPResourceStore {
 
        get totalTemplateCount(): number {
                let count = 0;
+
                for (const serverRes of this._serverResources.values()) {
                        count += serverRes.templates.length;
                }
@@ -108,12 +110,12 @@ class MCPResourceStore {
                templates: MCPResourceTemplate[]
        ): void {
                this._serverResources.set(serverName, {
-                       serverName,
-                       resources,
-                       templates,
+                       error: undefined,
                        lastFetched: new Date(),
                        loading: false,
-                       error: undefined
+                       resources,
+                       serverName,
+                       templates
                });
                console.log(
                        `[MCPResources][${serverName}] Set ${resources.length} resources, ${templates.length} templates`
@@ -125,15 +127,16 @@ class MCPResourceStore {
         */
        setServerLoading(serverName: string, loading: boolean): void {
                const existing = this._serverResources.get(serverName);
+
                if (existing) {
                        this._serverResources.set(serverName, { ...existing, loading });
                } else {
                        this._serverResources.set(serverName, {
-                               serverName,
-                               resources: [],
-                               templates: [],
+                               error: undefined,
                                loading,
-                               error: undefined
+                               resources: [],
+                               serverName,
+                               templates: []
                        });
                }
        }
@@ -145,14 +148,14 @@ class MCPResourceStore {
                const existing = this._serverResources.get(serverName);
 
                if (existing) {
-                       this._serverResources.set(serverName, { ...existing, loading: false, error });
+                       this._serverResources.set(serverName, { ...existing, error, loading: false });
                } else {
                        this._serverResources.set(serverName, {
-                               serverName,
-                               resources: [],
-                               templates: [],
+                               error,
                                loading: false,
-                               error
+                               resources: [],
+                               serverName,
+                               templates: []
                        });
                }
        }
@@ -173,14 +176,14 @@ class MCPResourceStore {
                for (const [serverName, serverRes] of this._serverResources) {
                        for (const resource of serverRes.resources) {
                                result.push({
-                                       uri: resource.uri,
-                                       name: resource.name,
-                                       title: resource.title,
+                                       annotations: resource.annotations,
                                        description: resource.description,
+                                       icons: resource.icons,
                                        mimeType: resource.mimeType,
+                                       name: resource.name,
                                        serverName,
-                                       annotations: resource.annotations,
-                                       icons: resource.icons
+                                       title: resource.title,
+                                       uri: resource.uri
                                });
                        }
                }
@@ -197,14 +200,14 @@ class MCPResourceStore {
                for (const [serverName, serverRes] of this._serverResources) {
                        for (const template of serverRes.templates) {
                                result.push({
-                                       uriTemplate: template.uriTemplate,
-                                       name: template.name,
-                                       title: template.title,
+                                       annotations: template.annotations,
                                        description: template.description,
+                                       icons: template.icons,
                                        mimeType: template.mimeType,
+                                       name: template.name,
                                        serverName,
-                                       annotations: template.annotations,
-                                       icons: template.icons
+                                       title: template.title,
+                                       uriTemplate: template.uriTemplate
                                });
                        }
                }
@@ -258,9 +261,9 @@ class MCPResourceStore {
                }
 
                this._cachedResources.set(resource.uri, {
-                       resource,
                        content,
                        fetchedAt: new Date(),
+                       resource,
                        subscribed: this._subscriptions.has(resource.uri)
                });
                console.log(`[MCPResources] Cached content for: ${resource.uri}`);
@@ -271,6 +274,7 @@ class MCPResourceStore {
         */
        getCachedContent(uri: string): MCPCachedResource | undefined {
                const cached = this._cachedResources.get(uri);
+
                if (!cached) return undefined;
 
                // Check if cache is still valid
@@ -315,13 +319,14 @@ class MCPResourceStore {
         */
        addSubscription(uri: string, serverName: string): void {
                this._subscriptions.set(uri, {
-                       uri,
                        serverName,
-                       subscribedAt: new Date()
+                       subscribedAt: new Date(),
+                       uri
                });
 
                // Update cached resource if exists
                const cached = this._cachedResources.get(uri);
+
                if (cached) {
                        this._cachedResources.set(uri, { ...cached, subscribed: true });
                }
@@ -337,6 +342,7 @@ class MCPResourceStore {
 
                // Update cached resource if exists
                const cached = this._cachedResources.get(uri);
+
                if (cached) {
                        this._cachedResources.set(uri, { ...cached, subscribed: false });
                }
@@ -360,6 +366,7 @@ class MCPResourceStore {
 
                // Update subscription last update time
                const sub = this._subscriptions.get(uri);
+
                if (sub) {
                        this._subscriptions.set(uri, { ...sub, lastUpdate: new Date() });
                }
@@ -373,12 +380,14 @@ class MCPResourceStore {
        handleResourcesListChanged(serverName: string): void {
                // Mark server resources as needing refresh
                const existing = this._serverResources.get(serverName);
+
                if (existing) {
                        this._serverResources.set(serverName, {
                                ...existing,
                                lastFetched: undefined // Mark as stale
                        });
                }
+
                console.log(`[MCPResources][${serverName}] Resources list changed, needs refresh`);
        }
 
@@ -396,8 +405,8 @@ class MCPResourceStore {
        addAttachment(resource: MCPResourceInfo): MCPResourceAttachment {
                const attachment: MCPResourceAttachment = {
                        id: generateAttachmentId(),
-                       resource,
-                       loading: true
+                       loading: true,
+                       resource
                };
 
                this._attachments = [...this._attachments, attachment];
@@ -411,7 +420,7 @@ class MCPResourceStore {
         */
        updateAttachmentContent(attachmentId: string, content: MCPResourceContent[]): void {
                this._attachments = this._attachments.map((att) =>
-                       att.id === attachmentId ? { ...att, content, loading: false, error: undefined } : att
+                       att.id === attachmentId ? { ...att, content, error: undefined, loading: false } : att
                );
        }
 
@@ -420,7 +429,7 @@ class MCPResourceStore {
         */
        updateAttachmentError(attachmentId: string, error: string): void {
                this._attachments = this._attachments.map((att) =>
-                       att.id === attachmentId ? { ...att, loading: false, error } : att
+                       att.id === attachmentId ? { ...att, error, loading: false } : att
                );
        }
 
@@ -486,14 +495,14 @@ class MCPResourceStore {
 
                        if (resource) {
                                return {
-                                       uri: resource.uri,
-                                       name: resource.name,
-                                       title: resource.title,
+                                       annotations: resource.annotations,
                                        description: resource.description,
+                                       icons: resource.icons,
                                        mimeType: resource.mimeType,
+                                       name: resource.name,
                                        serverName,
-                                       annotations: resource.annotations,
-                                       icons: resource.icons
+                                       title: resource.title,
+                                       uri: resource.uri
                                };
                        }
                }
@@ -537,6 +546,7 @@ class MCPResourceStore {
 
                for (const attachment of this._attachments) {
                        if (attachment.error) continue;
+
                        if (!attachment.content || attachment.content.length === 0) continue;
 
                        const resourceName = attachment.resource.title || attachment.resource.name;
@@ -566,6 +576,7 @@ class MCPResourceStore {
 
                for (const attachment of this._attachments) {
                        if (attachment.error) continue;
+
                        if (!attachment.content || attachment.content.length === 0) continue;
 
                        const resourceName = attachment.resource.title || attachment.resource.name;
@@ -583,12 +594,12 @@ class MCPResourceStore {
 
                        if (contentParts.length > 0) {
                                extras.push({
-                                       type: AttachmentType.MCP_RESOURCE,
+                                       content: contentParts.join(NEWLINE),
+                                       mimeType: attachment.resource.mimeType,
                                        name: resourceName,
-                                       uri: attachment.resource.uri,
                                        serverName: attachment.resource.serverName,
-                                       content: contentParts.join(NEWLINE),
-                                       mimeType: attachment.resource.mimeType
+                                       type: AttachmentType.MCP_RESOURCE,
+                                       uri: attachment.resource.uri
                                });
                        }
                }
index f153edb25ea992a3dc3e6843473a4481156c8ac3..4582caf5ce186d2a23e35922679eb1eaffcb7a15 100644 (file)
  * @see MCPService in services/mcp.service.ts for protocol operations
  */
 
+import type { ListChangedHandlers } from '@modelcontextprotocol/sdk/types.js';
 import { browser } from '$app/environment';
 import { SETTINGS_KEYS } from '$lib/constants';
-import { MCPService } from '$lib/services/mcp.service';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
-import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
-import { serverStore } from '$lib/stores/server.svelte';
-import { mode } from 'mode-watcher';
-import {
-       parseMcpServerSettings,
-       detectMcpTransportFromUrl,
-       uuid,
-       extractRootDomain
-} from '$lib/utils';
-import {
-       MCPConnectionPhase,
-       MCPLogLevel,
-       HealthCheckStatus,
-       MCPRefType,
-       ColorMode,
-       UrlProtocol
-} from '$lib/enums';
 import {
        DEFAULT_CACHE_TTL_MS,
        DEFAULT_MCP_CONFIG,
        EXPECTED_THEMED_ICON_PAIR_COUNT,
        MCP_ALLOWED_ICON_MIME_TYPES,
-       MCP_SERVER_ID_PREFIX,
+       MCP_RECONNECT_ATTEMPT_TIMEOUT_MS,
        MCP_RECONNECT_BACKOFF_MULTIPLIER,
        MCP_RECONNECT_INITIAL_DELAY,
        MCP_RECONNECT_MAX_DELAY,
-       MCP_RECONNECT_ATTEMPT_TIMEOUT_MS
+       MCP_SERVER_ID_PREFIX
 } from '$lib/constants';
+import {
+       ColorMode,
+       HealthCheckStatus,
+       MCPConnectionPhase,
+       MCPLogLevel,
+       MCPRefType,
+       UrlProtocol
+} from '$lib/enums';
+import { MCPService } from '$lib/services/mcp.service';
+import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
+import { config, settingsStore } from '$lib/stores/settings.svelte';
 import type {
-       MCPToolCall,
-       ServerStatus,
-       ToolExecutionResult,
-       MCPClientConfig,
-       MCPConnection,
-       HealthCheckParams,
-       ServerCapabilities,
        ClientCapabilities,
+       GetPromptResult,
+       HealthCheckParams,
+       HealthCheckState,
        MCPCapabilitiesInfo,
+       MCPClientConfig,
+       MCPConnection,
        MCPConnectionLog,
        MCPPromptInfo,
-       GetPromptResult,
-       Tool,
-       HealthCheckState,
-       MCPServerSettingsEntry,
-       MCPServerDisplayInfo,
-       MCPServerConfig,
-       MCPResourceIcon,
        MCPResourceAttachment,
-       MCPResourceContent
+       MCPResourceContent,
+       MCPResourceIcon,
+       MCPServerConfig,
+       MCPServerDisplayInfo,
+       MCPServerSettingsEntry,
+       MCPToolCall,
+       ServerCapabilities,
+       ServerStatus,
+       Tool,
+       ToolExecutionResult
 } from '$lib/types';
-import type { ListChangedHandlers } from '@modelcontextprotocol/sdk/types.js';
 import type { DatabaseMessageExtraMcpResource, McpServerOverride } from '$lib/types/database';
 import type { SettingsConfigType } from '$lib/types/settings';
+import {
+       detectMcpTransportFromUrl,
+       extractRootDomain,
+       parseMcpServerSettings,
+       uuid
+} from '$lib/utils';
+import { mode } from 'mode-watcher';
 
 class MCPStore {
        private _isInitializing = $state(false);
@@ -119,8 +119,10 @@ class MCPStore {
                }
 
                let parsed: unknown;
+
                if (typeof rawServers === 'string') {
                        const trimmed = rawServers.trim();
+
                        if (!trimmed) {
                                return [];
                        }
@@ -135,6 +137,7 @@ class MCPStore {
                } else {
                        parsed = rawServers;
                }
+
                if (!Array.isArray(parsed)) {
                        return [];
                }
@@ -144,12 +147,12 @@ class MCPStore {
                        const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined;
 
                        return {
-                               id: this.#generateServerId((entry as { id?: unknown })?.id, index),
-                               enabled: Boolean((entry as { enabled?: unknown })?.enabled),
-                               url,
-                               name: (entry as { name?: string })?.name,
                                displayName: (entry as { displayName?: string })?.displayName,
+                               enabled: Boolean((entry as { enabled?: unknown })?.enabled),
                                headers: headers || undefined,
+                               id: this.#generateServerId((entry as { id?: unknown })?.id, index),
+                               name: (entry as { name?: string })?.name,
+                               url,
                                useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)
                        } satisfies MCPServerSettingsEntry;
                });
@@ -162,6 +165,7 @@ class MCPStore {
        #requestTimeoutMs(): number {
                const seconds =
                        Number(config().mcpRequestTimeoutSeconds) || DEFAULT_MCP_CONFIG.requestTimeoutSeconds;
+
                return Math.round(seconds * 1000);
        }
 
@@ -177,9 +181,11 @@ class MCPStore {
                }
 
                let headers: Record<string, string> | undefined;
+
                if (entry.headers) {
                        try {
                                const parsed = JSON.parse(entry.headers);
+
                                if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed))
                                        headers = parsed as Record<string, string>;
                        } catch {
@@ -188,11 +194,11 @@ class MCPStore {
                }
 
                return {
-                       url: entry.url,
-                       transport: detectMcpTransportFromUrl(entry.url),
                        handshakeTimeoutMs: connectionTimeoutMs,
-                       requestTimeoutMs: this.#requestTimeoutMs(),
                        headers,
+                       requestTimeoutMs: this.#requestTimeoutMs(),
+                       transport: detectMcpTransportFromUrl(entry.url),
+                       url: entry.url,
                        useProxy: entry.useProxy
                };
        }
@@ -210,6 +216,7 @@ class MCPStore {
                // server's own `enabled` flag so partial override lists are not all
                // treated as disabled.
                const override = perChatOverrides?.find((o) => o.serverId === server.id);
+
                return override?.enabled ?? server.enabled;
        }
 
@@ -221,6 +228,7 @@ class MCPStore {
                perChatOverrides?: McpServerOverride[]
        ): MCPClientConfig | undefined {
                const rawServers = this.#parseServerSettings(cfg.mcpServers);
+
                if (!rawServers.length) {
                        return undefined;
                }
@@ -229,7 +237,9 @@ class MCPStore {
 
                for (const [index, entry] of rawServers.entries()) {
                        if (!this.#checkServerEnabled(entry, perChatOverrides)) continue;
+
                        const normalized = this.#buildServerConfig(entry);
+
                        if (normalized) servers[this.#generateServerId(entry.id, index)] = normalized;
                }
 
@@ -238,9 +248,9 @@ class MCPStore {
                }
 
                return {
-                       protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
                        capabilities: DEFAULT_MCP_CONFIG.capabilities,
                        clientInfo: DEFAULT_MCP_CONFIG.clientInfo,
+                       protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
                        requestTimeoutMs: this.#requestTimeoutMs(),
                        servers
                };
@@ -254,26 +264,26 @@ class MCPStore {
                clientCaps?: ClientCapabilities
        ): MCPCapabilitiesInfo {
                return {
+                       client: {
+                               elicitation: clientCaps?.elicitation
+                                       ? { form: !!clientCaps.elicitation.form, url: !!clientCaps.elicitation.url }
+                                       : undefined,
+                               roots: clientCaps?.roots ? { listChanged: clientCaps.roots.listChanged } : undefined,
+                               sampling: !!clientCaps?.sampling,
+                               tasks: !!clientCaps?.tasks
+                       },
                        server: {
-                               tools: serverCaps?.tools ? { listChanged: serverCaps.tools.listChanged } : undefined,
+                               completions: !!serverCaps?.completions,
+                               logging: !!serverCaps?.logging,
                                prompts: serverCaps?.prompts ? { listChanged: serverCaps.prompts.listChanged } : undefined,
                                resources: serverCaps?.resources
                                        ? {
-                                                       subscribe: serverCaps.resources.subscribe,
-                                                       listChanged: serverCaps.resources.listChanged
+                                                       listChanged: serverCaps.resources.listChanged,
+                                                       subscribe: serverCaps.resources.subscribe
                                                }
                                        : undefined,
-                               logging: !!serverCaps?.logging,
-                               completions: !!serverCaps?.completions,
-                               tasks: !!serverCaps?.tasks
-                       },
-                       client: {
-                               roots: clientCaps?.roots ? { listChanged: clientCaps.roots.listChanged } : undefined,
-                               sampling: !!clientCaps?.sampling,
-                               elicitation: clientCaps?.elicitation
-                                       ? { form: !!clientCaps.elicitation.form, url: !!clientCaps.elicitation.url }
-                                       : undefined,
-                               tasks: !!clientCaps?.tasks
+                               tasks: !!serverCaps?.tasks,
+                               tools: serverCaps?.tools ? { listChanged: serverCaps.tools.listChanged } : undefined
                        }
                };
        }
@@ -304,6 +314,7 @@ class MCPStore {
 
        get isEnabled(): boolean {
                const mcpConfig = this.#buildMcpClientConfig(config());
+
                return (
                        mcpConfig !== null && mcpConfig !== undefined && Object.keys(mcpConfig.servers).length > 0
                );
@@ -353,6 +364,7 @@ class MCPStore {
 
        clearHealthCheck(serverId: string): void {
                const { [serverId]: _removed, ...rest } = this._healthChecks;
+
                this._healthChecks = rest;
        }
 
@@ -390,6 +402,7 @@ class MCPStore {
                        return (
                                healthState.serverInfo?.title || healthState.serverInfo?.name || server.name || server.url
                        );
+
                return server.name || server.url;
        }
 
@@ -420,6 +433,7 @@ class MCPStore {
         */
        getServerDisplayName(serverId: string): string {
                const server = this.getServerById(serverId);
+
                return server ? this.getServerLabel(server) : serverId;
        }
 
@@ -454,16 +468,18 @@ class MCPStore {
 
                const validIcons = icons.filter((icon) => {
                        if (!icon.src || !this.#isValidIconUri(icon.src)) return false;
+
                        if (icon.mimeType && !MCP_ALLOWED_ICON_MIME_TYPES.has(icon.mimeType)) return false;
+
                        return true;
                });
 
                if (validIcons.length === 0) return null;
 
                const preferredTheme = isDark ? ColorMode.DARK : ColorMode.LIGHT;
-
                // 1. Prefer icon explicitly matching the current color scheme
                const themedIcon = validIcons.find((icon) => icon.theme === preferredTheme);
+
                if (themedIcon) return themedIcon.src;
 
                // 2. Handle universal icons (no theme specified)
@@ -490,12 +506,14 @@ class MCPStore {
         */
        getServerFavicon(serverId: string): string | null {
                const server = this.getServerById(serverId);
+
                if (!server) {
                        return null;
                }
 
                const isDark = mode.current === ColorMode.DARK;
                const healthState = this.getHealthCheckState(serverId);
+
                if (healthState.status === HealthCheckStatus.SUCCESS && healthState.serverInfo?.icons) {
                        const mcpIconUrl = this.#getMcpIconUrl(healthState.serverInfo.icons, isDark);
 
@@ -515,6 +533,7 @@ class MCPStore {
                try {
                        const url = new URL(serverUrl);
                        const rootDomain = extractRootDomain(url);
+
                        if (!rootDomain) return null;
 
                        const origin = `${url.protocol}//${rootDomain}`;
@@ -522,6 +541,7 @@ class MCPStore {
 
                        for (const path of candidates) {
                                const faviconUrl = `${origin}/${path}`;
+
                                if (this.#isValidIconUri(faviconUrl)) {
                                        return faviconUrl;
                                }
@@ -538,20 +558,23 @@ class MCPStore {
        ): MCPServerSettingsEntry {
                const servers = this.getServers();
                const newServer: MCPServerSettingsEntry = {
-                       id: serverData.id || (uuid() ?? `server-${Date.now()}`),
-                       enabled: serverData.enabled,
-                       url: serverData.url.trim(),
-                       name: serverData.name,
                        displayName: serverData.displayName,
+                       enabled: serverData.enabled,
                        headers: serverData.headers?.trim() || undefined,
+                       id: serverData.id || (uuid() ?? `server-${Date.now()}`),
+                       name: serverData.name,
+                       url: serverData.url.trim(),
                        useProxy: serverData.useProxy
                };
+
                settingsStore.updateConfig(SETTINGS_KEYS.MCP_SERVERS, JSON.stringify([...servers, newServer]));
+
                return newServer;
        }
 
        updateServer(id: string, updates: Partial<MCPServerSettingsEntry>): void {
                const servers = this.getServers();
+
                settingsStore.updateConfig(
                        SETTINGS_KEYS.MCP_SERVERS,
                        JSON.stringify(
@@ -562,6 +585,7 @@ class MCPStore {
 
        removeServer(id: string): void {
                const servers = this.getServers();
+
                settingsStore.updateConfig(
                        SETTINGS_KEYS.MCP_SERVERS,
                        JSON.stringify(servers.filter((s) => s.id !== id))
@@ -591,11 +615,13 @@ class MCPStore {
 
                const mcpConfig = this.#buildMcpClientConfig(config(), perChatOverrides);
                const signature = mcpConfig ? JSON.stringify(mcpConfig) : null;
+
                if (!signature) {
                        await this.shutdown();
 
                        return false;
                }
+
                if (this.isInitialized && this.configSignature === signature) {
                        return true;
                }
@@ -605,20 +631,22 @@ class MCPStore {
                }
 
                if (this.connections.size > 0 || this.initPromise) await this.shutdown();
+
                return this.initialize(signature, mcpConfig!);
        }
 
        private async initialize(signature: string, mcpConfig: MCPClientConfig): Promise<boolean> {
-               this.updateState({ isInitializing: true, error: null });
+               this.updateState({ error: null, isInitializing: true });
                this.configSignature = signature;
 
                const serverEntries = Object.entries(mcpConfig.servers);
 
                if (serverEntries.length === 0) {
-                       this.updateState({ isInitializing: false, toolCount: 0, connectedServers: [] });
+                       this.updateState({ connectedServers: [], isInitializing: false, toolCount: 0 });
 
                        return false;
                }
+
                this.initPromise = this.doInitialize(signature, mcpConfig, serverEntries);
 
                return this.initPromise;
@@ -652,9 +680,10 @@ class MCPStore {
                                        listChangedHandlers
                                );
 
-                               return { name, connection };
+                               return { connection, name };
                        })
                );
+
                if (this.configSignature !== signature) {
                        for (const result of results) {
                                if (result.status === 'fulfilled')
@@ -663,9 +692,10 @@ class MCPStore {
 
                        return false;
                }
+
                for (const result of results) {
                        if (result.status === 'fulfilled') {
-                               const { name, connection } = result.value;
+                               const { connection, name } = result.value;
 
                                this.connections.set(name, connection);
 
@@ -674,6 +704,7 @@ class MCPStore {
                                                console.warn(
                                                        `[MCPStore] Tool name conflict: "${tool.name}" exists in "${this.toolsIndex.get(tool.name)}" and "${name}". Using tool from "${name}".`
                                                );
+
                                        this.toolsIndex.set(tool.name, name);
                                }
                        } else {
@@ -682,12 +713,13 @@ class MCPStore {
                }
 
                const successCount = this.connections.size;
+
                if (successCount === 0 && serverEntries.length > 0) {
                        this.updateState({
-                               isInitializing: false,
+                               connectedServers: [],
                                error: 'All MCP server connections failed',
-                               toolCount: 0,
-                               connectedServers: []
+                               isInitializing: false,
+                               toolCount: 0
                        });
                        this.initPromise = null;
 
@@ -695,10 +727,10 @@ class MCPStore {
                }
 
                this.updateState({
-                       isInitializing: false,
+                       connectedServers: Array.from(this.connections.keys()),
                        error: null,
-                       toolCount: this.toolsIndex.size,
-                       connectedServers: Array.from(this.connections.keys())
+                       isInitializing: false,
+                       toolCount: this.toolsIndex.size
                });
                this.initPromise = null;
 
@@ -707,21 +739,24 @@ class MCPStore {
 
        private createListChangedHandlers(serverName: string): ListChangedHandlers {
                return {
-                       tools: {
-                               onChanged: (error: Error | null, tools: Tool[] | null) => {
+                       prompts: {
+                               onChanged: (error: Error | null) => {
                                        if (error) {
-                                               console.warn(`[MCPStore][${serverName}] Tools list changed error:`, error);
+                                               console.warn(`[MCPStore][${serverName}] Prompts list changed error:`, error);
+
                                                return;
                                        }
-                                       this.handleToolsListChanged(serverName, tools ?? []);
                                }
                        },
-                       prompts: {
-                               onChanged: (error: Error | null) => {
+                       tools: {
+                               onChanged: (error: Error | null, tools: Tool[] | null) => {
                                        if (error) {
-                                               console.warn(`[MCPStore][${serverName}] Prompts list changed error:`, error);
+                                               console.warn(`[MCPStore][${serverName}] Tools list changed error:`, error);
+
                                                return;
                                        }
+
+                                       this.handleToolsListChanged(serverName, tools ?? []);
                                }
                        }
                };
@@ -729,6 +764,7 @@ class MCPStore {
 
        private handleToolsListChanged(serverName: string, tools: Tool[]): void {
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        return;
                }
@@ -744,6 +780,7 @@ class MCPStore {
                                console.warn(
                                        `[MCPStore] Tool name conflict after list change: "${tool.name}" exists in "${this.toolsIndex.get(tool.name)}" and "${serverName}". Using tool from "${serverName}".`
                                );
+
                        this.toolsIndex.set(tool.name, serverName);
                }
                this.updateState({ toolCount: this.toolsIndex.size });
@@ -760,6 +797,7 @@ class MCPStore {
         */
        async releaseConnection(shutdownIfUnused = false): Promise<void> {
                this.activeFlowCount = Math.max(0, this.activeFlowCount - 1);
+
                if (shutdownIfUnused && this.activeFlowCount === 0) {
                        await this.shutdown();
                }
@@ -792,10 +830,10 @@ class MCPStore {
                this.serverConfigs.clear();
                this.configSignature = null;
                this.updateState({
-                       isInitializing: false,
+                       connectedServers: [],
                        error: null,
-                       toolCount: 0,
-                       connectedServers: []
+                       isInitializing: false,
+                       toolCount: 0
                });
        }
 
@@ -810,12 +848,14 @@ class MCPStore {
         */
        private async reconnectServer(serverName: string): Promise<void> {
                const serverConfig = this.serverConfigs.get(serverName);
+
                if (!serverConfig) {
                        throw new Error(`[MCPStore] No config found for ${serverName}, cannot reconnect`);
                }
 
                // Disconnect stale connection (clears old transport + session ID)
                const oldConnection = this.connections.get(serverName);
+
                if (oldConnection) {
                        await MCPService.disconnect(oldConnection).catch(console.warn);
                        this.connections.delete(serverName);
@@ -869,6 +909,7 @@ class MCPStore {
                }
 
                const serverConfig = this.serverConfigs.get(serverName);
+
                if (!serverConfig) {
                        console.error(`[MCPStore] No config found for ${serverName}, cannot reconnect`);
 
@@ -924,7 +965,6 @@ class MCPStore {
                                                },
                                                listChangedHandlers
                                        );
-
                                        const connection = await Promise.race([connectPromise, timeoutPromise]);
 
                                        // Replace old connection with new one
@@ -936,6 +976,7 @@ class MCPStore {
                                        }
 
                                        console.log(`[MCPStore][${serverName}] Reconnected successfully`);
+
                                        break;
                                } catch (error) {
                                        console.warn(`[MCPStore][${serverName}] Reconnection failed:`, error);
@@ -944,6 +985,7 @@ class MCPStore {
                        }
                } finally {
                        this.reconnectingServers.delete(serverName);
+
                        // If the phase callback signalled a disconnect while this function held
                        // the guard, kick off a fresh reconnect now that the guard is released.
                        if (needsReconnect) {
@@ -976,11 +1018,14 @@ class MCPStore {
         */
        findServerForTool(toolName: string): string | undefined {
                const fromIndex = this.toolsIndex.get(toolName);
+
                if (fromIndex) return fromIndex;
 
                for (const server of this.getServers()) {
                        const health = this._healthChecks[server.id];
+
                        if (!health || health.status !== HealthCheckStatus.SUCCESS) continue;
+
                        if (health.tools.some((tool) => tool.name === toolName)) {
                                return server.id;
                        }
@@ -997,8 +1042,11 @@ class MCPStore {
         */
        getServerFaviconForTool(toolName: string | undefined): string | null {
                if (!toolName) return null;
+
                const serverId = this.findServerForTool(toolName);
+
                if (!serverId) return null;
+
                return this.getServerFavicon(serverId);
        }
 
@@ -1039,6 +1087,7 @@ class MCPStore {
 
                for (const [serverId, state] of Object.entries(this._healthChecks)) {
                        if (!enabledServerIds.has(serverId)) continue;
+
                        if (
                                state.status === HealthCheckStatus.SUCCESS &&
                                state.capabilities?.server?.prompts !== undefined
@@ -1049,6 +1098,7 @@ class MCPStore {
 
                for (const [serverName, connection] of this.connections) {
                        if (!enabledServerIds.has(serverName)) continue;
+
                        if (connection.serverCapabilities?.prompts) {
                                return true;
                        }
@@ -1067,15 +1117,15 @@ class MCPStore {
 
                        for (const prompt of prompts) {
                                results.push({
-                                       name: prompt.name,
-                                       description: prompt.description,
-                                       title: prompt.title,
-                                       serverName,
                                        arguments: prompt.arguments?.map((arg) => ({
-                                               name: arg.name,
                                                description: arg.description,
+                                               name: arg.name,
                                                required: arg.required
-                                       }))
+                                       })),
+                                       description: prompt.description,
+                                       name: prompt.name,
+                                       serverName,
+                                       title: prompt.title
                                });
                        }
                }
@@ -1089,6 +1139,7 @@ class MCPStore {
                args?: Record<string, string>
        ): Promise<GetPromptResult> {
                const connection = this.connections.get(serverName);
+
                if (!connection) throw new Error(`Server "${serverName}" not found for prompt "${promptName}"`);
 
                return MCPService.getPrompt(connection, promptName, args);
@@ -1096,26 +1147,28 @@ class MCPStore {
 
        async executeTool(toolCall: MCPToolCall, signal?: AbortSignal): Promise<ToolExecutionResult> {
                const toolName = toolCall.function.name;
-
                const serverName = this.toolsIndex.get(toolName);
+
                if (!serverName) throw new Error(`Unknown tool: ${toolName}`);
 
                const connection = this.connections.get(serverName);
+
                if (!connection) throw new Error(`Server "${serverName}" is not connected`);
 
                const args = this.parseToolArguments(toolCall.function.arguments);
 
                try {
-                       return await MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
+                       return await MCPService.callTool(connection, { arguments: args, name: toolName }, signal);
                } catch (error) {
                        // Session expired (server restarted) - reconnect and retry once
                        if (MCPService.isSessionExpiredError(error)) {
                                await this.reconnectServer(serverName);
 
                                const newConnection = this.connections.get(serverName);
+
                                if (!newConnection) throw new Error(`Failed to reconnect to "${serverName}"`);
 
-                               return MCPService.callTool(newConnection, { name: toolName, arguments: args }, signal);
+                               return MCPService.callTool(newConnection, { arguments: args, name: toolName }, signal);
                        }
 
                        throw error;
@@ -1128,20 +1181,24 @@ class MCPStore {
                signal?: AbortSignal
        ): Promise<ToolExecutionResult> {
                const serverName = this.toolsIndex.get(toolName);
+
                if (!serverName) throw new Error(`Unknown tool: ${toolName}`);
+
                const connection = this.connections.get(serverName);
+
                if (!connection) throw new Error(`Server "${serverName}" is not connected`);
 
                try {
-                       return await MCPService.callTool(connection, { name: toolName, arguments: args }, signal);
+                       return await MCPService.callTool(connection, { arguments: args, name: toolName }, signal);
                } catch (error) {
                        if (MCPService.isSessionExpiredError(error)) {
                                await this.reconnectServer(serverName);
 
                                const newConnection = this.connections.get(serverName);
+
                                if (!newConnection) throw new Error(`Failed to reconnect to "${serverName}"`);
 
-                               return MCPService.callTool(newConnection, { name: toolName, arguments: args }, signal);
+                               return MCPService.callTool(newConnection, { arguments: args, name: toolName }, signal);
                        }
 
                        throw error;
@@ -1151,12 +1208,14 @@ class MCPStore {
        private parseToolArguments(args: string | Record<string, unknown>): Record<string, unknown> {
                if (typeof args === 'string') {
                        const trimmed = args.trim();
+
                        if (trimmed === '') {
                                return {};
                        }
 
                        try {
                                const parsed = JSON.parse(trimmed);
+
                                if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
                                        throw new Error(
                                                `Tool arguments must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`
@@ -1182,17 +1241,20 @@ class MCPStore {
                argumentValue: string
        ): Promise<{ values: string[]; total?: number; hasMore?: boolean } | null> {
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        console.warn(`[MCPStore] Server "${serverName}" is not connected`);
+
                        return null;
                }
+
                if (!connection.serverCapabilities?.completions) {
                        return null;
                }
 
                return MCPService.complete(
                        connection,
-                       { type: MCPRefType.PROMPT, name: promptName },
+                       { name: promptName, type: MCPRefType.PROMPT },
                        { name: argumentName, value: argumentValue }
                );
        }
@@ -1211,6 +1273,7 @@ class MCPStore {
 
                if (!connection) {
                        console.warn(`[MCPStore] Server "${serverName}" is not connected`);
+
                        return null;
                }
 
@@ -1256,6 +1319,7 @@ class MCPStore {
 
                try {
                        const parsed = JSON.parse(headersJson);
+
                        if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed))
                                return parsed as Record<string, string>;
                } catch {
@@ -1284,8 +1348,10 @@ class MCPStore {
                }
 
                const BATCH_SIZE = 5;
+
                for (let i = 0; i < serversToCheck.length; i += BATCH_SIZE) {
                        const batch = serversToCheck.slice(i, i + BATCH_SIZE);
+
                        await Promise.allSettled(batch.map((server) => this.runHealthCheck(server, promoteToActive)));
                }
        }
@@ -1307,6 +1373,7 @@ class MCPStore {
        async runHealthCheck(server: HealthCheckParams, promoteToActive = false): Promise<void> {
                // Check if we already have an active connection for this server
                const existingConnection = this.connections.get(server.id);
+
                if (existingConnection) {
                        // Reuse existing connection - just refresh tools list
                        try {
@@ -1315,21 +1382,23 @@ class MCPStore {
                                        existingConnection.serverCapabilities,
                                        existingConnection.clientCapabilities
                                );
+
                                this.updateHealthCheck(server.id, {
+                                       capabilities,
+                                       connectionTimeMs: existingConnection.connectionTimeMs,
+                                       instructions: existingConnection.instructions,
+                                       logs: [],
+                                       protocolVersion: existingConnection.protocolVersion,
+                                       serverInfo: existingConnection.serverInfo,
                                        status: HealthCheckStatus.SUCCESS,
                                        tools: tools.map((tool) => ({
-                                               name: tool.name,
                                                description: tool.description,
+                                               name: tool.name,
                                                title: tool.title
                                        })),
-                                       serverInfo: existingConnection.serverInfo,
-                                       capabilities,
-                                       transportType: existingConnection.transportType,
-                                       protocolVersion: existingConnection.protocolVersion,
-                                       instructions: existingConnection.instructions,
-                                       connectionTimeMs: existingConnection.connectionTimeMs,
-                                       logs: []
+                                       transportType: existingConnection.transportType
                                });
+
                                return;
                        } catch (error) {
                                console.warn(
@@ -1343,21 +1412,23 @@ class MCPStore {
 
                const trimmedUrl = server.url.trim();
                const logs: MCPConnectionLog[] = [];
+
                let currentPhase: MCPConnectionPhase = MCPConnectionPhase.IDLE;
 
                if (!trimmedUrl) {
                        this.updateHealthCheck(server.id, {
-                               status: HealthCheckStatus.ERROR,
+                               logs: [],
                                message: 'Please enter a server URL first.',
-                               logs: []
+                               status: HealthCheckStatus.ERROR
                        });
+
                        return;
                }
 
                this.updateHealthCheck(server.id, {
-                       status: HealthCheckStatus.CONNECTING,
+                       logs: [],
                        phase: MCPConnectionPhase.TRANSPORT_CREATING,
-                       logs: []
+                       status: HealthCheckStatus.CONNECTING
                });
 
                const timeoutMs = this.#requestTimeoutMs();
@@ -1365,11 +1436,11 @@ class MCPStore {
 
                try {
                        const serverConfig: MCPServerConfig = {
-                               url: trimmedUrl,
-                               transport: detectMcpTransportFromUrl(trimmedUrl),
                                handshakeTimeoutMs: DEFAULT_MCP_CONFIG.connectionTimeoutMs,
-                               requestTimeoutMs: timeoutMs,
                                headers,
+                               requestTimeoutMs: timeoutMs,
+                               transport: detectMcpTransportFromUrl(trimmedUrl),
+                               url: trimmedUrl,
                                useProxy: server.useProxy
                        };
 
@@ -1385,9 +1456,9 @@ class MCPStore {
                                        currentPhase = phase;
                                        logs.push(log);
                                        this.updateHealthCheck(server.id, {
-                                               status: HealthCheckStatus.CONNECTING,
+                                               logs: [...logs],
                                                phase,
-                                               logs: [...logs]
+                                               status: HealthCheckStatus.CONNECTING
                                        });
 
                                        // Handle WebSocket disconnection
@@ -1399,28 +1470,26 @@ class MCPStore {
                                        }
                                }
                        );
-
                        const tools = connection.tools.map((tool) => ({
-                               name: tool.name,
                                description: tool.description,
+                               name: tool.name,
                                title: tool.title
                        }));
-
                        const capabilities = this.#buildCapabilitiesInfo(
                                connection.serverCapabilities,
                                connection.clientCapabilities
                        );
 
                        this.updateHealthCheck(server.id, {
-                               status: HealthCheckStatus.SUCCESS,
-                               tools,
-                               serverInfo: connection.serverInfo,
                                capabilities,
-                               transportType: connection.transportType,
-                               protocolVersion: connection.protocolVersion,
-                               instructions: connection.instructions,
                                connectionTimeMs: connection.connectionTimeMs,
-                               logs
+                               instructions: connection.instructions,
+                               logs,
+                               protocolVersion: connection.protocolVersion,
+                               serverInfo: connection.serverInfo,
+                               status: HealthCheckStatus.SUCCESS,
+                               tools,
+                               transportType: connection.transportType
                        });
 
                        // Promote to active connection or disconnect
@@ -1434,18 +1503,18 @@ class MCPStore {
 
                        if (logs.at(-1)?.phase !== MCPConnectionPhase.ERROR) {
                                logs.push({
-                                       timestamp: new Date(),
-                                       phase: MCPConnectionPhase.ERROR,
+                                       level: MCPLogLevel.ERROR,
                                        message: `Connection failed: ${message}`,
-                                       level: MCPLogLevel.ERROR
+                                       phase: MCPConnectionPhase.ERROR,
+                                       timestamp: new Date()
                                });
                        }
 
                        this.updateHealthCheck(server.id, {
-                               status: HealthCheckStatus.ERROR,
+                               logs,
                                message,
                                phase: currentPhase,
-                               logs
+                               status: HealthCheckStatus.ERROR
                        });
                }
        }
@@ -1462,6 +1531,7 @@ class MCPStore {
                                        `[MCPStore] Tool name conflict during promotion: "${tool.name}" exists in "${this.toolsIndex.get(tool.name)}" and "${serverId}". Using tool from "${serverId}".`
                                );
                        }
+
                        this.toolsIndex.set(tool.name, serverId);
                }
 
@@ -1470,8 +1540,8 @@ class MCPStore {
 
                // Update state
                this.updateState({
-                       toolCount: this.toolsIndex.size,
-                       connectedServers: Array.from(this.connections.keys())
+                       connectedServers: Array.from(this.connections.keys()),
+                       toolCount: this.toolsIndex.size
                });
        }
 
@@ -1480,10 +1550,10 @@ class MCPStore {
 
                for (const [name, connection] of this.connections) {
                        statuses.push({
-                               name,
+                               error: undefined,
                                isConnected: true,
-                               toolCount: connection.tools.length,
-                               error: undefined
+                               name,
+                               toolCount: connection.tools.length
                        });
                }
 
@@ -1504,9 +1574,9 @@ class MCPStore {
                for (const [serverName, connection] of this.connections) {
                        if (connection.instructions) {
                                results.push({
+                                       instructions: connection.instructions,
                                        serverName,
-                                       serverTitle: connection.serverInfo?.title || connection.serverInfo?.name,
-                                       instructions: connection.instructions
+                                       serverTitle: connection.serverInfo?.title || connection.serverInfo?.name
                                });
                        }
                }
@@ -1528,9 +1598,9 @@ class MCPStore {
                for (const [serverId, state] of Object.entries(this._healthChecks)) {
                        if (state.status === HealthCheckStatus.SUCCESS && state.instructions) {
                                results.push({
+                                       instructions: state.instructions,
                                        serverId,
-                                       serverTitle: state.serverInfo?.title || state.serverInfo?.name,
-                                       instructions: state.instructions
+                                       serverTitle: state.serverInfo?.title || state.serverInfo?.name
                                });
                        }
                }
@@ -1579,12 +1649,14 @@ class MCPStore {
                                        .map((s) => s.id)
                        );
                }
+
                if (enabledServerIds.size === 0) {
                        return false;
                }
 
                for (const [serverId, state] of Object.entries(this._healthChecks)) {
                        if (!enabledServerIds.has(serverId)) continue;
+
                        if (
                                state.status === HealthCheckStatus.SUCCESS &&
                                state.capabilities?.server?.resources !== undefined
@@ -1595,6 +1667,7 @@ class MCPStore {
 
                for (const [serverName, connection] of this.connections) {
                        if (!enabledServerIds.has(serverName)) continue;
+
                        if (MCPService.supportsResources(connection)) {
                                return true;
                        }
@@ -1618,6 +1691,7 @@ class MCPStore {
                // Check active connections
                for (const [name, connection] of this.connections) {
                        if (!enabledServerIds.has(name)) continue;
+
                        if (MCPService.supportsResources(connection) && !servers.includes(name)) {
                                servers.push(name);
                        }
@@ -1626,6 +1700,7 @@ class MCPStore {
                // Also check health check states for servers not yet connected
                for (const [serverId, state] of Object.entries(this._healthChecks)) {
                        if (!enabledServerIds.has(serverId)) continue;
+
                        if (
                                !servers.includes(serverId) &&
                                state.status === HealthCheckStatus.SUCCESS &&
@@ -1645,6 +1720,7 @@ class MCPStore {
         */
        async fetchAllResources(forceRefresh: boolean = false): Promise<void> {
                const serversWithResources = this.getServersWithResources();
+
                if (serversWithResources.length === 0) {
                        return;
                }
@@ -1653,6 +1729,7 @@ class MCPStore {
                if (!forceRefresh) {
                        const allServersCached = serversWithResources.every((serverName) => {
                                const serverRes = mcpResourceStore.getServerResources(serverName);
+
                                if (!serverRes || !serverRes.lastFetched) {
                                        return false;
                                }
@@ -1687,8 +1764,10 @@ class MCPStore {
         */
        async fetchServerResources(serverName: string): Promise<void> {
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        console.warn(`[MCPStore] No connection found for server: ${serverName}`);
+
                        return;
                }
 
@@ -1707,6 +1786,7 @@ class MCPStore {
                        mcpResourceStore.setServerResources(serverName, resources, templates);
                } catch (error) {
                        const message = error instanceof Error ? error.message : String(error);
+
                        mcpResourceStore.setServerError(serverName, message);
                        console.error(`[MCPStore][${serverName}] Failed to fetch resources:`, error);
                }
@@ -1719,12 +1799,14 @@ class MCPStore {
        async readResource(uri: string): Promise<MCPResourceContent[] | null> {
                // Check cache first
                const cached = mcpResourceStore.getCachedContent(uri);
+
                if (cached) {
                        return cached.content;
                }
 
                // Find which server has this resource
                const serverName = mcpResourceStore.findServerForUri(uri);
+
                if (!serverName) {
                        console.error(`[MCPStore] No server found for resource URI: ${uri}`);
 
@@ -1732,6 +1814,7 @@ class MCPStore {
                }
 
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        console.error(`[MCPStore] No connection found for server: ${serverName}`);
 
@@ -1759,6 +1842,7 @@ class MCPStore {
         */
        async subscribeToResource(uri: string): Promise<boolean> {
                const serverName = mcpResourceStore.findServerForUri(uri);
+
                if (!serverName) {
                        console.error(`[MCPStore] No server found for resource URI: ${uri}`);
 
@@ -1766,6 +1850,7 @@ class MCPStore {
                }
 
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        console.error(`[MCPStore] No connection found for server: ${serverName}`);
 
@@ -1793,6 +1878,7 @@ class MCPStore {
         */
        async unsubscribeFromResource(uri: string): Promise<boolean> {
                const serverName = mcpResourceStore.findServerForUri(uri);
+
                if (!serverName) {
                        console.error(`[MCPStore] No server found for resource URI: ${uri}`);
 
@@ -1800,6 +1886,7 @@ class MCPStore {
                }
 
                const connection = this.connections.get(serverName);
+
                if (!connection) {
                        console.error(`[MCPStore] No connection found for server: ${serverName}`);
 
@@ -1824,6 +1911,7 @@ class MCPStore {
         */
        async attachResource(uri: string): Promise<MCPResourceAttachment | null> {
                const resourceInfo = mcpResourceStore.findResourceByUri(uri);
+
                if (!resourceInfo) {
                        console.error(`[MCPStore] Resource not found: ${uri}`);
 
@@ -1849,6 +1937,7 @@ class MCPStore {
                        }
                } catch (error) {
                        const message = error instanceof Error ? error.message : String(error);
+
                        mcpResourceStore.updateAttachmentError(attachment.id, message);
                }
 
@@ -1882,9 +1971,11 @@ class MCPStore {
         */
        consumeResourceAttachmentsAsExtras(): DatabaseMessageExtraMcpResource[] {
                const extras = mcpResourceStore.toMessageExtras();
+
                if (extras.length > 0) {
                        mcpResourceStore.clearAttachments();
                }
+
                return extras;
        }
 }
index dc6c01db276239ee9005ac56293c0b132487a90b..145de119ce7f3396fd213f2921b255d7a6163c5f 100644 (file)
@@ -1,31 +1,30 @@
 import { base } from '$app/paths';
-import { SvelteMap, SvelteSet } from 'svelte/reactivity';
-import { toast } from 'svelte-sonner';
 import {
-       ServerModelStatus,
-       ServerModelsSseEventType,
+       API_MODELS,
+       FAVORITE_MODELS_LOCALSTORAGE_KEY,
+       MODEL_PROPS_CACHE_MAX_ENTRIES,
+       MODEL_PROPS_CACHE_TTL_MS,
+       SSE_DATA_PREFIX,
+       SSE_LINE_SEPARATOR,
+       SSE_RECORD_SEPARATOR
+} from '$lib/constants';
+import {
+       FileTypeCategory,
        ModelModality,
-       FileTypeCategory
+       ServerModelsSseEventType,
+       ServerModelStatus
 } from '$lib/enums';
 import { ModelsService } from '$lib/services/models.service';
 import { PropsService } from '$lib/services/props.service';
-import { serverStore, isRouterMode } from '$lib/stores/server.svelte';
+import { conversationsStore } from '$lib/stores/conversations.svelte';
+import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
+import { getAuthHeaders, TTLCache } from '$lib/utils';
 import {
        detectThinkingSupport,
        detectThinkingSupportWithReason
 } from '$lib/utils/chat-template-thinking-detector';
-import { TTLCache, getAuthHeaders } from '$lib/utils';
-import {
-       MODEL_PROPS_CACHE_TTL_MS,
-       MODEL_PROPS_CACHE_MAX_ENTRIES,
-       FAVORITE_MODELS_LOCALSTORAGE_KEY,
-       API_MODELS,
-       SSE_RECORD_SEPARATOR,
-       SSE_LINE_SEPARATOR,
-       SSE_DATA_PREFIX
-} from '$lib/constants';
-
-import { conversationsStore } from '$lib/stores/conversations.svelte';
+import { SvelteMap, SvelteSet } from 'svelte/reactivity';
+import { toast } from 'svelte-sonner';
 
 /**
  * modelsStore - Reactive store for model management in both MODEL and ROUTER modes.
@@ -82,8 +81,8 @@ class ModelsStore {
         * TTL: 10 minutes — props don't change frequently.
         */
        private modelPropsCache = new TTLCache<string, ApiLlamaCppServerProps>({
-               ttlMs: MODEL_PROPS_CACHE_TTL_MS,
-               maxEntries: MODEL_PROPS_CACHE_MAX_ENTRIES
+               maxEntries: MODEL_PROPS_CACHE_MAX_ENTRIES,
+               ttlMs: MODEL_PROPS_CACHE_TTL_MS
        });
        private modelPropsFetching = $state<Set<string>>(new Set());
 
@@ -102,6 +101,7 @@ class ModelsStore {
 
        get selectedModel(): ModelOption | null {
                if (!this.selectedModelId) return null;
+
                return this.models.find((m) => m.id === this.selectedModelId) ?? null;
        }
 
@@ -130,7 +130,9 @@ class ModelsStore {
                if (isRouterMode()) return null;
 
                const props = serverStore.props;
+
                if (props?.model_alias) return props.model_alias;
+
                if (!props?.model_path) return null;
 
                return props.model_path.split(/(\\|\/)/).pop() || null;
@@ -138,6 +140,7 @@ class ModelsStore {
 
        get selectedModelContextSize(): number | null {
                if (!this.selectedModelName) return null;
+
                return this.getModelContextSize(this.selectedModelName);
        }
 
@@ -155,11 +158,13 @@ class ModelsStore {
                }
 
                const model = this.models.find((m) => m.model === modelId || m.id === modelId);
+
                if (model?.modalities) {
                        return model.modalities;
                }
 
                const props = this.modelPropsCache.get(modelId);
+
                if (props?.modalities) {
                        return this.buildModalities(props.modalities);
                }
@@ -181,11 +186,15 @@ class ModelsStore {
 
        getModelModalitiesArray(modelId: string): ModelModality[] {
                const modalities = this.getModelModalities(modelId);
+
                if (!modalities) return [];
 
                const result: ModelModality[] = [];
+
                if (modalities.vision) result.push(ModelModality.VISION);
+
                if (modalities.audio) result.push(ModelModality.AUDIO);
+
                if (modalities.video) result.push(ModelModality.VIDEO);
 
                return result;
@@ -261,12 +270,15 @@ class ModelsStore {
                }
 
                const modelId = this.selectedModelName;
+
                if (!modelId) return false;
 
                if (!this.modelPropsCache.get(modelId)) {
                        this.fetchModelProps(modelId);
                }
+
                const props = this.getModelProps(modelId);
+
                return detectThinkingSupport(props?.chat_template ?? '');
        }
 
@@ -287,6 +299,7 @@ class ModelsStore {
                }
 
                const props = this.getModelProps(modelId);
+
                return detectThinkingSupport(props?.chat_template ?? '');
        }
 
@@ -299,14 +312,17 @@ class ModelsStore {
                }
 
                const modelId = this.selectedModelName;
+
                if (!modelId) {
-                       return { supported: false, reason: 'No model selected' };
+                       return { reason: 'No model selected', supported: false };
                }
 
                if (!this.modelPropsCache.get(modelId)) {
                        this.fetchModelProps(modelId);
                }
+
                const props = this.getModelProps(modelId);
+
                return detectThinkingSupportWithReason(props?.chat_template ?? '');
        }
 
@@ -324,6 +340,7 @@ class ModelsStore {
         */
        async fetch(force = false): Promise<void> {
                if (this.inflightFetch) return this.inflightFetch;
+
                if (this.models.length > 0 && !force) return;
 
                this.inflightFetch = this.runFetch();
@@ -394,16 +411,16 @@ class ModelsStore {
                        const modelId = details?.model || item.id;
 
                        return {
-                               id: item.id,
-                               name: this.toDisplayName(displayNameSource),
-                               model: modelId,
-                               description: details?.description,
+                               aliases: item.aliases ?? [],
                                capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value)),
-                               modalities: this.buildArchitectureModalities(item.architecture),
+                               description: details?.description,
                                details: details?.details,
+                               id: item.id,
                                meta: item.meta ?? null,
+                               modalities: this.buildArchitectureModalities(item.architecture),
+                               model: modelId,
+                               name: this.toDisplayName(displayNameSource),
                                parsedId: ModelsService.parseModelId(modelId),
-                               aliases: item.aliases ?? [],
                                tags: item.tags ?? []
                        };
                });
@@ -419,10 +436,12 @@ class ModelsStore {
 
                try {
                        const response = await ModelsService.listRouter();
+
                        this.routerModels = response.data;
                        await this.fetchModalitiesForLoadedModels();
 
                        const visible = this.getVisibleModels();
+
                        if (visible.length === 1 && this.isModelLoaded(visible[0].model)) {
                                this.selectModelById(visible[0].id);
                        }
@@ -444,6 +463,7 @@ class ModelsStore {
         */
        async fetchModelProps(modelId: string): Promise<ApiLlamaCppServerProps | null> {
                const cached = this.modelPropsCache.get(modelId);
+
                if (cached) return cached;
 
                if (serverStore.isRouterMode && !this.isModelLoaded(modelId)) {
@@ -456,11 +476,14 @@ class ModelsStore {
 
                try {
                        const props = await PropsService.fetchForModel(modelId);
+
                        this.modelPropsCache.set(modelId, props);
                        this.propsCacheVersion++;
+
                        return props;
                } catch (error) {
                        console.warn(`Failed to fetch props for model ${modelId}:`, error);
+
                        return null;
                } finally {
                        this.modelPropsFetching.delete(modelId);
@@ -470,6 +493,7 @@ class ModelsStore {
        /** Fetch modalities for all loaded models from /props endpoint. */
        async fetchModalitiesForLoadedModels(): Promise<void> {
                const loadedModelIds = this.loadedModelIds;
+
                if (loadedModelIds.length === 0) return;
 
                const propsPromises = loadedModelIds.map((modelId) => this.fetchModelProps(modelId));
@@ -479,9 +503,11 @@ class ModelsStore {
 
                        this.models = this.models.map((model) => {
                                const modelIndex = loadedModelIds.indexOf(model.model);
+
                                if (modelIndex === -1) return model;
 
                                const props = results[modelIndex];
+
                                if (!props?.modalities) return model;
 
                                return { ...model, modalities: this.buildModalities(props.modalities) };
@@ -499,6 +525,7 @@ class ModelsStore {
         */
        async updateModelModalities(modelId: string): Promise<void> {
                const props = await this.fetchModelProps(modelId);
+
                if (!props?.modalities) return;
 
                this.models = this.models.map((model) =>
@@ -523,6 +550,7 @@ class ModelsStore {
         */
        getModelFromLastAssistantResponse(): string | null {
                const messages = conversationsStore.activeMessages;
+
                if (!messages || messages.length === 0) return null;
 
                for (let i = messages.length - 1; i >= 0; i--) {
@@ -540,17 +568,21 @@ class ModelsStore {
         */
        async selectModelFromLastAssistantResponse(): Promise<boolean> {
                const lastModel = this.getModelFromLastAssistantResponse();
+
                if (!lastModel || this.selectedModelName === lastModel) return false;
 
                const matchingModel = this.models.find((option) => option.model === lastModel);
+
                if (!matchingModel || !this.isModelLoaded(lastModel)) return false;
 
                try {
                        await this.selectModelById(matchingModel.id);
                        console.log(`[modelsStore] Automatically selected model: ${lastModel} from last message`);
+
                        return true;
                } catch (error) {
                        console.warn('[modelsStore] Failed to automatically select model from last message:', error);
+
                        return false;
                }
        }
@@ -568,33 +600,42 @@ class ModelsStore {
                if (this.selectedModelName) return;
 
                const availableModels = this.getVisibleModels();
+
                if (availableModels.length === 0) return;
 
                // Try to select model from last assistant response first
                const lastModel = this.getModelFromLastAssistantResponse();
+
                if (lastModel) {
                        const lastModelOption = availableModels.find((m) => m.model === lastModel);
+
                        if (lastModelOption) {
                                await this.selectModelById(lastModelOption.id);
+
                                if (this.isModelLoaded(lastModel)) {
                                        await this.fetchModelProps(lastModel);
                                }
+
                                return;
                        }
                }
 
                // Try a loaded model first
                const loadedModel = availableModels.find((m) => this.isModelLoaded(m.model));
+
                if (loadedModel) {
                        await this.selectModelById(loadedModel.id);
                        await this.fetchModelProps(loadedModel.model);
+
                        return;
                }
 
                // Try loading a favorite model
                const favorite = this.favoriteModelIds.values().next()?.value;
+
                if (favorite) {
                        await this.selectModelById(favorite);
+
                        return;
                }
 
@@ -612,9 +653,11 @@ class ModelsStore {
 
        async selectModelById(modelId: string): Promise<void> {
                if (!modelId || this.updating) return;
+
                if (this.selectedModelId === modelId) return;
 
                const option = this.models.find((model) => model.id === modelId);
+
                if (!option) throw new Error('Selected model is not available');
 
                this.updating = true;
@@ -633,6 +676,7 @@ class ModelsStore {
         */
        selectModelByName(modelName: string): void {
                const option = this.models.find((model) => model.model === modelName);
+
                if (option) {
                        this.selectedModelId = option.id;
                        this.selectedModelName = option.model;
@@ -679,6 +723,7 @@ class ModelsStore {
         */
        subscribeStatus(): void {
                if (this.statusReaderActive) return;
+
                if (!isRouterMode()) return;
 
                this.statusReaderActive = true;
@@ -719,15 +764,18 @@ class ModelsStore {
 
                                if (response.ok && response.body) {
                                        const reader = response.body.getReader();
+
                                        let buffer = '';
 
                                        while (!signal.aborted) {
-                                               const { value, done } = await reader.read();
+                                               const { done, value } = await reader.read();
+
                                                if (done) break;
 
                                                buffer += decoder.decode(value, { stream: true });
 
                                                let boundary = buffer.indexOf(SSE_RECORD_SEPARATOR);
+
                                                while (boundary !== -1) {
                                                        this.handleStatusRecord(buffer.slice(0, boundary));
                                                        buffer = buffer.slice(boundary + SSE_RECORD_SEPARATOR.length);
@@ -759,6 +807,7 @@ class ModelsStore {
                if (payload.length === 0) return;
 
                let envelope: ApiModelsSseEvent;
+
                try {
                        envelope = JSON.parse(payload);
                } catch {
@@ -779,12 +828,15 @@ class ModelsStore {
                        case ServerModelsSseEventType.MODEL_STATUS:
                        case ServerModelsSseEventType.STATUS_UPDATE:
                                this.applyModelStatus(event);
+
                                break;
                        case ServerModelsSseEventType.MODELS_RELOAD:
                                void this.fetchRouterModels();
+
                                break;
                        case ServerModelsSseEventType.MODEL_REMOVE:
                                this.removeRouterModel(event.model);
+
                                break;
                        case ServerModelsSseEventType.DOWNLOAD_PROGRESS:
                                break;
@@ -798,6 +850,7 @@ class ModelsStore {
        private applyModelStatus(event: ApiModelsSseEvent): void {
                const model = event.model;
                const data = event.data;
+
                if (!model || !data?.status) return;
 
                const status = data.status;
@@ -820,6 +873,7 @@ class ModelsStore {
 
                if (failed) {
                        this.rejectStatus(model, new Error(`Model failed: ${this.toDisplayName(model)}`));
+
                        return;
                }
 
@@ -842,12 +896,15 @@ class ModelsStore {
         */
        private setRouterModelStatus(modelId: string, status: ServerModelStatus): void {
                const idx = this.routerModels.findIndex((m) => m.id === modelId);
+
                if (idx === -1) return;
 
                const current = this.routerModels[idx];
+
                if (current.status.value === status) return;
 
                const next = [...this.routerModels];
+
                next[idx] = { ...current, status: { ...current.status, value: status } };
                this.routerModels = next;
        }
@@ -858,7 +915,7 @@ class ModelsStore {
         */
        private waitForStatus(modelId: string, target: ServerModelStatus): Promise<void> {
                return new Promise((resolve, reject) => {
-                       this.statusWaiters.set(modelId, { target, resolve, reject });
+                       this.statusWaiters.set(modelId, { reject, resolve, target });
                });
        }
 
@@ -867,6 +924,7 @@ class ModelsStore {
         */
        private settleStatus(modelId: string, status: ServerModelStatus): void {
                const waiter = this.statusWaiters.get(modelId);
+
                if (waiter && waiter.target === status) {
                        this.statusWaiters.delete(modelId);
                        waiter.resolve();
@@ -878,6 +936,7 @@ class ModelsStore {
         */
        private rejectStatus(modelId: string, error: Error): void {
                const waiter = this.statusWaiters.get(modelId);
+
                if (waiter) {
                        this.statusWaiters.delete(modelId);
                        waiter.reject(error);
@@ -886,6 +945,7 @@ class ModelsStore {
 
        async loadModel(modelId: string): Promise<void> {
                if (this.isModelLoaded(modelId)) return;
+
                if (this.modelLoadingStates.get(modelId)) return;
 
                this.modelLoadingStates.set(modelId, true);
@@ -895,6 +955,7 @@ class ModelsStore {
                this.subscribeStatus();
 
                const reachedLoaded = this.waitForStatus(modelId, ServerModelStatus.LOADED);
+
                reachedLoaded.catch(() => {});
 
                try {
@@ -905,6 +966,7 @@ class ModelsStore {
                        this.rejectStatus(modelId, error instanceof Error ? error : new Error('load failed'));
                        this.error = error instanceof Error ? error.message : 'Failed to load model';
                        toast.error(`Failed to load model: ${this.toDisplayName(modelId)}`);
+
                        throw error;
                } finally {
                        this.modelLoadingStates.set(modelId, false);
@@ -913,6 +975,7 @@ class ModelsStore {
 
        async unloadModel(modelId: string): Promise<void> {
                if (!this.isModelLoaded(modelId)) return;
+
                if (this.modelLoadingStates.get(modelId)) return;
 
                this.modelLoadingStates.set(modelId, true);
@@ -921,6 +984,7 @@ class ModelsStore {
                this.subscribeStatus();
 
                const reachedUnloaded = this.waitForStatus(modelId, ServerModelStatus.UNLOADED);
+
                reachedUnloaded.catch(() => {});
 
                try {
@@ -931,6 +995,7 @@ class ModelsStore {
                        this.rejectStatus(modelId, error instanceof Error ? error : new Error('unload failed'));
                        this.error = error instanceof Error ? error.message : 'Failed to unload model';
                        toast.error(`Failed to unload model: ${this.toDisplayName(modelId)}`);
+
                        throw error;
                } finally {
                        this.modelLoadingStates.set(modelId, false);
@@ -939,6 +1004,7 @@ class ModelsStore {
 
        async ensureModelLoaded(modelId: string): Promise<void> {
                if (this.isModelLoaded(modelId)) return;
+
                await this.loadModel(modelId);
        }
 
@@ -975,9 +1041,11 @@ class ModelsStore {
        private loadFavoritesFromStorage(): Set<string> {
                try {
                        const raw = localStorage.getItem(FAVORITE_MODELS_LOCALSTORAGE_KEY);
+
                        return raw ? new Set(JSON.parse(raw) as string[]) : new Set();
                } catch {
                        toast.error('Failed to load favorite models from local storage');
+
                        return new Set();
                }
        }
@@ -993,6 +1061,7 @@ class ModelsStore {
        private toDisplayName(id: string): string {
                const segments = id.split(/\\|\//);
                const candidate = segments.pop();
+
                return candidate && candidate.trim().length > 0 ? candidate : id;
        }
 
@@ -1000,9 +1069,9 @@ class ModelsStore {
                modalities: NonNullable<ApiLlamaCppServerProps['modalities']>
        ): ModelModalities {
                return {
-                       vision: modalities.vision ?? false,
                        audio: modalities.audio ?? false,
-                       video: modalities.video ?? false
+                       video: modalities.video ?? false,
+                       vision: modalities.vision ?? false
                };
        }
 
@@ -1015,9 +1084,9 @@ class ModelsStore {
                const inputs = architecture.input_modalities;
 
                return {
-                       vision: inputs.includes(FileTypeCategory.IMAGE),
                        audio: inputs.includes(FileTypeCategory.AUDIO),
-                       video: inputs.includes(FileTypeCategory.VIDEO)
+                       video: inputs.includes(FileTypeCategory.VIDEO),
+                       vision: inputs.includes(FileTypeCategory.IMAGE)
                };
        }
 
index c50fbe02db8933bc1c46e1eef77eb97a9030fae3..f013c5b4d3bfac7e541b6bafd9818aaba592f406 100644 (file)
@@ -1,5 +1,4 @@
 import { ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY } from '$lib/constants';
-
 import { SvelteSet } from 'svelte/reactivity';
 
 class PermissionsStore {
@@ -8,6 +7,7 @@ class PermissionsStore {
        constructor() {
                try {
                        const stored = localStorage.getItem(ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY);
+
                        if (stored) {
                                for (const name of JSON.parse(stored) as string[]) {
                                        if (typeof name === 'string') this._tools.add(name);
index 1e07f80ed7275269237ddf91097ff51b0cbb8b28..683ab23652d3de64d480a2eeb7677a5782a691a1 100644 (file)
@@ -28,6 +28,7 @@ export function persisted<T>(key: string, initialValue: T): PersistedValue<T> {
                try {
                        if (next === null || next === undefined) {
                                localStorage.removeItem(key);
+
                                return;
                        }
 
index 66ab411194577b3925a63de67351ea5c9137e2a0..03b6ddc169c3b5679f6131d87e84138cf7e0b8fe 100644 (file)
@@ -1,5 +1,5 @@
-import { PropsService } from '$lib/services/props.service';
 import { ServerRole } from '$lib/enums';
+import { PropsService } from '$lib/services/props.service';
 import { ApiError } from '$lib/utils/api-fetch';
 
 const LOADING_RETRY_INTERVAL_MS = 1000;
@@ -84,9 +84,11 @@ class ServerStore {
                if (this.fetchPromise) return this.fetchPromise;
 
                this.clearRetryTimer();
+
                if (!background) {
                        this.loading = true;
                }
+
                // Don't clear an existing "still loading" error before a retry -
                // doing so would unmount/remount the error banner every second.
                if (this.status !== 503) {
@@ -96,6 +98,7 @@ class ServerStore {
                const fetchPromise = (async () => {
                        try {
                                const props = await PropsService.fetch();
+
                                this.props = props;
                                this.error = null;
                                this.status = null;
@@ -112,6 +115,7 @@ class ServerStore {
                                if (!background) {
                                        this.loading = false;
                                }
+
                                this.fetchPromise = null;
                        }
                })();
@@ -132,6 +136,7 @@ class ServerStore {
 
        private scheduleRetry(): void {
                if (this.retryTimer) return;
+
                this.retryTimer = setTimeout(() => {
                        this.retryTimer = null;
                        this.fetch({ background: true });
@@ -155,6 +160,7 @@ class ServerStore {
 
        private detectRole(props: ApiLlamaCppServerProps): void {
                const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
+
                if (this.role !== newRole) {
                        this.role = newRole;
                        console.info(`Server running in ${newRole === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
index 898ab87cb8408007c85c3c04c06361d4ea7d06f2..f413638efa09cb568da8a5e978b009e2437cf07a 100644 (file)
  */
 
 import { browser } from '$app/environment';
-import { ColorMode } from '$lib/enums';
-import type { SettingsExportType } from '$lib/types';
-import { setMode } from 'mode-watcher';
 import {
        CONFIG_LOCALSTORAGE_KEY,
        SETTING_CONFIG_DEFAULT,
        SETTINGS_KEYS,
        USER_OVERRIDES_LOCALSTORAGE_KEY
 } from '$lib/constants';
-import { isMobile } from '$lib/stores/viewport.svelte';
+import { ColorMode } from '$lib/enums';
 import { ParameterSyncService } from '$lib/services/parameter-sync.service';
 import { serverStore } from '$lib/stores/server.svelte';
+import { isMobile } from '$lib/stores/viewport.svelte';
+import type { SettingsExportType } from '$lib/types';
 import {
        configToParameterRecord,
-       normalizeFloatingPoint,
        getConfigValue,
+       normalizeFloatingPoint,
        setConfigValue
 } from '$lib/utils';
+import { setMode } from 'mode-watcher';
 
 class SettingsStore {
        /**
@@ -143,6 +143,7 @@ class SettingsStore {
                        const legacyKeys = [...LEGACY_MARKDOWN_KEYS, LEGACY_RAW_TEXT_KEY].filter(
                                (key) => key in savedVal
                        );
+
                        if (legacyKeys.length > 0) {
                                if (!(SETTINGS_KEYS.RENDER_CONTENT_AS_RAW_TEXT in savedVal)) {
                                        if (LEGACY_RAW_TEXT_KEY in savedVal) {
@@ -153,6 +154,7 @@ class SettingsStore {
                                                ).some((key) => savedVal[key] === false);
                                        }
                                }
+
                                for (const key of legacyKeys) {
                                        delete (this.config as Record<string, unknown>)[key];
                                }
@@ -170,6 +172,7 @@ class SettingsStore {
                        const savedOverrides = JSON.parse(
                                localStorage.getItem(USER_OVERRIDES_LOCALSTORAGE_KEY) || '[]'
                        );
+
                        this.userOverrides = new Set(savedOverrides);
                } catch (error) {
                        console.warn('Failed to parse config from localStorage, using defaults:', error);
@@ -188,6 +191,7 @@ class SettingsStore {
                if (!browser) return;
 
                const legacyTheme = localStorage.getItem('theme');
+
                if (legacyTheme) {
                        this.config[SETTINGS_KEYS.THEME] = legacyTheme;
                        localStorage.removeItem('theme');
@@ -358,6 +362,7 @@ class SettingsStore {
         */
        syncWithServerDefaults(): void {
                const propsDefaults = this.getServerDefaults();
+
                if (Object.keys(propsDefaults).length === 0) return;
 
                const uiSettings = serverStore.uiSettings;
@@ -365,7 +370,6 @@ class SettingsStore {
 
                for (const [key, propsValue] of Object.entries(propsDefaults)) {
                        const currentValue = getConfigValue(this.config, key);
-
                        const normalizedCurrent = normalizeFloatingPoint(currentValue);
                        const normalizedDefault = normalizeFloatingPoint(propsValue);
 
@@ -497,6 +501,7 @@ class SettingsStore {
         */
        getParameterDiff() {
                const serverDefaults = this.getServerDefaults();
+
                if (Object.keys(serverDefaults).length === 0) return {};
 
                const configAsRecord = configToParameterRecord(
@@ -545,8 +550,10 @@ class SettingsStore {
                                >;
                                const safeServers = mcpServers.map((server) => {
                                        delete server.headers;
+
                                        return server;
                                });
+
                                configToExport.mcpServers = JSON.stringify(safeServers);
                        } catch {
                                // If parsing fails, just exclude the entire mcpServers field
@@ -555,10 +562,10 @@ class SettingsStore {
                }
 
                return {
-                       version: 1,
-                       timestamp: Date.now(),
                        config: configToExport,
-                       userOverrides: Array.from(this.userOverrides)
+                       timestamp: Date.now(),
+                       userOverrides: Array.from(this.userOverrides),
+                       version: 1
                };
        }
 
index 4114ef756646d828fa08f591101446a6378f627b..b1a946391a0b99f48d234594c727104b1b814f38 100644 (file)
@@ -1,6 +1,10 @@
-import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types';
-import { ToolsService } from '$lib/services/tools.service';
-import { mcpStore } from '$lib/stores/mcp.svelte';
+import {
+       buildSandboxToolDefinition,
+       DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY,
+       HOME_TILDE,
+       TOOL_GROUP_LABELS,
+       TOOL_SERVER_LABELS
+} from '$lib/constants';
 import {
        BuiltInTool,
        GlobSearchType,
@@ -9,15 +13,10 @@ import {
        ToolCallType,
        ToolSource
 } from '$lib/enums';
+import { ToolsService } from '$lib/services/tools.service';
+import { mcpStore } from '$lib/stores/mcp.svelte';
 import { config } from '$lib/stores/settings.svelte';
-import {
-       DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY,
-       buildSandboxToolDefinition,
-       HOME_TILDE,
-       TOOL_GROUP_LABELS,
-       TOOL_SERVER_LABELS
-} from '$lib/constants';
-
+import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types';
 import { SvelteMap, SvelteSet } from 'svelte/reactivity';
 
 /** Stable selection identity for a tool, shared by the disabled set and the permission store */
@@ -36,8 +35,10 @@ class ToolsStore {
        constructor() {
                try {
                        const stored = localStorage.getItem(DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY);
+
                        if (stored) {
                                const parsed = JSON.parse(stored);
+
                                if (Array.isArray(parsed)) {
                                        for (const key of parsed) {
                                                if (typeof key === 'string') this._disabledTools.add(key);
@@ -77,10 +78,15 @@ class ToolsStore {
 
        private inferTypeFromDefault(value: unknown): string | undefined {
                if (typeof value === 'string') return 'string';
+
                if (typeof value === 'boolean') return 'boolean';
+
                if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';
+
                if (Array.isArray(value)) return 'array';
+
                if (value !== null && typeof value === 'object') return 'object';
+
                return undefined;
        }
 
@@ -97,9 +103,11 @@ class ToolsStore {
                if (normalized.properties && typeof normalized.properties === 'object') {
                        const props = normalized.properties as Record<string, Record<string, unknown>>;
                        const normalizedProps: Record<string, Record<string, unknown>> = {};
+
                        for (const [key, prop] of Object.entries(props)) {
                                if (!prop || typeof prop !== 'object') {
                                        normalizedProps[key] = prop;
+
                                        continue;
                                }
 
@@ -107,6 +115,7 @@ class ToolsStore {
 
                                if (!normalizedProp.type && normalizedProp.default !== undefined) {
                                        const inferred = this.inferTypeFromDefault(normalizedProp.default);
+
                                        if (inferred) normalizedProp.type = inferred;
                                }
 
@@ -137,12 +146,12 @@ class ToolsStore {
                schema?: Record<string, unknown>
        ): OpenAIToolDefinition {
                return {
-                       type: ToolCallType.FUNCTION,
                        function: {
-                               name,
                                description,
-                               parameters: schema ?? { type: JsonSchemaType.OBJECT, properties: {}, required: [] }
-                       }
+                               name,
+                               parameters: schema ?? { properties: {}, required: [], type: JsonSchemaType.OBJECT }
+                       },
+                       type: ToolCallType.FUNCTION
                };
        }
 
@@ -166,10 +175,12 @@ class ToolsStore {
 
        get customTools(): OpenAIToolDefinition[] {
                const raw = config().customJson;
+
                if (!raw || typeof raw !== 'string') return [];
 
                try {
                        const parsed = JSON.parse(raw);
+
                        if (!Array.isArray(parsed)) return [];
 
                        return parsed.filter(
@@ -193,28 +204,30 @@ class ToolsStore {
                definition: OpenAIToolDefinition;
        }[] {
                const out: { serverId: string; serverName: string; definition: OpenAIToolDefinition }[] = [];
-
                const connections = mcpStore.getConnections();
+
                if (connections.size > 0) {
                        for (const [serverId, connection] of connections) {
                                const serverName = mcpStore.getServerDisplayName(serverId);
+
                                for (const tool of connection.tools) {
                                        const rawSchema = (tool.inputSchema as Record<string, unknown>) ?? {
-                                               type: JsonSchemaType.OBJECT,
                                                properties: {},
-                                               required: []
+                                               required: [],
+                                               type: JsonSchemaType.OBJECT
                                        };
+
                                        out.push({
-                                               serverId,
-                                               serverName,
                                                definition: {
-                                                       type: ToolCallType.FUNCTION,
                                                        function: {
-                                                               name: tool.name,
                                                                description: tool.description,
+                                                               name: tool.name,
                                                                parameters: this.normalizeJsonSchema(rawSchema)
-                                                       }
-                                               }
+                                                       },
+                                                       type: ToolCallType.FUNCTION
+                                               },
+                                               serverId,
+                                               serverName
                                        });
                                }
                        }
@@ -222,9 +235,9 @@ class ToolsStore {
                        for (const { serverId, serverName, tools } of this.getMcpToolsFromHealthChecks()) {
                                for (const tool of tools) {
                                        out.push({
+                                               definition: this.mcpDefinition(tool.name, tool.description),
                                                serverId,
-                                               serverName,
-                                               definition: this.mcpDefinition(tool.name, tool.description)
+                                               serverName
                                        });
                                }
                        }
@@ -237,48 +250,52 @@ class ToolsStore {
        get allTools(): ToolEntry[] {
                const entries: ToolEntry[] = [];
                const seen = new SvelteSet<string>();
-
                const push = (entry: ToolEntry) => {
                        if (seen.has(entry.key)) return;
+
                        seen.add(entry.key);
                        entries.push(entry);
                };
 
                for (const def of this._builtinTools) {
                        const name = def.function.name;
+
                        push({
-                               source: ToolSource.BUILTIN,
+                               definition: def,
                                key: this.toolKey(ToolSource.BUILTIN, name),
-                               definition: def
+                               source: ToolSource.BUILTIN
                        });
                }
 
                for (const def of this.frontendTools) {
                        const name = def.function.name;
+
                        push({
-                               source: ToolSource.FRONTEND,
+                               definition: def,
                                key: this.toolKey(ToolSource.FRONTEND, name),
-                               definition: def
+                               source: ToolSource.FRONTEND
                        });
                }
 
-               for (const { serverId, serverName, definition } of this.mcpEntries()) {
+               for (const { definition, serverId, serverName } of this.mcpEntries()) {
                        const name = definition.function.name;
+
                        push({
-                               source: ToolSource.MCP,
+                               definition,
+                               key: this.toolKey(ToolSource.MCP, name, serverId),
                                serverId,
                                serverName,
-                               key: this.toolKey(ToolSource.MCP, name, serverId),
-                               definition
+                               source: ToolSource.MCP
                        });
                }
 
                for (const def of this.customTools) {
                        const name = def.function.name;
+
                        push({
-                               source: ToolSource.CUSTOM,
+                               definition: def,
                                key: this.toolKey(ToolSource.CUSTOM, name),
-                               definition: def
+                               source: ToolSource.CUSTOM
                        });
                }
 
@@ -295,12 +312,13 @@ class ToolsStore {
                                entry.source === ToolSource.MCP ? `mcp:${entry.serverId ?? ''}` : entry.source;
 
                        let group = byKey.get(groupKey);
+
                        if (!group) {
                                group = {
-                                       source: entry.source,
                                        key: groupKey,
                                        label: this.groupLabel(entry),
                                        serverId: entry.serverId,
+                                       source: entry.source,
                                        tools: []
                                };
                                byKey.set(groupKey, group);
@@ -334,6 +352,7 @@ class ToolsStore {
         */
        getEnabledToolsForLLM(): OpenAIToolDefinition[] {
                const enabledNames = new SvelteSet<string>();
+
                for (const entry of this.allTools) {
                        if (!this._disabledTools.has(entry.key)) {
                                enabledNames.add(entry.definition.function.name);
@@ -342,10 +361,11 @@ class ToolsStore {
 
                const result: OpenAIToolDefinition[] = [];
                const seen = new SvelteSet<string>();
-
                const take = (def: OpenAIToolDefinition) => {
                        const name = def.function.name;
+
                        if (!enabledNames.has(name) || seen.has(name)) return;
+
                        seen.add(name);
                        result.push(def);
                };
@@ -389,6 +409,7 @@ class ToolsStore {
                } else {
                        this._disabledTools.add(key);
                }
+
                this.persistDisabledTools();
        }
 
@@ -403,7 +424,9 @@ class ToolsStore {
        /** Enable all tools belonging to a specific MCP server */
        enableAllToolsForServer(serverId: string): void {
                const connection = mcpStore.getConnections().get(serverId);
+
                if (!connection) return;
+
                for (const tool of connection.tools) {
                        this._disabledTools.delete(this.toolKey(ToolSource.MCP, tool.name, serverId));
                }
@@ -413,6 +436,7 @@ class ToolsStore {
        toggleGroup(group: ToolGroup): void {
                const allEnabled = group.tools.every((t) => this.isToolEnabled(t.key));
                const target = !allEnabled;
+
                for (const tool of group.tools) {
                        if (target) this._disabledTools.delete(tool.key);
                        else this._disabledTools.add(tool.key);
@@ -431,9 +455,12 @@ class ToolsStore {
                tools: { name: string; description?: string }[];
        }[] {
                const result: ReturnType<ToolsStore['getMcpToolsFromHealthChecks']> = [];
+
                for (const server of mcpStore.getServers()) {
                        if (!server.enabled) continue;
+
                        const health = mcpStore.getHealthCheckState(server.id);
+
                        if (health.status === HealthCheckStatus.SUCCESS && health.tools.length > 0) {
                                result.push({
                                        serverId: server.id,
@@ -442,6 +469,7 @@ class ToolsStore {
                                });
                        }
                }
+
                return result;
        }
 
@@ -450,6 +478,7 @@ class ToolsStore {
                for (const entry of this.allTools) {
                        if (entry.definition.function.name === toolName) return entry;
                }
+
                return null;
        }
 
@@ -461,11 +490,17 @@ class ToolsStore {
        /** Get the display label for the server that owns a given tool */
        getToolServerLabel(toolName: string): string {
                const entry = this.findEntryByName(toolName);
+
                if (!entry) return '';
+
                if (entry.serverName) return mcpStore.getServerDisplayName(entry.serverName);
+
                if (entry.source === ToolSource.BUILTIN) return TOOL_SERVER_LABELS[ToolSource.BUILTIN];
+
                if (entry.source === ToolSource.CUSTOM) return TOOL_SERVER_LABELS[ToolSource.CUSTOM];
+
                if (entry.source === ToolSource.FRONTEND) return TOOL_SERVER_LABELS[ToolSource.FRONTEND];
+
                return '';
        }
 
@@ -503,13 +538,16 @@ class ToolsStore {
 
                try {
                        const toolInfos = await ToolsService.list();
+
                        this._builtinTools = toolInfos.map((info) => info.definition);
                        this._cwdAwareTools = new SvelteSet(
                                toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool)
                        );
                } catch (err) {
                        const errorMessage = err instanceof Error ? err.message : String(err);
+
                        this._error = errorMessage;
+
                        // 403 from /tools means the server was started without --tools
                        // TODO: check status code instead of relying on message
                        if (errorMessage.includes('this feature is disabled')) {
@@ -531,18 +569,21 @@ class ToolsStore {
         */
        async resolveServerHome(): Promise<string | null> {
                if (this._serverHome !== undefined) return this._serverHome;
+
                try {
                        const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
-                               path: HOME_TILDE,
-                               type: GlobSearchType.DIR,
+                               limit: 1,
                                max_depth: 1,
-                               limit: 1
+                               path: HOME_TILDE,
+                               type: GlobSearchType.DIR
                        });
+
                        this._serverHome = typeof res.base === 'string' ? res.base : null;
                } catch {
                        // searches still work via a literal `~`, only `~` abbreviation degrades
                        this._serverHome = null;
                }
+
                return this._serverHome;
        }
 }
index d8248a6dc1ffdc12cca5256b089c3d74e9a33b78..64a2106739b0eb8c28b3b3956ce06986b3666c14 100644 (file)
@@ -18,13 +18,16 @@ async function loadVersion() {
 
        if (import.meta.env.DEV) {
                version = 'dev';
+
                return;
        }
 
        try {
                const res = await fetch(`${base}/_app/version.json`, { cache: 'no-store' });
+
                if (res.ok) {
                        const data = await res.json();
+
                        version = data.version ?? '';
                }
        } catch {
index 84136d29a8383dd25ae4328d005a06de0303dd77..e4ab61035bca88c5668c777c07de8b0faa1d4ae1 100644 (file)
@@ -1,13 +1,13 @@
-import type { MessageRole } from '$lib/enums';
-import { ToolCallType } from '$lib/enums';
 import type {
        ApiChatCompletionRequest,
        ApiChatCompletionToolCall,
        ApiChatMessageContentPart,
        ApiChatMessageData
 } from './api';
-import type { ChatMessageTimings, ChatMessagePromptProgress } from './chat';
+import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
 import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from './database';
+import type { MessageRole } from '$lib/enums';
+import { ToolCallType } from '$lib/enums';
 
 /**
  * Agentic orchestration configuration.
index bdfcddfae7049795c13ca9099ceb9f02d5bb245c..ebf0a2a48b6a4546b62bcf792d02e37f6ccd565b 100644 (file)
@@ -1,11 +1,11 @@
+import type { ChatMessagePromptProgress, ChatRole } from './chat';
 import type {
        ContentPartType,
        FileTypeAudio,
-       ServerModelStatus,
        ServerModelsSseEventType,
+       ServerModelStatus,
        ServerRole
 } from '$lib/enums';
-import type { ChatMessagePromptProgress, ChatRole } from './chat';
 
 export type AudioInputFormat = FileTypeAudio.WAV | FileTypeAudio.MP3;
 
index 58a7e83b80170f743823af5b6440681758f7f935..2da043905790f6b0e3ff13e5b5c2e5b246aeb025 100644 (file)
@@ -1,6 +1,6 @@
-import type { ChatFormCommandAction, ErrorDialogType, FileMentionEntryType } from '$lib/enums';
 import type { ApiChatCompletionToolCall } from './api';
 import type { DatabaseMessage, DatabaseMessageExtra } from './database';
+import type { ChatFormCommandAction, ErrorDialogType, FileMentionEntryType } from '$lib/enums';
 
 export interface ChatUploadedFile {
        id: string;
index f5480778dee32956f64442171a5a2e5f6d07916f..b239aa0251b50e4437c23b5b8c9c6de15089f931 100644 (file)
@@ -1,5 +1,5 @@
-import type { ChatMessageTimings, ChatRole, ChatMessageType } from '$lib/types/chat';
 import { AttachmentType, ReasoningEffort } from '$lib/enums';
+import type { ChatMessageTimings, ChatMessageType, ChatRole } from '$lib/types/chat';
 
 export interface McpServerOverride {
        serverId: string;
index 2b5937913d638be0c16b13ea6b63694167773b88..d01bbac0098890813d54cbf62fb90350a756a19d 100644 (file)
@@ -1,19 +1,19 @@
-import type { MCPConnectionPhase, MCPLogLevel, HealthCheckStatus } from '$lib/enums/mcp.enums';
-import type { ToolSource } from '$lib/enums/tools.enums';
+import type { MimeTypeUnion } from './common';
 import type {
+       CallToolResult,
        Client,
        ClientCapabilities as SDKClientCapabilities,
-       ServerCapabilities as SDKServerCapabilities,
+       GetPromptResult,
        Implementation as SDKImplementation,
-       Tool,
-       CallToolResult,
        Prompt,
-       GetPromptResult,
        PromptMessage,
+       ServerCapabilities as SDKServerCapabilities,
+       Tool,
        Transport
 } from '@modelcontextprotocol/sdk';
-import type { MimeTypeUnion } from './common';
 import type { ColorMode } from '$lib/enums';
+import type { HealthCheckStatus, MCPConnectionPhase, MCPLogLevel } from '$lib/enums/mcp.enums';
+import type { ToolSource } from '$lib/enums/tools.enums';
 
 export type { Tool, CallToolResult, Prompt, GetPromptResult, PromptMessage };
 export type ClientCapabilities = SDKClientCapabilities;
index fbad500f9902a8c99ec97690477186f5c2725c69..55f304ea24273561abbb4dadf5fe1770d936ce9e 100644 (file)
@@ -1,15 +1,15 @@
-import type { SETTING_CONFIG_DEFAULT, SETTINGS_SECTION_TITLES } from '$lib/constants';
 import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
-import type { OpenAIToolDefinition } from './mcp';
 import type { DatabaseMessageExtra } from './database';
+import type { OpenAIToolDefinition } from './mcp';
+import type { Icon } from '@lucide/svelte';
+import type { SETTING_CONFIG_DEFAULT, SETTINGS_SECTION_TITLES } from '$lib/constants';
 import type {
        ParameterSource,
-       SyncableParameterType,
+       ReasoningEffort,
        SettingsFieldType,
        StreamConnectionState,
-       ReasoningEffort
+       SyncableParameterType
 } from '$lib/enums';
-import type { Icon } from '@lucide/svelte';
 import type { Component } from 'svelte';
 
 export type SettingsConfigValue = string | number | boolean | undefined;
index f9bbf37850942056636fb4daca7fe463fd6f0d40..95b6256a515f6190376768a71e2e6619a2305e74 100644 (file)
@@ -1,5 +1,5 @@
-import type { ToolSource } from '$lib/enums';
 import type { OpenAIToolDefinition } from './mcp';
+import type { ToolSource } from '$lib/enums';
 
 export interface ToolEntry {
        source: ToolSource;
index 135ef087a0a0d965335ef33c5ec69b24b2d070e7..9626de751b498947e548655e31de9c440a35356b 100644 (file)
@@ -8,7 +8,6 @@
 
 // the standard DOMException name for a cancelled operation
 const ABORT_ERROR_NAME = 'AbortError';
-
 // browser specific TypeError messages emitted when a fetch reader is cut by page unload,
 // navigation, or a transient network drop. functionally aborts, not actionable errors
 const ABORT_LIKE_MESSAGE_PATTERNS = [
@@ -62,16 +61,20 @@ export function isAbortError(error: unknown): boolean {
        if (error instanceof DOMException && error.name === ABORT_ERROR_NAME) {
                return true;
        }
+
        if (error instanceof Error) {
                if (error.name === ABORT_ERROR_NAME) {
                        return true;
                }
+
                // these patterns are functionally aborts, keep them out of the red console
                if (error instanceof TypeError) {
                        const msg = error.message ?? '';
+
                        if (ABORT_LIKE_MESSAGE_PATTERNS.some((re) => re.test(msg))) return true;
                }
        }
+
        return false;
 }
 
@@ -101,6 +104,7 @@ export function createLinkedController(...signals: (AbortSignal | undefined)[]):
                // If already aborted, abort immediately
                if (signal.aborted) {
                        controller.abort(signal.reason);
+
                        return controller;
                }
 
index fecc2b155ad30b7761988ec20312ee34c9b632dd..9c92a42d40ea46bc69861db510d743f48884c508 100644 (file)
@@ -1,15 +1,8 @@
-import {
-       AgenticSectionType,
-       AttachmentType,
-       ContinueIntentKind,
-       MessageRole,
-       ToolResultKind
-} from '$lib/enums';
 import {
        ATTACHMENT_SAVED_REGEX,
        MARKDOWN_ATX_HEADING_REGEX,
-       MARKDOWN_BOLD_REGEX,
        MARKDOWN_BLOCKQUOTE_REGEX,
+       MARKDOWN_BOLD_REGEX,
        MARKDOWN_CODE_FENCE_REGEX,
        MARKDOWN_LINK_REGEX,
        MARKDOWN_LIST_BULLET_REGEX,
@@ -21,6 +14,13 @@ import {
        SEARCH_SUMMARY_TOTAL_REGEX,
        TOOL_RESULT_JSON_OPEN_REGEX
 } from '$lib/constants';
+import {
+       AgenticSectionType,
+       AttachmentType,
+       ContinueIntentKind,
+       MessageRole,
+       ToolResultKind
+} from '$lib/enums';
 import type { ApiChatCompletionToolCall } from '$lib/types/api';
 import type {
        DatabaseMessage,
@@ -78,9 +78,10 @@ function deriveSingleTurnSections(
                const hasContentAfterReasoning =
                        !!message.content?.trim() || toolCalls.length > 0 || streamingToolCalls.length > 0;
                const isPending = isStreaming && !hasContentAfterReasoning;
+
                sections.push({
-                       type: isPending ? AgenticSectionType.REASONING_PENDING : AgenticSectionType.REASONING,
                        content: message.reasoningContent,
+                       type: isPending ? AgenticSectionType.REASONING_PENDING : AgenticSectionType.REASONING,
                        wasInterrupted: !isStreaming && !hasContentAfterReasoning
                });
        }
@@ -88,16 +89,16 @@ function deriveSingleTurnSections(
        // 2. Text content
        if (message.content?.trim()) {
                sections.push({
-                       type: AgenticSectionType.TEXT,
-                       content: message.content
+                       content: message.content,
+                       type: AgenticSectionType.TEXT
                });
        }
 
        // 3. Persisted tool calls (from message.toolCalls field)
        const toolCalls = parseToolCalls(message.toolCalls);
-
        // Index tool messages by toolCallId for O(1) lookup instead of O(n) find()
        const toolMsgById = new Map<string, DatabaseMessage>();
+
        for (const tm of toolMessages) {
                if (tm.toolCallId && !toolMsgById.has(tm.toolCallId)) {
                        toolMsgById.set(tm.toolCallId, tm);
@@ -112,29 +113,32 @@ function deriveSingleTurnSections(
                        : isStreaming
                                ? AgenticSectionType.TOOL_CALL_PENDING
                                : AgenticSectionType.TOOL_CALL;
+
                sections.push({
-                       type,
                        content: resultMsg?.content || '',
-                       toolName: tc.function?.name,
                        toolArgs: tc.function?.arguments,
+                       toolCallId: tc.id,
+                       toolCwd: resultMsg?.toolCwd,
+                       toolName: tc.function?.name,
                        toolResult: resultMsg?.content,
                        toolResultExtras: resultMsg?.extra,
-                       toolCwd: resultMsg?.toolCwd,
-                       toolCallId: tc.id
+                       type
                });
        }
 
        // 4. Streaming tool calls (not yet persisted - currently being received)
        const persistedIds = new Set(toolCalls.map((t) => t.id).filter(Boolean));
+
        for (const tc of streamingToolCalls) {
                // Skip if already in persisted tool calls
                if (tc.id && persistedIds.has(tc.id)) continue;
+
                sections.push({
-                       type: AgenticSectionType.TOOL_CALL_STREAMING,
                        content: '',
-                       toolName: tc.function?.name,
                        toolArgs: tc.function?.arguments,
-                       toolCallId: tc.id
+                       toolCallId: tc.id,
+                       toolName: tc.function?.name,
+                       type: AgenticSectionType.TOOL_CALL_STREAMING
                });
        }
 
@@ -168,8 +172,8 @@ export function deriveAgenticSections(
        }
 
        const sections: AgenticSection[] = [];
-
        const firstTurnToolMsgs = collectToolMessages(toolMessages, 0);
+
        sections.push(...deriveSingleTurnSections(message, firstTurnToolMsgs));
 
        let i = firstTurnToolMsgs.length;
@@ -212,10 +216,12 @@ export function buildAssistantRawOutput(sections: AgenticSection[]): string {
                        case AgenticSectionType.REASONING:
                        case AgenticSectionType.REASONING_PENDING:
                                parts.push(`${REASONING_TAGS.START}${NEWLINE}${section.content}${REASONING_TAGS.END}`);
+
                                break;
 
                        case AgenticSectionType.TEXT:
                                parts.push(section.content);
+
                                break;
 
                        case AgenticSectionType.TOOL_CALL:
@@ -281,8 +287,8 @@ export function splitSearchSummaryList(
        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);
+
        if (totalMatch) {
                captureTotal(parseInt(totalMatch[1], 10));
        }
@@ -316,11 +322,13 @@ export function parseToolResultWithImages(
                .join(NEWLINE);
        const cacheKey = `${imageNames}:${toolResult}`;
        const cached = toolResultLinesCache.get(cacheKey);
+
        if (cached !== undefined) return cached;
 
        const lines = toolResult.split(NEWLINE);
        const result = lines.map((line) => {
                const match = line.match(ATTACHMENT_SAVED_REGEX);
+
                if (!match || !extras) return { text: line };
 
                const attachmentName = match[1];
@@ -329,12 +337,13 @@ export function parseToolResultWithImages(
                                e.type === AttachmentType.IMAGE && e.name === attachmentName
                );
 
-               return { text: line, image };
+               return { image, text: line };
        });
 
        if (toolResultLinesCache.size >= TOOL_RESULT_LINES_CACHE_MAX_SIZE) {
                toolResultLinesCache.delete(toolResultLinesCache.keys().next().value!);
        }
+
        toolResultLinesCache.set(cacheKey, result);
 
        return result;
@@ -359,9 +368,11 @@ export function classifyToolResult(content: string | undefined): ToolResultKind
        if (!content) return ToolResultKind.TEXT;
 
        const cached = classifyCache.get(content);
+
        if (cached !== undefined) return cached;
 
        const trimmed = content.trim();
+
        if (!trimmed) return ToolResultKind.TEXT;
 
        let result: ToolResultKind = ToolResultKind.TEXT;
@@ -383,6 +394,7 @@ export function classifyToolResult(content: string | undefined): ToolResultKind
        if (classifyCache.size >= CLASSIFY_CACHE_MAX_SIZE) {
                classifyCache.delete(classifyCache.keys().next().value!);
        }
+
        classifyCache.set(content, result);
 
        return result;
@@ -405,13 +417,17 @@ function looksLikeMarkdown(content: string): boolean {
 
        for (const line of lines) {
                if (MARKDOWN_ATX_HEADING_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_NUMBERED_REGEX.test(line)) return true;
        }
 
        // Inline structural markers anywhere in the body.
        if (MARKDOWN_LINK_REGEX.test(content)) return true;
+
        if (MARKDOWN_BOLD_REGEX.test(content)) return true;
 
        // Tables: a pipe-bearing header line followed by a separator row.
@@ -438,11 +454,14 @@ function parseToolCalls(toolCallsJson?: string): ApiChatCompletionToolCall[] {
        if (!toolCallsJson) return [];
 
        const cached = toolCallsParseCache.get(toolCallsJson);
+
        if (cached) return cached;
 
        let result: ApiChatCompletionToolCall[];
+
        try {
                const parsed = JSON.parse(toolCallsJson);
+
                result = Array.isArray(parsed) ? parsed : [];
        } catch {
                result = [];
@@ -451,6 +470,7 @@ function parseToolCalls(toolCallsJson?: string): ApiChatCompletionToolCall[] {
        if (toolCallsParseCache.size >= TOOL_CALLS_CACHE_MAX_SIZE) {
                toolCallsParseCache.delete(toolCallsParseCache.keys().next().value!);
        }
+
        toolCallsParseCache.set(toolCallsJson, result);
 
        return result;
@@ -508,6 +528,7 @@ export function classifyContinueIntent(messages: DatabaseMessage[], idx: number)
        }
 
        const hasToolCalls = parseToolCalls(target.toolCalls).length > 0;
+
        if (!hasToolCalls) {
                return { kind: ContinueIntentKind.APPEND_TEXT };
        }
@@ -516,6 +537,7 @@ export function classifyContinueIntent(messages: DatabaseMessage[], idx: number)
        // messages directly after the assistant turn that owns them, so the first
        // non tool message marks the boundary.
        let lastTrailingTool = idx;
+
        for (let i = idx + 1; i < messages.length; i++) {
                if (messages[i].role === MessageRole.TOOL) {
                        lastTrailingTool = i;
index e9d90625830fcfd240fa5463e2e5fcc994eecaa1..7c3b9a9d232530c8ed14f9aa204913566e324bca 100644 (file)
@@ -1,7 +1,7 @@
+import { getAuthHeaders, getJsonHeaders } from './api-headers';
 import { base } from '$app/paths';
-import { getJsonHeaders, getAuthHeaders } from './api-headers';
-import { UrlProtocol } from '$lib/enums';
 import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants/error';
+import { UrlProtocol } from '$lib/enums';
 
 /**
  * API Fetch Utilities
@@ -61,16 +61,15 @@ export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
  */
 export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}): Promise<T> {
        const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
-
        const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
        const headers = { ...baseHeaders, ...customHeaders };
-
        const url =
                path.startsWith(UrlProtocol.HTTP) || path.startsWith(UrlProtocol.HTTPS)
                        ? path
                        : `${base}${path}`;
 
        let response;
+
        try {
                response = await fetch(url, {
                        ...fetchOptions,
@@ -82,6 +81,7 @@ export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}):
 
        if (!response.ok) {
                const errorMessage = await parseErrorMessage(response);
+
                throw new ApiError(errorMessage, response.status);
        }
 
@@ -118,11 +118,11 @@ export async function apiFetchWithParams<T>(
        }
 
        const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
-
        const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
        const headers = { ...baseHeaders, ...customHeaders };
 
        let response;
+
        try {
                response = await fetch(url.toString(), {
                        ...fetchOptions,
@@ -134,6 +134,7 @@ export async function apiFetchWithParams<T>(
 
        if (!response.ok) {
                const errorMessage = await parseErrorMessage(response);
+
                throw new ApiError(errorMessage, response.status);
        }
 
@@ -154,8 +155,8 @@ export async function apiPost<T, B = unknown>(
        options: ApiFetchOptions = {}
 ): Promise<T> {
        return apiFetch<T>(path, {
-               method: 'POST',
                body: JSON.stringify(body),
+               method: 'POST',
                ...options
        });
 }
@@ -167,12 +168,15 @@ export async function apiPost<T, B = unknown>(
 async function parseErrorMessage(response: Response): Promise<string> {
        try {
                const errorData = await response.json();
+
                if (errorData?.error?.message) {
                        return errorData.error.message;
                }
+
                if (errorData?.error && typeof errorData.error === 'string') {
                        return errorData.error;
                }
+
                if (errorData?.message) {
                        return errorData.message;
                }
@@ -181,6 +185,7 @@ async function parseErrorMessage(response: Response): Promise<string> {
        }
 
        const httpErrorStr = HTTP_CODE_TO_STRING[response.status];
+
        if (httpErrorStr) {
                return httpErrorStr;
        }
@@ -195,8 +200,10 @@ async function parseErrorMessage(response: Response): Promise<string> {
  */
 function beautifyNetworkError(throwable: unknown): string {
        let message;
+
        if (throwable instanceof Error) {
                message = throwable.message;
+
                if (throwable.name === 'TypeError' && message.includes('fetch')) {
                        return ERROR_MESSAGES.NETWORK.UNREACHABLE;
                }
index da0ec9db5f0a499532502f7d24d747e4ef869a80..16823de011e7188696b32f8a7652814e46f0b227 100644 (file)
@@ -1,4 +1,4 @@
-import { config } from '$lib/stores/settings.svelte';
+import { redactValue } from './redact';
 import {
        AUTHORIZATION_HEADER,
        BEARER_PREFIX,
@@ -7,7 +7,7 @@ import {
        REDACTED_HEADERS
 } from '$lib/constants';
 import { MimeTypeApplication } from '$lib/enums';
-import { redactValue } from './redact';
+import { config } from '$lib/stores/settings.svelte';
 
 /**
  * Get authorization headers for API requests
index 5f5986f9c2c28369c16e9e472006897a30bb81da..7b087ba9ad6f4de406b067d63f28321aecdeafd9 100644 (file)
@@ -1,6 +1,6 @@
-import { base } from '$app/paths';
 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 { MimeTypeApplication } from '$lib/enums';
 import { config } from '$lib/stores/settings.svelte';
@@ -36,6 +36,7 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
                        }
 
                        console.warn(`Server responded with status ${response.status} during API key validation`);
+
                        return;
                }
        } catch (err) {
index 30c7043bf0e2f24118cb27cb79ff4653993d7b30..0ec7cf40d488f221068986815618d9a012ba5703 100644 (file)
@@ -1,10 +1,10 @@
 import { AttachmentType, FileTypeCategory, SpecialFileType } from '$lib/enums';
-import { getFileTypeCategory, getFileTypeCategoryByExtension, isImageFile } from '$lib/utils';
 import type {
        AttachmentDisplayItemsOptions,
        ChatAttachmentDisplayItem,
        ChatUploadedFile
 } from '$lib/types';
+import { getFileTypeCategory, getFileTypeCategoryByExtension, isImageFile } from '$lib/utils';
 
 /**
  * Check if a display item represents an MCP prompt
@@ -14,9 +14,11 @@ export function isMcpPrompt(item: ChatAttachmentDisplayItem): boolean {
        if (item.attachment?.type === AttachmentType.MCP_PROMPT) {
                return true;
        }
+
        if (item.uploadedFile?.type === SpecialFileType.MCP_PROMPT && item.uploadedFile.mcpPrompt) {
                return true;
        }
+
        return false;
 }
 
@@ -47,21 +49,21 @@ function getUploadedFileCategory(file: ChatUploadedFile): FileTypeCategory | nul
 export function getAttachmentDisplayItems(
        options: AttachmentDisplayItemsOptions
 ): ChatAttachmentDisplayItem[] {
-       const { uploadedFiles = [], attachments = [] } = options;
+       const { attachments = [], uploadedFiles = [] } = options;
        const items: ChatAttachmentDisplayItem[] = [];
 
        // Add uploaded files (ChatForm)
        for (const file of uploadedFiles) {
                items.push({
                        id: file.id,
-                       name: file.name,
-                       size: file.size,
-                       preview: file.preview,
                        isImage: getUploadedFileCategory(file) === FileTypeCategory.IMAGE,
                        isLoading: file.isLoading,
                        loadError: file.loadError,
-                       uploadedFile: file,
-                       textContent: file.textContent
+                       name: file.name,
+                       preview: file.preview,
+                       size: file.size,
+                       textContent: file.textContent,
+                       uploadedFile: file
                });
        }
 
@@ -70,13 +72,13 @@ export function getAttachmentDisplayItems(
                const isImage = isImageFile(attachment);
 
                items.push({
+                       attachment,
+                       attachmentIndex: index,
                        id: `attachment-${index}`,
+                       isImage,
                        name: attachment.name,
-                       size: 'size' in attachment ? attachment.size : undefined,
                        preview: isImage && 'base64Url' in attachment ? attachment.base64Url : undefined,
-                       isImage,
-                       attachment,
-                       attachmentIndex: index,
+                       size: 'size' in attachment ? attachment.size : undefined,
                        textContent: 'content' in attachment ? attachment.content : undefined
                });
        }
index ab207b7a44d2516ca246e3346ee310659414eabb..1241d6e0559332e4a493ea65444636e09d4fcbd2 100644 (file)
@@ -23,9 +23,9 @@ export class AudioRecorder {
                try {
                        this.stream = await navigator.mediaDevices.getUserMedia({
                                audio: {
+                                       autoGainControl: true,
                                        echoCancellation: true,
-                                       noiseSuppression: true,
-                                       autoGainControl: true
+                                       noiseSuppression: true
                                }
                        });
 
@@ -37,6 +37,7 @@ export class AudioRecorder {
                        this.recordingState = true;
                } catch (error) {
                        console.error('Failed to start recording:', error);
+
                        throw new Error('Failed to access microphone. Please check permissions.');
                }
        }
@@ -49,6 +50,7 @@ export class AudioRecorder {
 
                        if (!recorder || recorder.state === 'inactive') {
                                reject(new Error('No active recording to stop'));
+
                                return;
                        }
 
@@ -156,18 +158,19 @@ export async function convertToWav(audioBlob: Blob): Promise<Blob> {
                }
 
                const arrayBuffer = await audioBlob.arrayBuffer();
-
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
 
                try {
                        const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+
                        return audioBufferToWav(audioBuffer);
                } finally {
                        audioContext.close();
                }
        } catch (error) {
                console.error('Failed to convert audio to WAV:', error);
+
                return audioBlob;
        }
 }
@@ -181,10 +184,8 @@ function audioBufferToWav(buffer: AudioBuffer): Blob {
        const byteRate = sampleRate * blockAlign;
        const dataSize = length * blockAlign;
        const bufferSize = 44 + dataSize;
-
        const arrayBuffer = new ArrayBuffer(bufferSize);
        const view = new DataView(arrayBuffer);
-
        const writeString = (offset: number, string: string) => {
                for (let i = 0; i < string.length; i++) {
                        view.setUint8(offset + i, string.charCodeAt(i));
@@ -207,17 +208,22 @@ function audioBufferToWav(buffer: AudioBuffer): Blob {
 
        // Cache channel arrays, write PCM via Int16Array (native little-endian, matches WAV)
        const channels: Float32Array[] = new Array(numberOfChannels);
+
        for (let c = 0; c < numberOfChannels; c++) {
                channels[c] = buffer.getChannelData(c);
        }
 
        const pcm = new Int16Array(arrayBuffer, 44, length * numberOfChannels);
+
        let p = 0;
+
        for (let i = 0; i < length; i++) {
                for (let c = 0; c < numberOfChannels; c++) {
                        let s = channels[c][i];
+
                        if (s > 1) s = 1;
                        else if (s < -1) s = -1;
+
                        pcm[p++] = s * 0x7fff;
                }
        }
@@ -237,8 +243,8 @@ export function createAudioFile(audioBlob: Blob, filename?: string): File {
        const defaultFilename = `recording-${timestamp}.${extension}`;
 
        return new File([audioBlob], filename || defaultFilename, {
-               type: audioBlob.type,
-               lastModified: Date.now()
+               lastModified: Date.now(),
+               type: audioBlob.type
        });
 }
 
index 6ff701318a11f4b83401fc2b238cd899146f5dd7..6c2c895cbe17e2c08e200159299efeb42dae1613 100644 (file)
@@ -25,6 +25,7 @@ export function findMessageById(
        id: string | null | undefined
 ): DatabaseMessage | undefined {
        if (!id) return undefined;
+
        return messages.find((m) => m.id === id);
 }
 
@@ -52,9 +53,11 @@ export function filterByLeafNodeId(
 
        // Find the starting node (leaf node or latest if not found)
        let startNode: DatabaseMessage | undefined = nodeMap.get(leafNodeId);
+
        if (!startNode) {
                // If leaf node not found, use the message with latest timestamp
                let latestTime = -1;
+
                for (const msg of messages) {
                        if (msg.timestamp > latestTime) {
                                startNode = msg;
@@ -65,6 +68,7 @@ export function filterByLeafNodeId(
 
        // Traverse from leaf to root, collecting messages
        let currentNode: DatabaseMessage | undefined = startNode;
+
        while (currentNode) {
                // Include message if it's not root, or if we want to include root
                if (currentNode.type !== 'root' || includeRoot) {
@@ -75,16 +79,19 @@ export function filterByLeafNodeId(
                if (currentNode.parent === null) {
                        break;
                }
+
                currentNode = nodeMap.get(currentNode.parent);
        }
 
        // Sort: system messages first, then by timestamp
        result.sort((a, b) => {
                if (a.role === MessageRole.SYSTEM && b.role !== MessageRole.SYSTEM) return -1;
+
                if (a.role !== MessageRole.SYSTEM && b.role === MessageRole.SYSTEM) return 1;
 
                return a.timestamp - b.timestamp;
        });
+
        return result;
 }
 
@@ -101,9 +108,11 @@ function findLeafNodeInMap(
        messageId: string
 ): string {
        let currentNode: DatabaseMessage | undefined = nodeMap.get(messageId);
+
        while (currentNode && currentNode.children.length > 0) {
                // Follow the last child (most recent branch)
                const lastChildId = currentNode.children[currentNode.children.length - 1];
+
                currentNode = nodeMap.get(lastChildId);
        }
 
@@ -115,6 +124,7 @@ function findLeafNodeInMap(
  */
 export function findLeafNode(messages: readonly DatabaseMessage[], messageId: string): string {
        const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
+
        return findLeafNodeInMap(nodeMap, messageId);
 }
 
@@ -169,6 +179,7 @@ export function getMessageSiblings(
        messageId: string
 ): ChatMessageSiblingInfo | null {
        const message = nodeMap.get(messageId);
+
        if (!message) {
                return null;
        }
@@ -177,40 +188,39 @@ export function getMessageSiblings(
        if (message.parent === null) {
                // No parent means this is likely a root node with no siblings
                return {
+                       currentIndex: 0,
                        message,
                        siblingIds: [messageId],
-                       currentIndex: 0,
                        totalSiblings: 1
                };
        }
 
        const parentNode = nodeMap.get(message.parent);
+
        if (!parentNode) {
                // Parent not found - treat as single message
                return {
+                       currentIndex: 0,
                        message,
                        siblingIds: [messageId],
-                       currentIndex: 0,
                        totalSiblings: 1
                };
        }
 
        // Get all sibling IDs (including self)
        const siblingIds = parentNode.children;
-
        // Convert sibling message IDs to their corresponding leaf node IDs
        // This allows navigation between different conversation branches
        const siblingLeafIds = siblingIds.map((siblingId: string) =>
                findLeafNodeInMap(nodeMap, siblingId)
        );
-
        // Find current message's position among siblings
        const currentIndex = siblingIds.indexOf(messageId);
 
        return {
+               currentIndex,
                message,
                siblingIds: siblingLeafIds,
-               currentIndex,
                totalSiblings: siblingIds.length
        };
 }
@@ -226,11 +236,14 @@ export function buildSiblingInfoMap(
 ): Map<string, ChatMessageSiblingInfo> {
        const nodeMap = new Map(messages.map((msg) => [msg.id, msg] as const));
        const siblingMap = new Map<string, ChatMessageSiblingInfo>();
+
        for (const msg of messages) {
                const info = getMessageSiblings(nodeMap, msg.id);
+
                if (info) {
                        siblingMap.set(msg.id, info);
                }
        }
+
        return siblingMap;
 }
index 4e414dd5457b56ed9ee16ab1fa55bdce49239931..2f5b2494d00683ae46642ae632cc4e294aeed3ae 100644 (file)
@@ -1,4 +1,4 @@
-import { DEFAULT_CACHE_TTL_MS, DEFAULT_CACHE_MAX_ENTRIES } from '$lib/constants';
+import { DEFAULT_CACHE_MAX_ENTRIES, DEFAULT_CACHE_TTL_MS } from '$lib/constants';
 
 /**
  * TTL Cache - Time-To-Live cache implementation for memory optimization
@@ -46,15 +46,18 @@ export class TTLCache<K extends string, V> {
         */
        get(key: K): V | null {
                const entry = this.cache.get(key);
+
                if (!entry) return null;
 
                if (Date.now() > entry.expiresAt) {
                        this.delete(key);
+
                        return null;
                }
 
                // Update last accessed time for LRU-like behavior
                entry.lastAccessed = Date.now();
+
                return entry.value;
        }
 
@@ -71,9 +74,9 @@ export class TTLCache<K extends string, V> {
                const now = Date.now();
 
                this.cache.set(key, {
-                       value,
                        expiresAt: now + ttl,
-                       lastAccessed: now
+                       lastAccessed: now,
+                       value
                });
        }
 
@@ -82,10 +85,12 @@ export class TTLCache<K extends string, V> {
         */
        has(key: K): boolean {
                const entry = this.cache.get(key);
+
                if (!entry) return false;
 
                if (Date.now() > entry.expiresAt) {
                        this.delete(key);
+
                        return false;
                }
 
@@ -97,9 +102,11 @@ export class TTLCache<K extends string, V> {
         */
        delete(key: K): boolean {
                const entry = this.cache.get(key);
+
                if (entry && this.onEvict) {
                        this.onEvict(key, entry.value);
                }
+
                return this.cache.delete(key);
        }
 
@@ -112,6 +119,7 @@ export class TTLCache<K extends string, V> {
                                this.onEvict(key, entry.value);
                        }
                }
+
                this.cache.clear();
        }
 
@@ -128,6 +136,7 @@ export class TTLCache<K extends string, V> {
         */
        prune(): number {
                const now = Date.now();
+
                let pruned = 0;
 
                for (const [key, entry] of this.cache) {
@@ -180,16 +189,20 @@ export class TTLCache<K extends string, V> {
         */
        touch(key: K): boolean {
                const entry = this.cache.get(key);
+
                if (!entry) return false;
 
                const now = Date.now();
+
                if (now > entry.expiresAt) {
                        this.delete(key);
+
                        return false;
                }
 
                entry.expiresAt = now + this.ttlMs;
                entry.lastAccessed = now;
+
                return true;
        }
 }
@@ -210,14 +223,17 @@ export class ReactiveTTLMap<K extends string, V> {
 
        get(key: K): V | null {
                const entry = this.entries.get(key);
+
                if (!entry) return null;
 
                if (Date.now() > entry.expiresAt) {
                        this.entries.delete(key);
+
                        return null;
                }
 
                entry.lastAccessed = Date.now();
+
                return entry.value;
        }
 
@@ -230,18 +246,20 @@ export class ReactiveTTLMap<K extends string, V> {
                const now = Date.now();
 
                this.entries.set(key, {
-                       value,
                        expiresAt: now + ttl,
-                       lastAccessed: now
+                       lastAccessed: now,
+                       value
                });
        }
 
        has(key: K): boolean {
                const entry = this.entries.get(key);
+
                if (!entry) return false;
 
                if (Date.now() > entry.expiresAt) {
                        this.entries.delete(key);
+
                        return false;
                }
 
@@ -262,6 +280,7 @@ export class ReactiveTTLMap<K extends string, V> {
 
        prune(): number {
                const now = Date.now();
+
                let pruned = 0;
 
                for (const [key, entry] of this.entries) {
index c5d9b730650b8e5e8815d1471bd56cde1a34209c..c4aa4d597ce1163f21d648159a1eff424a289a64 100644 (file)
@@ -1,6 +1,6 @@
+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 { getJpegOrientationFromDataURL, isJpegMimeType } from './jpeg-orientation';
 import { MimeTypeImage } from '$lib/enums';
 
 /**
@@ -37,7 +37,6 @@ export function capImageDataURLSize(
                        const orientation = isJpegMimeType(mimeType)
                                ? getJpegOrientationFromDataURL(base64UrlImage)
                                : 1;
-
                        const img = new Image();
 
                        img.onload = () => {
@@ -56,6 +55,7 @@ export function capImageDataURLSize(
 
                                        if (maxPixels > 0 && totalPixels > maxPixels) {
                                                const scaleFactor = Math.sqrt(maxPixels / totalPixels);
+
                                                canvas.width = Math.floor(targetWidth * scaleFactor);
                                                canvas.height = Math.floor(targetHeight * scaleFactor);
                                        } else if (orientation > 1) {
@@ -81,6 +81,7 @@ export function capImageDataURLSize(
                } catch (error) {
                        const message = error instanceof Error ? error.message : String(error);
                        const errorMessage = `Error resizing image: ${message}`;
+
                        console.error(errorMessage, error);
                        reject(new Error(errorMessage));
                }
index da6382f5cf94dbe3aee2843f05fd2628b9505e8f..33e9b4fb54d5a9194e9e849d97aed8872a47b0cd 100644 (file)
@@ -12,7 +12,6 @@
  */
 
 const THINKING_KWARG_VARS = ['enable_thinking', 'reasoning_effort', 'thinking_budget'];
-
 /**
  * Paired thinking-content tag patterns.
  *
@@ -30,7 +29,6 @@ const THINKING_TAG_PATTERNS: Array<[string, string | null]> = [
        ['<seed:think|>', '</seed:think|>'],
        ['<think></think>', null]
 ];
-
 const JINJA_THINKING_CONDITIONALS: RegExp[] = [
        // Matches: {% if enable thinking %}, {% if enable_thinking %}, {% if (enable_thinking is defined) %}
        // Handles: underscore-separated (enable_thinking), space-separated (enable thinking),
@@ -47,11 +45,13 @@ const JINJA_THINKING_CONDITIONALS: RegExp[] = [
  */
 export function detectThinkingSupport(t: string): boolean {
        if (!t) return false;
+
        for (const kwarg of THINKING_KWARG_VARS) {
                const regex = new RegExp(
                        `(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`,
                        'i'
                );
+
                if (regex.test(t)) return true;
        }
        for (const p of JINJA_THINKING_CONDITIONALS) {
@@ -60,27 +60,31 @@ export function detectThinkingSupport(t: string): boolean {
        for (const [s, e] of THINKING_TAG_PATTERNS) {
                if (t.includes(s) && (!e || t.includes(e))) return true;
        }
+
        return false;
 }
 
 export function detectThinkingSupportWithReason(t: string): { supported: boolean; reason: string } {
-       if (!t) return { supported: false, reason: 'No chat template available' };
+       if (!t) return { reason: 'No chat template available', supported: false };
+
        for (const kwarg of THINKING_KWARG_VARS) {
                const regex = new RegExp(
                        `(\\{\\{[^{}]*\\b${kwarg}\\b[^{}]*\\}\\}|\\{%[^{}]*\\b${kwarg}\\b[^{}]*%\\})`,
                        'i'
                );
+
                if (regex.test(t)) {
-                       return { supported: true, reason: 'Found: ' + kwarg };
+                       return { reason: 'Found: ' + kwarg, supported: true };
                }
        }
        for (const p of JINJA_THINKING_CONDITIONALS) {
-               if (p.test(t)) return { supported: true, reason: 'Found: thinking conditional' };
+               if (p.test(t)) return { reason: 'Found: thinking conditional', supported: true };
        }
        for (const [s, e] of THINKING_TAG_PATTERNS) {
                if (t.includes(s) && (!e || t.includes(e))) {
-                       return { supported: true, reason: 'Found: ' + s + (e ? ' .. ' + e : ' (self)') };
+                       return { reason: 'Found: ' + s + (e ? ' .. ' + e : ' (self)'), supported: true };
                }
        }
-       return { supported: false, reason: 'No thinking patterns found' };
+
+       return { reason: 'No thinking patterns found', supported: false };
 }
index 8fcb554b1a9a66257523af1db10340ff2d552c4d..96a20858aa652724bd10b251ebba0670af195511 100644 (file)
@@ -1,16 +1,16 @@
-import { toast } from 'svelte-sonner';
 import { AttachmentType } from '$lib/enums';
 import type {
+       ClipboardAttachment,
+       ClipboardMcpPromptAttachment,
+       ClipboardTextAttachment,
        DatabaseMessageExtra,
-       DatabaseMessageExtraTextFile,
        DatabaseMessageExtraLegacyContext,
        DatabaseMessageExtraMcpPrompt,
        DatabaseMessageExtraMcpResource,
-       ClipboardTextAttachment,
-       ClipboardMcpPromptAttachment,
-       ClipboardAttachment,
+       DatabaseMessageExtraTextFile,
        ParsedClipboardContent
 } from '$lib/types';
+import { toast } from 'svelte-sonner';
 
 /**
  * Copy text to clipboard with toast notification
@@ -30,11 +30,13 @@ export async function copyToClipboard(
                if (navigator.clipboard && navigator.clipboard.writeText) {
                        await navigator.clipboard.writeText(text);
                        toast.success(successMessage);
+
                        return true;
                }
 
                // Fallback for non-secure contexts
                const textArea = document.createElement('textarea');
+
                textArea.value = text;
                textArea.style.position = 'fixed';
                textArea.style.left = '-999999px';
@@ -44,10 +46,12 @@ export async function copyToClipboard(
                textArea.select();
 
                const successful = document.execCommand('copy');
+
                document.body.removeChild(textArea);
 
                if (successful) {
                        toast.success(successMessage);
+
                        return true;
                } else {
                        throw new Error('execCommand failed');
@@ -55,6 +59,7 @@ export async function copyToClipboard(
        } catch (error) {
                console.error('Failed to copy to clipboard:', error);
                toast.error(errorMessage);
+
                return false;
        }
 }
@@ -127,28 +132,32 @@ export function formatMessageForClipboard(
 
        if (asPlainText) {
                const parts = [content];
+
                for (const att of textAttachments) {
                        parts.push(att.content);
                }
+
                return parts.join('\n\n');
        }
 
        const clipboardAttachments: ClipboardAttachment[] = textAttachments.map((att) => {
                if (att.type === AttachmentType.MCP_PROMPT) {
                        const mcpAtt = att as DatabaseMessageExtraMcpPrompt;
+
                        return {
-                               type: AttachmentType.MCP_PROMPT,
+                               arguments: mcpAtt.arguments,
+                               content: mcpAtt.content,
                                name: mcpAtt.name,
-                               serverName: mcpAtt.serverName,
                                promptName: mcpAtt.promptName,
-                               content: mcpAtt.content,
-                               arguments: mcpAtt.arguments
+                               serverName: mcpAtt.serverName,
+                               type: AttachmentType.MCP_PROMPT
                        } as ClipboardMcpPromptAttachment;
                }
+
                return {
-                       type: AttachmentType.TEXT,
+                       content: att.content,
                        name: att.name,
-                       content: att.content
+                       type: AttachmentType.TEXT
                } as ClipboardTextAttachment;
        });
 
@@ -164,9 +173,9 @@ export function formatMessageForClipboard(
  */
 export function parseClipboardContent(clipboardText: string): ParsedClipboardContent {
        const defaultResult: ParsedClipboardContent = {
+               mcpPromptAttachments: [],
                message: clipboardText,
-               textAttachments: [],
-               mcpPromptAttachments: []
+               textAttachments: []
        };
 
        if (!clipboardText.startsWith('"')) {
@@ -182,16 +191,19 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon
 
                        if (escaped) {
                                escaped = false;
+
                                continue;
                        }
 
                        if (char === '\\') {
                                escaped = true;
+
                                continue;
                        }
 
                        if (char === '"') {
                                stringEndIndex = i;
+
                                break;
                        }
                }
@@ -202,45 +214,43 @@ export function parseClipboardContent(clipboardText: string): ParsedClipboardCon
 
                const jsonStringPart = clipboardText.substring(0, stringEndIndex + 1);
                const remainingPart = clipboardText.substring(stringEndIndex + 1).trim();
-
                const message = JSON.parse(jsonStringPart) as string;
 
                if (!remainingPart || !remainingPart.startsWith('[')) {
                        return {
+                               mcpPromptAttachments: [],
                                message,
-                               textAttachments: [],
-                               mcpPromptAttachments: []
+                               textAttachments: []
                        };
                }
 
                const attachments = JSON.parse(remainingPart) as unknown[];
-
                const validTextAttachments: ClipboardTextAttachment[] = [];
                const validMcpPromptAttachments: ClipboardMcpPromptAttachment[] = [];
 
                for (const att of attachments) {
                        if (isValidMcpPromptAttachment(att)) {
                                validMcpPromptAttachments.push({
-                                       type: AttachmentType.MCP_PROMPT,
+                                       arguments: att.arguments,
+                                       content: att.content,
                                        name: att.name,
-                                       serverName: att.serverName,
                                        promptName: att.promptName,
-                                       content: att.content,
-                                       arguments: att.arguments
+                                       serverName: att.serverName,
+                                       type: AttachmentType.MCP_PROMPT
                                });
                        } else if (isValidTextAttachment(att)) {
                                validTextAttachments.push({
-                                       type: AttachmentType.TEXT,
+                                       content: att.content,
                                        name: att.name,
-                                       content: att.content
+                                       type: AttachmentType.TEXT
                                });
                        }
                }
 
                return {
+                       mcpPromptAttachments: validMcpPromptAttachments,
                        message,
-                       textAttachments: validTextAttachments,
-                       mcpPromptAttachments: validMcpPromptAttachments
+                       textAttachments: validTextAttachments
                };
        } catch {
                return defaultResult;
@@ -307,5 +317,6 @@ export function hasClipboardAttachments(clipboardText: string): boolean {
        }
 
        const parsed = parseClipboardContent(clipboardText);
+
        return parsed.textAttachments.length > 0 || parsed.mcpPromptAttachments.length > 0;
 }
index 4361f311b7a7ad512ac5e49a5b716fd842a81044..9ce1fc9f300b92c4c99143cc3fc0fbae575fa657 100644 (file)
@@ -1,15 +1,15 @@
-import hljs from 'highlight.js';
 import {
-       NEWLINE,
+       AMPERSAND_REGEX,
        DEFAULT_LANGUAGE,
+       FENCE_PATTERN,
+       GT_REGEX,
        LANG_PATTERN,
-       AMPERSAND_REGEX,
        LT_REGEX,
-       GT_REGEX,
-       FENCE_PATTERN,
+       NEWLINE,
        TRIM_LEADING_PADDING_REGEX,
        TRIM_TRAILING_PADDING_REGEX
 } from '$lib/constants';
+import hljs from 'highlight.js';
 
 export interface IncompleteCodeBlock {
        language: string;
@@ -41,21 +41,25 @@ export function splitGluedClosingCodeFences(markdown: string): string {
        if (!markdown.includes('```')) return markdown;
 
        const lines = markdown.split(NEWLINE);
+
        let inside = false;
        let changed = false;
 
        for (let i = 0; i < lines.length; i++) {
                const match = FENCE_LINE_REGEX.exec(lines[i]);
+
                if (!match) continue;
 
                if (!inside) {
                        inside = true;
+
                        continue;
                }
 
                inside = false;
 
                const trailing = match[2];
+
                if (trailing.includes('`') || !/\s/.test(trailing)) continue;
 
                lines[i] = lines[i].slice(0, lines[i].length - trailing.length);
@@ -106,9 +110,11 @@ export function highlightCode(code: string, language: string, autoDetect = true)
        // (e.g., when text after a code block changes but the code itself doesn't).
        const cacheKey = `${language}:${autoDetect}:${code}`;
        const cached = highlightCache.get(cacheKey);
+
        if (cached) return cached;
 
        const trimmed = trimCodePadding(code);
+
        let result: string;
 
        try {
@@ -129,6 +135,7 @@ export function highlightCode(code: string, language: string, autoDetect = true)
        if (highlightCache.size >= HIGHLIGHT_CACHE_MAX_SIZE) {
                highlightCache.delete(highlightCache.keys().next().value!);
        }
+
        highlightCache.set(cacheKey, result);
 
        return result;
@@ -147,11 +154,13 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock
        // A code block is incomplete if there's an odd number of ``` fences
        const fencePattern = new RegExp(FENCE_PATTERN.source, FENCE_PATTERN.flags);
        const fences: number[] = [];
+
        let fenceMatch;
 
        while ((fenceMatch = fencePattern.exec(markdown)) !== null) {
                // Store the position after the ```
                const pos = fenceMatch[0].startsWith(NEWLINE) ? fenceMatch.index + 1 : fenceMatch.index;
+
                fences.push(pos);
        }
 
@@ -164,7 +173,6 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock
        // The last fence is the opening of the incomplete block
        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;
@@ -172,8 +180,8 @@ export function detectIncompleteCodeBlock(markdown: string): IncompleteCodeBlock
        const code = markdown.slice(codeStartIndex);
 
        return {
-               language,
                code,
+               language,
                openingIndex
        };
 }
index 0e7537ac2151b852e34f3bfa4e3676dd010c3e25..de1db30576d627883edd11eccb9547ae67703a6b 100644 (file)
@@ -11,7 +11,7 @@ export function findCommandToken(
        const name = spaceIdx === -1 ? rest : rest.slice(0, spaceIdx);
        const args = spaceIdx === -1 ? '' : rest.slice(spaceIdx + 1);
 
-       return { name, args, end: value.length };
+       return { args, end: value.length, name };
 }
 
 /**
@@ -26,6 +26,8 @@ export interface CommandDismissSnapshot {
 
 export function takeCommandDismissSnapshot(value: string): CommandDismissSnapshot | null {
        const token = findCommandToken(value);
+
        if (!token) return null;
-       return { name: token.name, args: token.args };
+
+       return { args: token.args, name: token.name };
 }
index cbfc68c679663dea99bff1eda305a29c39959885..6dfe327a436906d799a11724bbcf5e8ee75ec9ea 100644 (file)
@@ -27,16 +27,18 @@ export interface DiffLine {
 export function computeLineDiff(oldText: string, newText: string): DiffLine[] {
        const oldLines = splitLines(oldText);
        const newLines = splitLines(newText);
-
        const m = oldLines.length;
        const n = newLines.length;
 
        if (m === 0 && n === 0) return [];
-       if (m === 0) return newLines.map((t, k) => ({ kind: DiffLineKind.ADD, text: t, newLine: k + 1 }));
+
+       if (m === 0) return newLines.map((t, k) => ({ kind: DiffLineKind.ADD, newLine: k + 1, text: t }));
+
        if (n === 0)
-               return oldLines.map((t, k) => ({ kind: DiffLineKind.REMOVE, text: t, oldLine: k + 1 }));
+               return oldLines.map((t, k) => ({ kind: DiffLineKind.REMOVE, oldLine: k + 1, text: t }));
 
        const lcs: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
+
        for (let i = 1; i <= m; i++) {
                for (let j = 1; j <= n; j++) {
                        if (oldLines[i - 1] === newLines[j - 1]) {
@@ -48,36 +50,39 @@ export function computeLineDiff(oldText: string, newText: string): DiffLine[] {
        }
 
        const result: DiffLine[] = [];
+
        let i = m;
        let j = n;
+
        while (i > 0 && j > 0) {
                if (oldLines[i - 1] === newLines[j - 1]) {
                        result.push({
                                kind: DiffLineKind.CONTEXT,
-                               text: oldLines[i - 1],
+                               newLine: j,
                                oldLine: i,
-                               newLine: j
+                               text: oldLines[i - 1]
                        });
                        i--;
                        j--;
                } else if (lcs[i - 1][j] >= lcs[i][j - 1]) {
-                       result.push({ kind: DiffLineKind.REMOVE, text: oldLines[i - 1], oldLine: i });
+                       result.push({ kind: DiffLineKind.REMOVE, oldLine: i, text: oldLines[i - 1] });
                        i--;
                } else {
-                       result.push({ kind: DiffLineKind.ADD, text: newLines[j - 1], newLine: j });
+                       result.push({ kind: DiffLineKind.ADD, newLine: j, text: newLines[j - 1] });
                        j--;
                }
        }
        while (i > 0) {
-               result.push({ kind: DiffLineKind.REMOVE, text: oldLines[i - 1], oldLine: i });
+               result.push({ kind: DiffLineKind.REMOVE, oldLine: i, text: oldLines[i - 1] });
                i--;
        }
        while (j > 0) {
-               result.push({ kind: DiffLineKind.ADD, text: newLines[j - 1], newLine: j });
+               result.push({ kind: DiffLineKind.ADD, newLine: j, text: newLines[j - 1] });
                j--;
        }
 
        result.reverse();
+
        return result;
 }
 
@@ -87,19 +92,25 @@ export function computeLineDiff(oldText: string, newText: string): DiffLine[] {
  */
 export function renderUnifiedDiff(lines: DiffLine[]): string {
        if (lines.length === 0) return '';
+
        return lines.map((l) => prefixFor(l.kind) + l.text).join('\n');
 }
 
 /** Column-1 marker for a `DiffLine`: ` `, `+`, or `-`. */
 export function prefixFor(kind: DiffLineKind): string {
        if (kind === DiffLineKind.ADD) return '+';
+
        if (kind === DiffLineKind.REMOVE) return '-';
+
        return ' ';
 }
 
 function splitLines(text: string): string[] {
        if (text === '') return [];
+
        const parts = text.split('\n');
+
        if (parts[parts.length - 1] === '') parts.pop();
+
        return parts.map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l));
 }
index b85242d85dbfe131ef7a0c76fac2192ea410d01b..8a774c6d1c29c982c8e1b280866f2c529e8ed4dd 100644 (file)
@@ -27,6 +27,7 @@ export function getConfigValue<T extends SettingsConfigType>(
        key: string
 ): string | number | boolean | undefined {
        const value = (config as Record<string, unknown>)[key];
+
        return value as string | number | boolean | undefined;
 }
 
@@ -42,6 +43,7 @@ export function configToParameterRecord<T extends SettingsConfigType>(
 
        for (const key of keys) {
                const value = getConfigValue(config, key);
+
                if (value !== undefined) {
                        record[key] = value;
                }
index 0f136bdbee7416d7ce77107ab977d169222d2afd..114fdab64af308d4931963ad3a4ea65a1784b9b8 100644 (file)
@@ -47,13 +47,13 @@ export type ContentToken =
 // Block wrappers browsers insert for newlines; each folds back into a
 // single `\n` during serialization.
 const BLOCK_TAG_NAMES = new Set(['DIV', 'P']);
-
 // `file://` is required so plain URLs stay as text; `)` terminates only
 // when not followed by whitespace or `[` (adjacent badges keep working).
 const MENTION_BADGE_RE = fileMentionLinkRe('g');
 
 function badgeSourceLength(name: string, path: string): number {
        if (!name || !path) return 0;
+
        return `[${name}](file://${path})`.length;
 }
 
@@ -74,6 +74,7 @@ const CODE_SPAN_RE = /(```[\s\S]*?```)|(`[^`\n]+`)/g;
  */
 export function containsCodeSpan(value: string): boolean {
        CODE_SPAN_RE.lastIndex = 0;
+
        return CODE_SPAN_RE.test(value);
 }
 
@@ -89,11 +90,14 @@ const CODE_FENCE_RE = /```/g;
  */
 export function isOffsetInCodeBlock(source: string, offset: number): boolean {
        let inside = false;
+
        CODE_FENCE_RE.lastIndex = 0;
 
        let match: RegExpExecArray | null;
+
        while ((match = CODE_FENCE_RE.exec(source)) !== null) {
                if (match.index + match[0].length > offset) break;
+
                inside = !inside;
        }
 
@@ -110,10 +114,13 @@ export function isOffsetInCodeBlock(source: string, offset: number): boolean {
  */
 export function tokenizeContent(input: string): ContentToken[] {
        const tokens: ContentToken[] = [];
+
        let cursor = 0;
+
        CODE_SPAN_RE.lastIndex = 0;
 
        let match: RegExpExecArray | null;
+
        while ((match = CODE_SPAN_RE.exec(input)) !== null) {
                const start = match.index;
 
@@ -141,9 +148,11 @@ export function tokenizeContent(input: string): ContentToken[] {
  */
 function pushTextAndBadgeTokens(input: string, tokens: ContentToken[]) {
        let cursor = 0;
+
        MENTION_BADGE_RE.lastIndex = 0;
 
        let match: RegExpExecArray | null;
+
        while ((match = MENTION_BADGE_RE.exec(input)) !== null) {
                const [whole, name, path] = match;
                const start = match.index;
@@ -185,14 +194,17 @@ export function serializeContent(root: HTMLElement): string {
                for (const child of Array.from(parent.childNodes)) {
                        if (child.nodeType === Node.TEXT_NODE) {
                                const text = child.textContent ?? '';
+
                                if (text.length > 0) {
                                        if (pendingBlockBoundary) {
                                                out += '\n';
                                                pendingBlockBoundary = false;
                                        }
+
                                        out += text;
                                        first = false;
                                }
+
                                continue;
                        }
 
@@ -203,55 +215,69 @@ export function serializeContent(root: HTMLElement): string {
                        if (el.dataset.mentionBadge === 'true') {
                                const name = el.dataset.mentionName ?? '';
                                const path = el.dataset.mentionPath ?? '';
+
                                if (name && path) {
                                        if (pendingBlockBoundary) {
                                                out += '\n';
                                                pendingBlockBoundary = false;
                                        }
+
                                        out += `[${name}](file://${path})`;
                                        first = false;
                                }
+
                                continue;
                        }
 
                        if (el.dataset.codeToken !== undefined) {
                                const isBlock = el.dataset.codeToken === 'block';
+
                                if (isBlock && (pendingBlockBoundary || !first)) out += '\n';
+
                                pendingBlockBoundary = false;
                                walk(el);
                                first = false;
+
                                if (isBlock) pendingBlockBoundary = true;
+
                                continue;
                        }
 
                        if (el.tagName === 'BR') {
                                const isHatch =
                                        isCodeBlockElement(el.previousSibling) || isCodeBlockElement(el.nextSibling);
+
                                if (!isHatch && el.nextSibling) {
                                        if (pendingBlockBoundary) {
                                                out += '\n';
                                                pendingBlockBoundary = false;
                                        }
+
                                        out += '\n';
                                        first = false;
                                }
+
                                continue;
                        }
 
                        if (BLOCK_TAG_NAMES.has(el.tagName)) {
                                if (pendingBlockBoundary || !first) out += '\n';
+
                                pendingBlockBoundary = false;
                                walk(el);
                                first = false;
+
                                continue;
                        }
 
                        walk(el);
+
                        if (pendingBlockBoundary) first = false;
                }
        };
 
        walk(root);
+
        return out;
 }
 
@@ -265,6 +291,7 @@ export function serializeContent(root: HTMLElement): string {
  */
 export function domMatchesTokens(root: HTMLElement, tokens: ContentToken[]): boolean {
        const expected = tokens.filter((token) => token.kind !== 'text');
+
        let index = 0;
 
        const walk = (parent: Node): boolean => {
@@ -277,21 +304,28 @@ export function domMatchesTokens(root: HTMLElement, tokens: ContentToken[]): boo
 
                        if (!isBadge && !isCode) {
                                if (!walk(el)) return false;
+
                                continue;
                        }
 
                        const token = expected[index++];
+
                        if (!token) return false;
 
                        if (isBadge) {
                                if (token.kind !== 'badge') return false;
+
                                if (token.name !== (el.dataset.mentionName ?? '')) return false;
+
                                if (token.path !== (el.dataset.mentionPath ?? '')) return false;
+
                                continue;
                        }
 
                        const codeKind = el.dataset.codeToken === 'block' ? 'codeBlock' : 'inlineCode';
+
                        if (token.kind !== codeKind) return false;
+
                        if (
                                (token.kind === 'inlineCode' || token.kind === 'codeBlock') &&
                                token.text !== (el.textContent ?? '')
@@ -319,6 +353,7 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
 
        // A point is at/before the caret iff it falls inside [root start, caret].
        const pre = range.cloneRange();
+
        pre.selectNodeContents(root);
        pre.setEnd(range.endContainer, range.endOffset);
        const atOrBeforeCaret = (node: Node, offset: number) => pre.comparePoint(node, offset) !== 1;
@@ -338,27 +373,39 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
 
                        if (child.nodeType === Node.TEXT_NODE) {
                                const text = child.textContent ?? '';
+
                                if (text.length === 0) continue;
+
                                if (pendingPoint) {
-                                       const { node, index } = pendingPoint;
+                                       const { index, node } = pendingPoint;
+
                                        pendingPoint = null;
+
                                        if (!atOrBeforeCaret(node, index)) {
                                                done = true;
+
                                                return;
                                        }
+
                                        total += 1;
                                }
+
                                if (!atOrBeforeCaret(child, 0)) {
                                        done = true;
+
                                        return;
                                }
+
                                if (range.endContainer === child) {
                                        total += range.endOffset;
                                        done = true;
+
                                        return;
                                }
+
                                total += text.length;
                                first = false;
+
                                continue;
                        }
 
@@ -369,52 +416,72 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
                        const elIndex = Array.prototype.indexOf.call(parentNode.childNodes, el);
 
                        if (pendingPoint) {
-                               const { node, index } = pendingPoint;
+                               const { index, node } = pendingPoint;
+
                                pendingPoint = null;
+
                                if (!atOrBeforeCaret(node, index)) {
                                        done = true;
+
                                        return;
                                }
+
                                total += 1;
                        }
 
                        if (el.dataset.mentionBadge === 'true') {
                                const len = badgeSourceLength(el.dataset.mentionName ?? '', el.dataset.mentionPath ?? '');
+
                                if (len === 0) continue;
+
                                if (!atOrBeforeCaret(parentNode, elIndex + 1)) {
                                        done = true;
+
                                        return;
                                }
+
                                total += len;
                                first = false;
+
                                continue;
                        }
 
                        if (el.dataset.codeToken !== undefined) {
                                const isBlock = el.dataset.codeToken === 'block';
+
                                if (isBlock && !first) {
                                        if (!atOrBeforeCaret(el, 0)) {
                                                done = true;
+
                                                return;
                                        }
+
                                        total += 1;
                                }
+
                                walk(el);
                                first = false;
-                               if (isBlock) pendingPoint = { node: parentNode, index: elIndex + 1 };
+
+                               if (isBlock) pendingPoint = { index: elIndex + 1, node: parentNode };
+
                                continue;
                        }
 
                        if (el.tagName === 'BR') {
                                const isHatch =
                                        isCodeBlockElement(el.previousSibling) || isCodeBlockElement(el.nextSibling);
+
                                if (isHatch || !el.nextSibling) continue;
+
                                if (!atOrBeforeCaret(parentNode, elIndex + 1)) {
                                        done = true;
+
                                        return;
                                }
+
                                total += 1;
                                first = false;
+
                                continue;
                        }
 
@@ -422,22 +489,29 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
                                if (!first) {
                                        if (!atOrBeforeCaret(el, 0)) {
                                                done = true;
+
                                                return;
                                        }
+
                                        total += 1;
                                }
+
                                walk(el);
                                first = false;
+
                                continue;
                        }
 
                        const before = total;
+
                        walk(el);
+
                        if (total > before) first = false;
                }
        };
 
        walk(root);
+
        return total;
 }
 
@@ -463,20 +537,25 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment {
                        if (tokens[index - 1]?.kind === 'codeBlock' && text.startsWith('\n')) {
                                text = text.slice(1);
                        }
+
                        if (tokens[index + 1]?.kind === 'codeBlock' && text.endsWith('\n')) {
                                text = text.slice(0, -1);
                        }
+
                        if (text.length === 0) continue;
 
                        fragment.appendChild(document.createTextNode(text));
+
                        continue;
                }
 
                if (token.kind === 'inlineCode' || token.kind === 'codeBlock') {
                        const code = document.createElement('code');
+
                        code.dataset.codeToken = token.kind === 'codeBlock' ? 'block' : 'inline';
                        code.textContent = token.text;
                        fragment.appendChild(code);
+
                        continue;
                }
 
@@ -488,6 +567,7 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment {
                }
 
                const badge = document.createElement('span');
+
                badge.dataset.mentionBadge = 'true';
                badge.dataset.mentionName = token.name;
                badge.dataset.mentionPath = token.path;
@@ -496,6 +576,7 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment {
                badge.contentEditable = 'false';
 
                const svg = document.createElementNS(MENTION_BADGE_SVG_ATTRIBUTES['xmlns'], 'svg');
+
                for (const [attr, value] of Object.entries(MENTION_BADGE_SVG_ATTRIBUTES)) {
                        svg.setAttribute(attr, value);
                }
@@ -505,11 +586,13 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment {
 
                for (const d of getMentionBadgeIconPaths(token.path)) {
                        const path = document.createElementNS(MENTION_BADGE_SVG_ATTRIBUTES['xmlns'], 'path');
+
                        path.setAttribute('d', d);
                        svg.appendChild(path);
                }
 
                const label = document.createElement('span');
+
                label.classList.add('shrink-0', 'truncate');
                label.textContent = getMentionBadgeLabel(
                        token.name,
@@ -530,7 +613,9 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment {
 // (badge, another block, an existing hatch) or a non-empty text node.
 function hasLineBeside(node: Node | null): boolean {
        if (!node) return false;
+
        if (node.nodeType === Node.ELEMENT_NODE) return true;
+
        return (node.textContent ?? '') !== '';
 }
 
@@ -561,6 +646,7 @@ export function syncCodeBlockHatches(root: HTMLElement) {
                // A `<br>` with no code block around is a real newline (browser
                // Shift+Enter shape) and stays.
                let prevElement = child.previousSibling;
+
                while (prevElement && prevElement.nodeType !== Node.ELEMENT_NODE) {
                        prevElement = prevElement.previousSibling;
                }
@@ -601,14 +687,17 @@ export function stripBlockBoundaryLineBreaks(root: HTMLElement): boolean {
 
        for (const child of Array.from(root.childNodes)) {
                if (child.nodeType !== Node.TEXT_NODE) continue;
+
                if (!isCodeBlockElement(child.previousSibling)) continue;
 
                let text = child.textContent ?? '';
+
                if (!/^\n{2,}$/.test(text)) continue;
 
                text = text.slice(1);
 
                const atBufferEnd = !child.nextSibling || child.nextSibling.nodeName === 'BR';
+
                if (atBufferEnd) {
                        text = text.slice(0, -1);
                }
@@ -634,12 +723,15 @@ export function badgeAwareWordJump(
        direction: 'forward' | 'backward'
 ): number | null {
        let masked = '';
+
        const badgeSpans: Array<[number, number]> = [];
 
        for (const token of tokenizeContent(source)) {
                const len =
                        token.kind === 'badge' ? badgeSourceLength(token.name, token.path) : token.text.length;
+
                if (token.kind === 'badge') badgeSpans.push([masked.length, masked.length + len]);
+
                masked += token.kind === 'badge' ? 'a'.repeat(len) : token.text;
        }
 
@@ -649,6 +741,7 @@ export function badgeAwareWordJump(
        const spanStartingAt = (index: number) => badgeSpans.find(([start]) => start === index);
        const spanEndingAt = (index: number) => badgeSpans.find(([, end]) => end === index);
        const n = masked.length;
+
        let i = offset;
 
        if (direction === 'forward') {
@@ -656,24 +749,32 @@ export function badgeAwareWordJump(
                if (!(i < n && isWord(i))) {
                        while (i < n && !isWord(i)) i++;
                }
+
                while (i < n && isWord(i)) {
                        const span = spanStartingAt(i);
+
                        if (span) {
                                i = span[1];
+
                                break;
                        }
+
                        i++;
                }
        } else {
                if (!(i > 0 && isWord(i - 1))) {
                        while (i > 0 && !isWord(i - 1)) i--;
                }
+
                while (i > 0 && isWord(i - 1)) {
                        const span = spanEndingAt(i);
+
                        if (span) {
                                i = span[0];
+
                                break;
                        }
+
                        i--;
                }
        }
@@ -682,6 +783,7 @@ export function badgeAwareWordJump(
 
        const lo = Math.min(offset, i);
        const hi = Math.max(offset, i);
+
        return badgeSpans.some(([start, end]) => start < hi && end > lo) ? i : null;
 }
 
@@ -692,7 +794,9 @@ export function badgeAwareWordJump(
  */
 export function leadingBadgeEdgeOffset(source: string, caret: number): number | null {
        const [first] = tokenizeContent(source);
+
        if (!first || first.kind !== 'badge') return null;
+
        return caret === badgeSourceLength(first.name, first.path) ? 0 : null;
 }
 
@@ -708,6 +812,7 @@ export function leadingBadgeEdgeOffset(source: string, caret: number): number |
  */
 export function textOffsetToRange(root: HTMLElement, offset: number): Range {
        const range = document.createRange();
+
        let remaining = offset;
        let landed = false;
        let pendingBlockBoundary = false;
@@ -717,7 +822,6 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
                range.setEnd(node, nodeOffset);
                landed = true;
        };
-
        const walk = (parent: Node) => {
                let first = true;
 
@@ -726,23 +830,32 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
 
                        if (child.nodeType === Node.TEXT_NODE) {
                                const text = child.textContent ?? '';
+
                                if (text.length === 0) continue;
+
                                if (pendingBlockBoundary) {
                                        // The synthesized separator maps to the near edge of the
                                        // content that follows the block.
                                        pendingBlockBoundary = false;
+
                                        if (remaining === 0) {
                                                land(child, 0);
+
                                                return;
                                        }
+
                                        remaining -= 1;
                                }
+
                                if (remaining <= text.length) {
                                        land(child, remaining);
+
                                        return;
                                }
+
                                remaining -= text.length;
                                first = false;
+
                                continue;
                        }
 
@@ -752,17 +865,23 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
 
                        if (el.dataset.mentionBadge === 'true') {
                                const len = badgeSourceLength(el.dataset.mentionName ?? '', el.dataset.mentionPath ?? '');
+
                                if (len === 0) continue;
+
                                if (pendingBlockBoundary) {
                                        pendingBlockBoundary = false;
+
                                        if (remaining === 0) {
                                                range.setStartBefore(el);
                                                range.setEndBefore(el);
                                                landed = true;
+
                                                return;
                                        }
+
                                        remaining -= 1;
                                }
+
                                if (remaining <= len) {
                                        if (remaining === 0) {
                                                range.setStartBefore(el);
@@ -771,53 +890,72 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
                                                range.setStartAfter(el);
                                                range.setEndAfter(el);
                                        }
+
                                        landed = true;
+
                                        return;
                                }
+
                                remaining -= len;
                                first = false;
+
                                continue;
                        }
 
                        if (el.dataset.codeToken !== undefined) {
                                const isBlock = el.dataset.codeToken === 'block';
+
                                if (isBlock && (pendingBlockBoundary || !first)) {
                                        pendingBlockBoundary = false;
+
                                        if (remaining === 0) {
                                                range.setStartBefore(el);
                                                range.setEndBefore(el);
                                                landed = true;
+
                                                return;
                                        }
+
                                        remaining -= 1;
                                }
 
                                const len = (el.textContent ?? '').length;
+
                                if (remaining === 0) {
                                        range.setStartBefore(el);
                                        range.setEndBefore(el);
                                        landed = true;
+
                                        return;
                                }
+
                                if (remaining === len) {
                                        range.setStartAfter(el);
                                        range.setEndAfter(el);
                                        landed = true;
+
                                        return;
                                }
+
                                if (remaining < len) {
                                        walk(el);
+
                                        return;
                                }
+
                                remaining -= len;
+
                                if (isBlock) remaining -= 1;
+
                                first = false;
+
                                continue;
                        }
 
                        if (el.tagName === 'BR') {
                                const isHatch =
                                        isCodeBlockElement(el.previousSibling) || isCodeBlockElement(el.nextSibling);
+
                                if (isHatch) {
                                        // Escape hatch: no source length; offset 0 lands before it
                                        // so text typed there takes its place.
@@ -826,49 +964,66 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
                                                range.setEndBefore(el);
                                                landed = true;
                                        }
+
                                        continue;
                                }
+
                                if (!el.nextSibling) continue;
+
                                if (pendingBlockBoundary) {
                                        pendingBlockBoundary = false;
+
                                        if (remaining === 0) {
                                                range.setStartBefore(el);
                                                range.setEndBefore(el);
                                                landed = true;
+
                                                return;
                                        }
+
                                        remaining -= 1;
                                }
+
                                if (remaining === 0) {
                                        range.setStartBefore(el);
                                        range.setEndBefore(el);
                                        landed = true;
+
                                        return;
                                }
+
                                remaining -= 1;
                                first = false;
+
                                continue;
                        }
 
                        if (BLOCK_TAG_NAMES.has(el.tagName)) {
                                if (pendingBlockBoundary || !first) {
                                        pendingBlockBoundary = false;
+
                                        if (remaining === 0) {
                                                // The boundary newline belongs to the previous line.
                                                range.setStartBefore(el);
                                                range.setEndBefore(el);
                                                landed = true;
+
                                                return;
                                        }
+
                                        remaining -= 1;
                                }
+
                                walk(el);
                                first = false;
+
                                continue;
                        }
 
                        const before = remaining;
+
                        walk(el);
+
                        if (remaining < before) first = false;
                }
        };
@@ -877,6 +1032,7 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
 
        if (!landed) {
                const last = root.lastChild;
+
                if (last && last.nodeName === 'BR') {
                        range.setStartBefore(last);
                        range.setEndBefore(last);
index af404445f7889c85184329af9314f617a4408619..449ebcba609bfe8df492109d557647089c2021ba 100644 (file)
@@ -1,14 +1,14 @@
 import { convertPDFToImage, convertPDFToText } from './pdf-processing';
 import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
+import { isLikelyTextFile, readFileAsText } from './text-files';
 import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
-import { FileTypeCategory, AttachmentType, SpecialFileType } from '$lib/enums';
 import { SETTINGS_KEYS } from '$lib/constants';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { AttachmentType, FileTypeCategory, SpecialFileType } from '$lib/enums';
 import { modelsStore } from '$lib/stores/models.svelte';
+import { config, settingsStore } from '$lib/stores/settings.svelte';
+import type { ChatUploadedFile, DatabaseMessageExtra, FileProcessingResult } from '$lib/types';
 import { getFileTypeCategory } from '$lib/utils';
-import { readFileAsText, isLikelyTextFile } from './text-files';
 import { toast } from 'svelte-sonner';
-import type { FileProcessingResult, ChatUploadedFile, DatabaseMessageExtra } from '$lib/types';
 
 function readFileAsBase64(file: File): Promise<string> {
        return new Promise((resolve, reject) => {
@@ -18,6 +18,7 @@ function readFileAsBase64(file: File): Promise<string> {
                        // Extract base64 data without the data URL prefix
                        const dataUrl = reader.result as string;
                        const base64 = dataUrl.split(',')[1];
+
                        resolve(base64);
                };
 
@@ -37,13 +38,13 @@ export async function parseFilesToMessageExtras(
        for (const file of files) {
                if (file.type === SpecialFileType.MCP_PROMPT && file.mcpPrompt) {
                        extras.push({
-                               type: AttachmentType.MCP_PROMPT,
+                               arguments: file.mcpPrompt.arguments,
+                               content: file.textContent ?? '',
                                name: file.name,
-                               size: file.size,
-                               serverName: file.mcpPrompt.serverName,
                                promptName: file.mcpPrompt.promptName,
-                               content: file.textContent ?? '',
-                               arguments: file.mcpPrompt.arguments
+                               serverName: file.mcpPrompt.serverName,
+                               size: file.size,
+                               type: AttachmentType.MCP_PROMPT
                        });
 
                        continue;
@@ -68,10 +69,10 @@ export async function parseFilesToMessageExtras(
                                }
 
                                extras.push({
-                                       type: AttachmentType.IMAGE,
+                                       base64Url,
                                        name: file.name,
                                        size: file.size,
-                                       base64Url
+                                       type: AttachmentType.IMAGE
                                });
                        }
                } else if (getFileTypeCategory(file.type) === FileTypeCategory.AUDIO) {
@@ -80,11 +81,11 @@ export async function parseFilesToMessageExtras(
                                const base64Data = await readFileAsBase64(file.file);
 
                                extras.push({
-                                       type: AttachmentType.AUDIO,
+                                       base64Data: base64Data,
+                                       mimeType: file.type,
                                        name: file.name,
                                        size: file.size,
-                                       base64Data: base64Data,
-                                       mimeType: file.type
+                                       type: AttachmentType.AUDIO
                                });
                        } catch (error) {
                                console.error(`Failed to process audio file ${file.name}:`, error);
@@ -95,11 +96,11 @@ export async function parseFilesToMessageExtras(
                                const base64Data = await readFileAsBase64(file.file);
 
                                extras.push({
-                                       type: AttachmentType.VIDEO,
+                                       base64Data: base64Data,
+                                       mimeType: file.type,
                                        name: file.name,
                                        size: file.size,
-                                       base64Data: base64Data,
-                                       mimeType: file.type
+                                       type: AttachmentType.VIDEO
                                });
                        } catch (error) {
                                console.error(`Failed to process video file ${file.name}:`, error);
@@ -149,13 +150,13 @@ export async function parseFilesToMessageExtras(
                                                );
 
                                                extras.push({
-                                                       type: AttachmentType.PDF,
-                                                       name: file.name,
-                                                       size: file.size,
+                                                       base64Data: base64Data,
                                                        content: `PDF file with ${images.length} pages`,
                                                        images: images,
+                                                       name: file.name,
                                                        processedAsImages: true,
-                                                       base64Data: base64Data
+                                                       size: file.size,
+                                                       type: AttachmentType.PDF
                                                });
                                        } catch (imageError) {
                                                console.warn(
@@ -167,12 +168,12 @@ export async function parseFilesToMessageExtras(
                                                const content = await convertPDFToText(file.file);
 
                                                extras.push({
-                                                       type: AttachmentType.PDF,
-                                                       name: file.name,
-                                                       size: file.size,
+                                                       base64Data: base64Data,
                                                        content: content,
+                                                       name: file.name,
                                                        processedAsImages: false,
-                                                       base64Data: base64Data
+                                                       size: file.size,
+                                                       type: AttachmentType.PDF
                                                });
                                        }
                                } else {
@@ -185,12 +186,12 @@ export async function parseFilesToMessageExtras(
                                        });
 
                                        extras.push({
-                                               type: AttachmentType.PDF,
-                                               name: file.name,
-                                               size: file.size,
+                                               base64Data: base64Data,
                                                content: content,
+                                               name: file.name,
                                                processedAsImages: false,
-                                               base64Data: base64Data
+                                               size: file.size,
+                                               type: AttachmentType.PDF
                                        });
                                }
                        } catch (error) {
@@ -206,10 +207,10 @@ export async function parseFilesToMessageExtras(
                                        emptyFiles.push(file.name);
                                } else if (isLikelyTextFile(content)) {
                                        extras.push({
-                                               type: AttachmentType.TEXT,
+                                               content: content,
                                                name: file.name,
                                                size: file.size,
-                                               content: content
+                                               type: AttachmentType.TEXT
                                        });
                                } else {
                                        console.warn(`File ${file.name} appears to be binary and will be skipped`);
@@ -220,5 +221,5 @@ export async function parseFilesToMessageExtras(
                }
        }
 
-       return { extras, emptyFiles };
+       return { emptyFiles, extras };
 }
index 26a60533ae6fc986f809a4446629785356c47c2f..c03b5f06141acf16ad29eb3d42d745258aa648da 100644 (file)
@@ -16,11 +16,13 @@ export function getFileTypeLabel(input: string | undefined): string {
        // Handle MIME types (contains '/')
        if (input.includes('/')) {
                const subtype = input.split('/').pop();
+
                if (subtype) {
                        // Handle special cases like 'vnd.ms-excel' → 'EXCEL'
                        if (subtype.includes('.')) {
                                return subtype.split('.').pop()?.toUpperCase() || 'FILE';
                        }
+
                        return subtype.toUpperCase();
                }
        }
@@ -28,6 +30,7 @@ export function getFileTypeLabel(input: string | undefined): string {
        // Handle file names (contains '.')
        if (input.includes('.')) {
                const ext = input.split('.').pop();
+
                if (ext) return ext.toUpperCase();
        }
 
index e61564174e8d78bec875a6b1f2880268f80ee5e0..fd8828fc139803829d35d441886cb149b8a198e0 100644 (file)
@@ -1,9 +1,9 @@
 import {
        AUDIO_FILE_TYPES,
-       VIDEO_FILE_TYPES,
        IMAGE_FILE_TYPES,
        PDF_FILE_TYPES,
-       TEXT_FILE_TYPES
+       TEXT_FILE_TYPES,
+       VIDEO_FILE_TYPES
 } from '$lib/constants';
 import {
        FileExtensionAudio,
@@ -13,9 +13,9 @@ import {
        FileTypeCategory,
        MimeTypeApplication,
        MimeTypeAudio,
-       MimeTypeVideo,
        MimeTypeImage,
-       MimeTypeText
+       MimeTypeText,
+       MimeTypeVideo
 } from '$lib/enums';
 
 function normalizeMimeType(mimeType: string): string {
@@ -224,6 +224,7 @@ export function isFileTypeSupported(filename: string, mimeType?: string): boolea
        // Images are detected and handled separately for vision models
        if (mimeType) {
                const category = getFileTypeCategory(mimeType);
+
                if (
                        category === FileTypeCategory.IMAGE ||
                        category === FileTypeCategory.AUDIO ||
@@ -235,6 +236,7 @@ export function isFileTypeSupported(filename: string, mimeType?: string): boolea
 
        // Check extension for known types (especially images without MIME)
        const extCategory = getFileTypeCategoryByExtension(filename);
+
        if (
                extCategory === FileTypeCategory.IMAGE ||
                extCategory === FileTypeCategory.AUDIO ||
index 24a2c1c94c1899f11bf45ad0c864664861c1a6f5..27555a47be669fa3f639450106625072d15bd880 100644 (file)
@@ -1,9 +1,9 @@
 import {
+       MEDIUM_DURATION_THRESHOLD,
        MS_PER_SECOND,
-       SECONDS_PER_MINUTE,
        SECONDS_PER_HOUR,
-       SHORT_DURATION_THRESHOLD,
-       MEDIUM_DURATION_THRESHOLD
+       SECONDS_PER_MINUTE,
+       SHORT_DURATION_THRESHOLD
 } from '$lib/constants';
 
 /**
@@ -15,6 +15,7 @@ import {
  */
 export function formatFileSize(bytes: number | unknown): string {
        if (typeof bytes !== 'number') return 'Unknown';
+
        if (bytes === 0) return '0 Bytes';
 
        const k = 1024;
@@ -70,6 +71,7 @@ export function formatNumber(num: number | unknown): string {
 export function formatJsonPretty(jsonString: string): string {
        try {
                const parsed = JSON.parse(jsonString);
+
                return JSON.stringify(parsed, null, 2);
        } catch {
                return jsonString;
@@ -84,8 +86,8 @@ export function formatJsonPretty(jsonString: string): string {
  */
 export function formatTime(date: Date): string {
        return date.toLocaleTimeString('en-US', {
-               hour12: false,
                hour: '2-digit',
+               hour12: false,
                minute: '2-digit',
                second: '2-digit'
        });
@@ -114,7 +116,6 @@ export function formatPerformanceTime(ms: number): string {
        const hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
        const minutes = Math.floor((totalSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
        const seconds = Math.floor(totalSeconds % SECONDS_PER_MINUTE);
-
        const parts: string[] = [];
 
        if (hours > 0) {
@@ -149,5 +150,6 @@ export function formatAttachmentText(
        extra?: string
 ): string {
        const header = extra ? `${name} (${extra})` : name;
+
        return `\n\n--- ${label}: ${header} ---\n${content}`;
 }
index 66f7a8f6a4f90b65883b3257dd0aa43fd1ef74c2..c7d5bca79bd57b809cb8a02c8f93cb28901c0471 100644 (file)
@@ -4,22 +4,22 @@
  * result instead of re-walking the tree.
  */
 
-import { BuiltInTool, GlobSearchType } from '$lib/enums';
-import { ToolsService } from '$lib/services/tools.service';
+import { lastPathSegment } from './path-display';
+import {
+       buildGlobSearchArgs,
+       type GlobEntry,
+       type GlobSearchArgs,
+       joinPath,
+       rankEntries
+} from './working-directory';
 import {
        GLOB_WILDCARD,
        PATH_NAV_MAX_DEPTH,
        PATH_SEPARATOR,
        WINDOWS_SEPARATOR
 } from '$lib/constants';
-import { lastPathSegment } from './path-display';
-import {
-       buildGlobSearchArgs,
-       joinPath,
-       rankEntries,
-       type GlobEntry,
-       type GlobSearchArgs
-} from './working-directory';
+import { BuiltInTool, GlobSearchType } from '$lib/enums';
+import { ToolsService } from '$lib/services/tools.service';
 
 const SEARCH_CACHE_TTL_MS = 2000;
 
@@ -45,13 +45,14 @@ export async function runGlobSearch(
 ): Promise<GlobSearchResult> {
        const key = `${type}\u0000${args.path}\u0000${args.include}\u0000${args.maxDepth}\u0000${limit}`;
        const cached = searchCache.get(key);
+
        if (cached && Date.now() - cached.at < SEARCH_CACHE_TTL_MS) {
                return { base: cached.base, entries: cached.results };
        }
 
        const res = await ToolsService.executeToolRaw(
                BuiltInTool.FILE_GLOB_SEARCH,
-               { path: args.path, type, include: args.include, max_depth: args.maxDepth, limit },
+               { include: args.include, limit, max_depth: args.maxDepth, path: args.path, type },
                signal
        );
 
@@ -60,11 +61,13 @@ export async function runGlobSearch(
        const base = typeof res.base === 'string' ? res.base : '';
        const entries = Array.isArray(res.entries) ? (res.entries as GlobEntry[]) : [];
        const now = Date.now();
+
        // prune stale entries so the short-lived cache cannot grow unbounded
        for (const [k, v] of searchCache) {
                if (now - v.at >= SEARCH_CACHE_TTL_MS) searchCache.delete(k);
        }
-       searchCache.set(key, { results: entries, base, at: now });
+       searchCache.set(key, { at: now, base, results: entries });
+
        return { base, entries };
 }
 
@@ -93,7 +96,7 @@ export interface GlobSearchChildResult {
 }
 
 function toEntryResult(e: GlobEntry, base: string): GlobEntryResult {
-       return { path: joinPath(base, e.path), name: lastPathSegment(e.path), type: e.type };
+       return { name: lastPathSegment(e.path), path: joinPath(base, e.path), type: e.type };
 }
 
 /**
@@ -110,19 +113,19 @@ export async function runGlobSearchWithChildren(
        options: GlobSearchChildOptions = {}
 ): Promise<GlobSearchChildResult> {
        const {
-               type = GlobSearchType.ALL,
+               childMaxDepth = PATH_NAV_MAX_DEPTH,
                descendOnTrailingSeparator = false,
-               childMaxDepth = PATH_NAV_MAX_DEPTH
+               type = GlobSearchType.ALL
        } = options;
-
        const args = buildGlobSearchArgs(query, scopePath, searchDepth);
        const res = await runGlobSearch(args, type, limit, signal);
-       if (res.error) return { base: res.base, args, entries: [], error: res.error };
+
+       if (res.error) return { args, base: res.base, entries: [], error: res.error };
 
        const ranked = rankEntries(res.entries, args.rankQuery);
        const entries = ranked.map((e) => toEntryResult(e, res.base));
-
        const last = args.last;
+
        if (last) {
                const wantsDescend = descendOnTrailingSeparator
                        ? query.endsWith(PATH_SEPARATOR) || query.endsWith(WINDOWS_SEPARATOR)
@@ -130,22 +133,25 @@ export async function runGlobSearchWithChildren(
                const exact = ranked.find(
                        (e) => e.type === 'dir' && lastPathSegment(e.path).toLowerCase() === last.toLowerCase()
                );
+
                if (wantsDescend && exact) {
                        const exactDir = joinPath(res.base, exact.path);
                        const childRes = await runGlobSearch(
-                               { path: exactDir, include: GLOB_WILDCARD, maxDepth: childMaxDepth, rankQuery: '' },
+                               { include: GLOB_WILDCARD, maxDepth: childMaxDepth, path: exactDir, rankQuery: '' },
                                type,
                                limit,
                                signal
                        );
+
                        if (!childRes.error) {
                                const children = childRes.entries
                                        .map((e) => toEntryResult(e, childRes.base))
                                        .sort((a, b) => a.path.localeCompare(b.path));
-                               return { base: res.base, args, entries: [...entries, ...children], exactDir };
+
+                               return { args, base: res.base, entries: [...entries, ...children], exactDir };
                        }
                }
        }
 
-       return { base: res.base, args, entries };
+       return { args, base: res.base, entries };
 }
index 0b907b8300331a51915027204cb4aee2b787c944..ec54ca2e71a6530463c489468456c41bf70874d4 100644 (file)
@@ -12,6 +12,7 @@ export function parseHeadersToArray(headersJson: string): { key: string; value:
 
        try {
                const parsed = JSON.parse(headersJson);
+
                if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
                        return Object.entries(parsed).map(([key, value]) => ({
                                key,
index 4ccb992a3b32d02ebfda25b5dfba8043515830bc..143b713bf6193dccbf3b82f4769146e1e2f6e947 100644 (file)
@@ -1,5 +1,5 @@
-import { MimeTypeImage } from '$lib/enums';
 import { HEIC_JPEG_QUALITY } from '$lib/constants/image-size';
+import { MimeTypeImage } from '$lib/enums';
 
 // heic requires a relatively large decoder, in order to reduce primary bundle size
 // we lazily load this decoder from a CDN when needed, and cache it for future conversions
@@ -32,12 +32,13 @@ export async function heicFileToJpegDataURL(file: File | Blob): Promise<string>
        const { heicTo } = await getHeicTo();
        const jpegBlob = await heicTo({
                blob: file,
-               type: MimeTypeImage.JPEG,
-               quality: HEIC_JPEG_QUALITY
+               quality: HEIC_JPEG_QUALITY,
+               type: MimeTypeImage.JPEG
        });
 
        return new Promise((resolve, reject) => {
                const reader = new FileReader();
+
                reader.onload = () => resolve(reader.result as string);
                reader.onerror = () => reject(reader.error);
                reader.readAsDataURL(jpegBlob);
index 15c21017ade885ae7edbf638e6fa1c57e1ffdd44..d0dfb9271359e60722127f8382d79a77aa58a728 100644 (file)
@@ -1,13 +1,13 @@
 import {
+       APP1_MARKER,
+       EXIF_ORIENTATION_TAG,
        EXIF_SCAN_BYTE_LIMIT,
+       EXIF_SIGNATURE,
+       IFD_ENTRY_SIZE,
        JPEG_SOI_MARKER,
-       APP1_MARKER,
        SOS_MARKER,
-       EXIF_SIGNATURE,
        TIFF_LITTLE_ENDIAN,
-       TIFF_MAGIC,
-       EXIF_ORIENTATION_TAG,
-       IFD_ENTRY_SIZE
+       TIFF_MAGIC
 } from '$lib/constants/jpeg-exif';
 import { MimeTypeImage } from '$lib/enums';
 
index bbeed825006d757a076cef9d7e83879002b5cf29..acf4d4a702c656af37fb48bdedf387d5c40558c8 100644 (file)
@@ -15,10 +15,10 @@ import {
        LATEX_INLINE_CONVERT_REGEXP,
        LATEX_INLINE_DELIMITER,
        LATEX_INLINE_OPEN,
+       LATEX_LINEBREAK_REGEXP,
        LATEX_MATH_AND_CODE_PATTERN,
        LATEX_MHCHEM_CE,
        LATEX_MHCHEM_PU,
-       LATEX_LINEBREAK_REGEXP,
        LATEX_NEIGHBOR_CHAR_REGEXP,
        LATEX_NON_WHITESPACE_REGEXP,
        LATEX_PLACEHOLDER_REGEXP,
@@ -45,6 +45,7 @@ export function maskInlineLaTeX(content: string, latexExpressions: string[]): st
        if (!content.includes(LATEX_INLINE_DELIMITER)) {
                return content;
        }
+
        return content
                .split(NEWLINE)
                .map((line) => {
@@ -60,6 +61,7 @@ export function maskInlineLaTeX(content: string, latexExpressions: string[]): st
 
                                if (openDollarIndex == -1) {
                                        processedLine += line.slice(currentPosition);
+
                                        break;
                                }
 
@@ -68,6 +70,7 @@ export function maskInlineLaTeX(content: string, latexExpressions: string[]): st
 
                                if (closeDollarIndex == -1) {
                                        processedLine += line.slice(currentPosition);
+
                                        break;
                                }
 
@@ -107,6 +110,7 @@ export function maskInlineLaTeX(content: string, latexExpressions: string[]): st
                                // Treat as LaTeX
                                processedLine += line.slice(currentPosition, openDollarIndex);
                                const latexContent = line.slice(openDollarIndex, closeDollarIndex + 1);
+
                                latexExpressions.push(latexContent);
                                processedLine += `<<LATEX_${latexExpressions.length - 1}>>`;
                                currentPosition = closeDollarIndex + 1;
@@ -147,7 +151,6 @@ function escapeMhchem(text: string): string {
 }
 
 const doEscapeMhchem = false;
-
 /**
  * Preprocesses markdown content to safely handle LaTeX math expressions while protecting
  * against false positives (e.g., dollar amounts like $5.99) and ensuring proper rendering.
@@ -179,6 +182,7 @@ export function preprocessLaTeX(content: string): string {
        // incomplete code block stays the same across multiple tokens, so the
        // full protect/restore pipeline would re-run unnecessarily.
        const cached = latexCache.get(content);
+
        if (cached !== undefined) return cached;
 
        // Save original before the function mutates `content` through steps 0-8
@@ -193,7 +197,9 @@ export function preprocessLaTeX(content: string): string {
                if (latexCache.size >= LATEX_CACHE_MAX_SIZE) {
                        latexCache.delete(latexCache.keys().next().value!);
                }
+
                latexCache.set(originalContent, content);
+
                return content;
        }
 
@@ -203,12 +209,16 @@ export function preprocessLaTeX(content: string): string {
        const lines = content.split(NEWLINE);
        const processedLines = lines.map((line, index) => {
                const match = line.match(LATEX_BLOCKQUOTE_PREFIX_REGEXP);
+
                if (match) {
                        blockquoteMarkers.set(index, match[1]);
+
                        return line.slice(match[1].length);
                }
+
                return line;
        });
+
        content = processedLines.join(NEWLINE);
 
        // Step 1: Protect code blocks
@@ -232,7 +242,9 @@ export function preprocessLaTeX(content: string): string {
                        if (group1.endsWith(LATEX_BACKSLASH)) {
                                return match; // Backslash before \[, do nothing.
                        }
+
                        const hasSuffix = LATEX_NON_WHITESPACE_REGEXP.test(group3);
+
                        let optBreak;
 
                        if (hasSuffix) {
@@ -264,15 +276,19 @@ export function preprocessLaTeX(content: string): string {
        // Step 4: Restore protected LaTeX expressions (they are valid)
        content = content.replace(LATEX_PLACEHOLDER_REGEXP, (_, index) => {
                let expr = latexExpressions[parseInt(index)];
+
                const match = expr.match(LATEX_LINEBREAK_REGEXP);
+
                if (match) {
                        // Katex: The $$-delimiters should be in their own line
                        // if there are \\-line-breaks.
                        const formula = match[1];
                        const prefix = formula.startsWith(NEWLINE) ? '' : NEWLINE;
                        const suffix = formula.endsWith(NEWLINE) ? '' : NEWLINE;
+
                        expr = LATEX_DISPLAY_DELIMITER + prefix + formula + suffix + LATEX_DISPLAY_DELIMITER;
                }
+
                return expr;
        });
 
@@ -313,14 +329,17 @@ export function preprocessLaTeX(content: string): string {
                const finalLines = content.split(NEWLINE);
                const restoredLines = finalLines.map((line, index) => {
                        const marker = blockquoteMarkers.get(index);
+
                        return marker ? marker + line : line;
                });
+
                content = restoredLines.join(NEWLINE);
        }
 
        if (latexCache.size >= LATEX_CACHE_MAX_SIZE) {
                latexCache.delete(latexCache.keys().next().value!);
        }
+
        latexCache.set(originalContent, content);
 
        return content;
index c5a98d1e05f36b53f5ccc92c121128b024fbf2af..c4493d870362ebb1031b426b5d1ae42195cde21b 100644 (file)
@@ -1,40 +1,40 @@
-import type { MCPServerSettingsEntry, MCPResourceContent, MCPResourceInfo } from '$lib/types';
-import {
-       MCPTransportType,
-       MCPLogLevel,
-       UrlProtocol,
-       MimeTypePrefix,
-       MimeTypeIncludes,
-       UriPattern,
-       MimeTypeText
-} from '$lib/enums';
-import {
-       MCP_SERVER_ID_PREFIX,
-       IMAGE_FILE_EXTENSION_REGEX,
-       CODE_FILE_EXTENSION_REGEX,
-       TEXT_FILE_EXTENSION_REGEX,
-       PROTOCOL_PREFIX_REGEX,
-       FILE_EXTENSION_REGEX,
-       DISPLAY_NAME_SEPARATOR_REGEX,
-       PATH_SEPARATOR,
-       RESOURCE_TEXT_CONTENT_SEPARATOR,
-       DEFAULT_RESOURCE_FILENAME,
-       MCP_SSE_ENDPOINT,
-       MCP_SSE_ENDPOINT_SLASH,
-       MCP_SSE_ENDPOINT_QUERY
-} from '$lib/constants';
 import {
+       AlertTriangle,
+       Code,
        Database,
        File,
        FileText,
        Image,
-       Code,
        Info,
-       AlertTriangle,
        XCircle
 } from '@lucide/svelte';
-import type { Component } from 'svelte';
+import {
+       CODE_FILE_EXTENSION_REGEX,
+       DEFAULT_RESOURCE_FILENAME,
+       DISPLAY_NAME_SEPARATOR_REGEX,
+       FILE_EXTENSION_REGEX,
+       IMAGE_FILE_EXTENSION_REGEX,
+       MCP_SERVER_ID_PREFIX,
+       MCP_SSE_ENDPOINT,
+       MCP_SSE_ENDPOINT_QUERY,
+       MCP_SSE_ENDPOINT_SLASH,
+       PATH_SEPARATOR,
+       PROTOCOL_PREFIX_REGEX,
+       RESOURCE_TEXT_CONTENT_SEPARATOR,
+       TEXT_FILE_EXTENSION_REGEX
+} from '$lib/constants';
+import {
+       MCPLogLevel,
+       MCPTransportType,
+       MimeTypeIncludes,
+       MimeTypePrefix,
+       MimeTypeText,
+       UriPattern,
+       UrlProtocol
+} from '$lib/enums';
+import type { MCPResourceContent, MCPResourceInfo, MCPServerSettingsEntry } from '$lib/types';
 import type { MimeTypeUnion } from '$lib/types/common';
+import type { Component } from 'svelte';
 
 /**
  * Detects the MCP transport type from a URL.
@@ -73,6 +73,7 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
 
        if (typeof rawServers === 'string') {
                const trimmed = rawServers.trim();
+
                if (!trimmed) return [];
 
                try {
@@ -97,12 +98,12 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
                                : `${MCP_SERVER_ID_PREFIX}-${index + 1}`;
 
                return {
-                       id,
-                       enabled: Boolean((entry as { enabled?: unknown })?.enabled),
-                       url,
-                       name: (entry as { name?: string })?.name,
                        displayName: (entry as { displayName?: string })?.displayName,
+                       enabled: Boolean((entry as { enabled?: unknown })?.enabled),
                        headers: headers || undefined,
+                       id,
+                       name: (entry as { name?: string })?.name,
+                       url,
                        useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)
                } satisfies MCPServerSettingsEntry;
        });
@@ -161,6 +162,7 @@ export function isImageMimeType(mimeType?: MimeTypeUnion): boolean {
 export function parseResourcePath(uri: string): string[] {
        try {
                const withoutProtocol = uri.replace(PROTOCOL_PREFIX_REGEX, '');
+
                return withoutProtocol.split(PATH_SEPARATOR).filter((p) => p.length > 0);
        } catch {
                return [uri];
@@ -176,6 +178,7 @@ export function parseResourcePath(uri: string): string[] {
  */
 export function getDisplayName(pathPart: string): string {
        const withoutExt = pathPart.replace(FILE_EXTENSION_REGEX, '');
+
        return withoutExt
                .split(DISPLAY_NAME_SEPARATOR_REGEX)
                .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
@@ -191,6 +194,7 @@ export function getDisplayName(pathPart: string): string {
 export function getResourceDisplayName(resource: MCPResourceInfo): string {
        try {
                const parts = parseResourcePath(resource.uri);
+
                return parts[parts.length - 1] || resource.name || resource.uri;
        } catch {
                return resource.name || resource.uri;
@@ -207,6 +211,7 @@ export function getResourceDisplayName(resource: MCPResourceInfo): string {
 export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean {
        const mime = mimeType?.toLowerCase() || '';
        const u = uri?.toLowerCase() || '';
+
        return (
                mime.includes(MimeTypeIncludes.JSON) ||
                mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
@@ -225,6 +230,7 @@ export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean
 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);
 }
 
@@ -271,6 +277,7 @@ export function getResourceIcon(mimeType?: MimeTypeUnion, uri?: string): Compone
  */
 export function getResourceTextContent(content: MCPResourceContent[] | null | undefined): string {
        if (!content) return '';
+
        return content
                .filter((c): c is { uri: string; mimeType?: MimeTypeUnion; text: string } => 'text' in c)
                .map((c) => c.text)
@@ -308,6 +315,7 @@ export function downloadResourceContent(
        const blob = new Blob([text], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
+
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
index ed0712b46f454d539c19d7c931f1766cc26e1aca..7b76a22800e596c0609fe1786dcd6f3fbc6df37b 100644 (file)
@@ -1,9 +1,9 @@
 import { abbreviateHome, lastPathSegment } from './path-display';
+import { FILE_URI_PREFIX } from '$lib/constants';
 import {
        MENTION_BADGE_FILE_ICON_PATHS,
        MENTION_BADGE_FOLDER_ICON_PATHS
 } from '$lib/constants/mention-badge';
-import { FILE_URI_PREFIX } from '$lib/constants';
 import { FileMentionEntryType } from '$lib/enums';
 import type { FileMentionEntry } from '$lib/types';
 
@@ -59,8 +59,11 @@ export function getMentionBadgeLabel(
        home?: string | null
 ): string {
        if (!showFullPath) return name;
+
        const decoded = decodeFileLinkPath(path.replace(/\/+$/, ''));
+
        if (!decoded) return name;
+
        return abbreviateHome(decoded, home);
 }
 
@@ -75,6 +78,7 @@ export function buildMentionInsertion(
        token: { start: number; end: number }
 ): { newValue: string; caretOffset: number } | null {
        if (token.start < 0 || token.end > value.length || token.start > token.end) return null;
+
        // Strip the entry's directory marker so it is not doubled below.
        const cleanedPath = entry.path.replace(/\/+$/, '');
        const pathWithSeparator =
@@ -82,5 +86,6 @@ export function buildMentionInsertion(
        const basename = lastPathSegment(cleanedPath) || entry.name;
        const insertion = `[${basename}](${FILE_URI_PREFIX}${encodeFileLinkPath(pathWithSeparator)}) `;
        const newValue = value.slice(0, token.start) + insertion + value.slice(token.end);
-       return { newValue, caretOffset: token.start + insertion.length };
+
+       return { caretOffset: token.start + insertion.length, newValue };
 }
index be26143609fadb0c4509bcefb48c729cadcd5afa..5c98af3051be1066a9cad801be0e900468cdf891 100644 (file)
@@ -27,29 +27,35 @@ export function findMentionToken(
        if (cursor <= 0 || cursor > value.length) return null;
 
        let atIndex = -1;
+
        for (let i = cursor - 1; i >= 0; i--) {
                const ch = value[i];
+
                if (ch === '@') {
                        const prev = i > 0 ? value[i - 1] : '';
+
                        if (i === 0 || TOKEN_BOUNDARY_CHARS.has(prev)) {
                                atIndex = i;
                        }
+
                        break;
                }
+
                if (TOKEN_BOUNDARY_CHARS.has(ch)) break;
        }
 
        if (atIndex === -1) return null;
 
        let end = atIndex + 1;
+
        while (end < value.length && !TOKEN_BOUNDARY_CHARS.has(value[end])) {
                end++;
        }
 
        return {
-               start: atIndex,
                end,
-               query: value.slice(atIndex + 1, end)
+               query: value.slice(atIndex + 1, end),
+               start: atIndex
        };
 }
 
@@ -68,6 +74,8 @@ export function takeMentionDismissSnapshot(
        cursor: number
 ): MentionDismissSnapshot | null {
        const token = findMentionToken(value, cursor);
+
        if (!token) return null;
-       return { start: token.start, query: token.query };
+
+       return { query: token.query, start: token.start };
 }
index bfdee75ce323e47586245e9e25a19d25a8de7f87..ca7fb3dc60a70f537f77a4f77d160caf358f901f 100644 (file)
@@ -3,9 +3,9 @@
  * Ensures only compatible file types are processed based on model capabilities
  */
 
-import { getFileTypeCategory } from '$lib/utils';
 import { FileTypeCategory } from '$lib/enums';
 import type { ModalityCapabilities } from '$lib/types';
+import { getFileTypeCategory } from '$lib/utils';
 
 /**
  * Check if a file type is supported by the given modalities
@@ -72,11 +72,11 @@ export function filterFilesByModalities(
        const supportedFiles: File[] = [];
        const unsupportedFiles: File[] = [];
        const modalityReasons: Record<string, string> = {};
-
-       const { hasVision, hasAudio, hasVideo } = capabilities;
+       const { hasAudio, hasVideo, hasVision } = capabilities;
 
        for (const file of files) {
                const category = getFileTypeCategory(file.type);
+
                let isSupported = true;
                let reason = '';
 
@@ -86,6 +86,7 @@ export function filterFilesByModalities(
                                        isSupported = false;
                                        reason = 'Images require a vision-capable model';
                                }
+
                                break;
 
                        case FileTypeCategory.AUDIO:
@@ -93,6 +94,7 @@ export function filterFilesByModalities(
                                        isSupported = false;
                                        reason = 'Audio files require an audio-capable model';
                                }
+
                                break;
 
                        case FileTypeCategory.VIDEO:
@@ -100,6 +102,7 @@ export function filterFilesByModalities(
                                        isSupported = false;
                                        reason = 'Video files require a video-capable model';
                                }
+
                                break;
 
                        case FileTypeCategory.TEXT:
@@ -121,7 +124,7 @@ export function filterFilesByModalities(
                }
        }
 
-       return { supportedFiles, unsupportedFiles, modalityReasons };
+       return { modalityReasons, supportedFiles, unsupportedFiles };
 }
 
 /**
@@ -138,23 +141,28 @@ export function generateModalityErrorMessage(
 ): string {
        if (unsupportedFiles.length === 0) return '';
 
-       const { hasVision, hasAudio, hasVideo } = capabilities;
+       const { hasAudio, hasVideo, hasVision } = capabilities;
 
        let message = '';
 
        if (unsupportedFiles.length === 1) {
                const file = unsupportedFiles[0];
                const reason = modalityReasons[file.name];
+
                message = `The file "${file.name}" cannot be uploaded: ${reason}.`;
        } else {
                const fileNames = unsupportedFiles.map((f) => f.name).join(', ');
+
                message = `The following files cannot be uploaded: ${fileNames}.`;
        }
 
        // Add helpful information about what is supported
        const supportedTypes: string[] = ['text files', 'PDFs'];
+
        if (hasVision) supportedTypes.push('images');
+
        if (hasAudio) supportedTypes.push('audio files');
+
        if (hasVideo) supportedTypes.push('video files');
 
        message += ` This model supports: ${supportedTypes.join(', ')}.`;
index 86d47d2f3a5712fcfb52c7f767821338a0967883..42d2ee25413af78e1097c33e320cf2f5f891cfba 100644 (file)
@@ -2,8 +2,10 @@ export function parseExecShellCommandError(
        toolResultString: string | undefined
 ): string | undefined {
        if (!toolResultString) return undefined;
+
        try {
                const parsed: unknown = JSON.parse(toolResultString);
+
                if (
                        parsed &&
                        typeof parsed === 'object' &&
@@ -15,5 +17,6 @@ export function parseExecShellCommandError(
        } catch {
                // Plain-text result = stdout/stderr, no structured error to surface.
        }
+
        return undefined;
 }
index b864e6b130e602e509bdfc14ba1b71db81a401e7..1f7ec557edd13059e1b7f7846bdf9630f3a828e7 100644 (file)
@@ -24,12 +24,13 @@ export function parseExecShellCommandExitStatus(
        if (!toolResultString) return undefined;
 
        const match = toolResultString.match(EXIT_CODE_TAIL_REGEX);
+
        if (!match) return undefined;
 
        return {
                code: Number.parseInt(match[1], 10),
-               timedOut: match[0].includes('exit due to timed out'),
-               rawText: match[0]
+               rawText: match[0],
+               timedOut: match[0].includes('exit due to timed out')
        };
 }
 
@@ -43,5 +44,6 @@ export function isExitCodeSummaryLine(
        status: ExecShellExitStatus | undefined
 ): boolean {
        if (!status) return false;
+
        return lineText.trim() === status.rawText.trim();
 }
index 58439bd6e30148052a54ca6f26f57aa3f883668e..49bc1bc7043463ef85ea1e5642ff3f5f36a36fa3 100644 (file)
@@ -7,13 +7,11 @@ const JSON_OBJECT_OPEN = '{';
 const JSON_OBJECT_CLOSE = '}';
 const JSON_ARRAY_OPEN = '[';
 const JSON_ARRAY_CLOSE = ']';
-
 // Trailing punctuation to strip before re-closing a partial object/array.
 // Matches an optional trailing comma plus any trailing whitespace; lets
 // us re-emit a syntactically-valid JSON document without an orphaned
 // comma when the model cut off mid-key.
 const TRAILING_JSON_PUNCTUATION_REGEX = /,?\s*$/;
-
 /** Bounded cache for parsePartialJsonArgs results. */
 const PARTIAL_JSON_CACHE_MAX_SIZE = 32;
 const partialJsonCache = new Map<string, Record<string, unknown> | null>();
@@ -22,6 +20,7 @@ function cacheResult(input: string, result: Record<string, unknown> | null): voi
        if (partialJsonCache.size >= PARTIAL_JSON_CACHE_MAX_SIZE) {
                partialJsonCache.delete(partialJsonCache.keys().next().value!);
        }
+
        partialJsonCache.set(input, result);
 }
 
@@ -32,12 +31,14 @@ function cacheResult(input: string, result: Record<string, unknown> | null): voi
 // render during streaming even when toolArgs hasn't changed.
 export function parsePartialJsonArgs(toolArgsString: string): Record<string, unknown> | null {
        const cached = partialJsonCache.get(toolArgsString);
+
        if (cached !== undefined) return cached;
 
        let result: Record<string, unknown> | null;
 
        try {
                const parsed: unknown = JSON.parse(toolArgsString);
+
                result =
                        parsed && typeof parsed === 'object' && !Array.isArray(parsed)
                                ? (parsed as Record<string, unknown>)
@@ -47,6 +48,7 @@ export function parsePartialJsonArgs(toolArgsString: string): Record<string, unk
        }
 
        cacheResult(toolArgsString, result);
+
        return result;
 }
 
@@ -54,41 +56,55 @@ export function parsePartialJsonArgs(toolArgsString: string): Record<string, unk
 function scanPartialJson(toolArgsString: string): Record<string, unknown> | null {
        let inString = false;
        let escape = false;
+
        const stack: ('{' | '[')[] = [];
 
        for (let i = 0; i < toolArgsString.length; i++) {
                const ch = toolArgsString[i];
+
                if (escape) {
                        escape = false;
+
                        continue;
                }
+
                if (ch === JSON_BACKSLASH && inString) {
                        escape = true;
+
                        continue;
                }
+
                if (ch === JSON_QUOTE) {
                        inString = !inString;
+
                        continue;
                }
+
                if (inString) continue;
+
                if (ch === JSON_OBJECT_OPEN) stack.push(JSON_OBJECT_OPEN);
                else if (ch === JSON_OBJECT_CLOSE) {
                        if (stack.length === 0 || stack[stack.length - 1] !== JSON_OBJECT_OPEN) return null;
+
                        stack.pop();
                } else if (ch === JSON_ARRAY_OPEN) stack.push(JSON_ARRAY_OPEN);
                else if (ch === JSON_ARRAY_CLOSE) {
                        if (stack.length === 0 || stack[stack.length - 1] !== JSON_ARRAY_OPEN) return null;
+
                        stack.pop();
                }
        }
 
        let completed = toolArgsString;
+
        if (escape) {
                // Dangling escape at end of partial JSON: escape the trailing
                // backslash as a literal so we can close the string cleanly.
                completed += JSON_BACKSLASH;
        }
+
        if (inString) completed += JSON_QUOTE;
+
        if (!inString) completed = completed.replace(TRAILING_JSON_PUNCTUATION_REGEX, '');
 
        // Close in reverse nesting order: innermost container first.
@@ -98,6 +114,7 @@ function scanPartialJson(toolArgsString: string): Record<string, unknown> | null
 
        try {
                const parsed: unknown = JSON.parse(completed);
+
                return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
                        ? (parsed as Record<string, unknown>)
                        : null;
index 05820194cbce048912dd2f00c4bea54859f90752..8f8c081e0495e2365d47d055cd733c86ec69dfc3 100644 (file)
@@ -1,5 +1,3 @@
-import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
-import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
 import {
        CWD_CHANGED_PREFIX,
        CWD_CLEARED_TEXT,
@@ -8,10 +6,13 @@ import {
        HOME_TILDE,
        HOME_TILDE_PREFIX
 } 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, '');
        const idx = trimmed.lastIndexOf(PATH_SEPARATOR);
+
        return idx === -1 ? trimmed : trimmed.slice(idx + 1);
 }
 
@@ -22,10 +23,14 @@ export function abbreviateWorkingDir(
        home: string | null | undefined
 ): string {
        if (!path) return '';
+
        if (!home) return lastPathSegment(path);
+
        if (path === home) return HOME_TILDE;
+
        if (path.startsWith(home + PATH_SEPARATOR))
                return HOME_TILDE_PREFIX + path.slice(home.length + 1);
+
        return lastPathSegment(path);
 }
 
@@ -33,9 +38,12 @@ export function abbreviateWorkingDir(
 // unchanged - used where the full path matters.
 export function abbreviateHome(path: string, home: string | null | undefined): string {
        if (!home) return path;
+
        if (path === home) return HOME_TILDE;
+
        if (path.startsWith(home + PATH_SEPARATOR))
                return HOME_TILDE_PREFIX + path.slice(home.length + 1);
+
        return path;
 }
 
@@ -55,6 +63,7 @@ export interface CwdMessageInfo {
  */
 export function formatCwdMessage(cwd: string, home: string | null): string {
        const display = abbreviateWorkingDir(cwd, home);
+
        return `${CWD_CHANGED_PREFIX}[${FILE_URI_PREFIX}${cwd}](${display}).`;
 }
 
@@ -65,15 +74,20 @@ export function formatCwdMessage(cwd: string, home: string | null): string {
  */
 export function parseCwdMessage(content: string): CwdMessageInfo | null {
        const trimmed = content.trim();
+
        if (trimmed === CWD_CLEARED_TEXT) {
-               return { path: null, display: '' };
+               return { display: '', path: null };
        }
+
        if (trimmed.startsWith(CWD_CHANGED_PREFIX)) {
                const rest = trimmed.slice(CWD_CHANGED_PREFIX.length);
                // not anchored to the end: guidance may follow the link
                const link = rest.match(CWD_LINK_REGEX);
-               if (link) return { path: link[1], display: link[2] };
-               return { path: rest, display: rest };
+
+               if (link) return { display: link[2], path: link[1] };
+
+               return { display: rest, path: rest };
        }
+
        return null;
 }
index 8cf99207cf3b4437da75435780b35ac69fe87fd9..b79f8d78569ba77520c15a29c7f063ca80036df4 100644 (file)
@@ -16,6 +16,7 @@ if (browser) {
        import('pdfjs-dist/build/pdf.worker.min.mjs?raw')
                .then((workerModule) => {
                        const workerBlob = new Blob([workerModule.default], { type: 'application/javascript' });
+
                        pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob);
                })
                .catch(() => {
@@ -31,6 +32,7 @@ if (browser) {
 async function getFileAsBuffer(file: File): Promise<ArrayBuffer> {
        return new Promise((resolve, reject) => {
                const reader = new FileReader();
+
                reader.onload = (event) => {
                        if (event.target?.result) {
                                resolve(event.target.result as ArrayBuffer);
@@ -59,7 +61,6 @@ export async function convertPDFToText(file: File): Promise<string> {
                const buffer = await getFileAsBuffer(file);
                const pdf = await pdfjs.getDocument({ data: buffer }).promise;
                const numPages = pdf.numPages;
-
                const textContentPromises: Promise<TextContent>[] = [];
 
                for (let i = 1; i <= numPages; i++) {
@@ -75,6 +76,7 @@ export async function convertPDFToText(file: File): Promise<string> {
                return textItems.join('\n');
        } catch (error) {
                console.error('Error converting PDF to text:', error);
+
                throw new Error(
                        `Failed to convert PDF to text: ${error instanceof Error ? error.message : 'Unknown error'}`
                );
@@ -111,10 +113,11 @@ export async function convertPDFToImage(file: File, scale: number = 1.5): Promis
                        }
 
                        const task = page.render({
+                               canvas: canvas,
                                canvasContext: ctx,
-                               viewport: viewport,
-                               canvas: canvas
+                               viewport: viewport
                        });
+
                        pages.push(
                                task.promise.then(() => {
                                        return canvas.toDataURL(MimeTypeImage.PNG);
@@ -125,6 +128,7 @@ export async function convertPDFToImage(file: File, scale: number = 1.5): Promis
                return await Promise.all(pages);
        } catch (error) {
                console.error('Error converting PDF to images:', error);
+
                throw new Error(
                        `Failed to convert PDF to images: ${error instanceof Error ? error.message : 'Unknown error'}`
                );
index bffbe890069485a16c50c20c9180b0d9651ad982..7ad4f0b62a860e372610e375c7ac51e1cc7b8f46 100644 (file)
@@ -4,6 +4,7 @@ export function portalToBody(node: HTMLElement) {
        }
 
        const target = document.body;
+
        if (!target) {
                return;
        }
index 91b619cfb925fe10f19576e2d44bbe98f580177d..49bdd2412fa53ed1816bca769b310183bfeaa631 100644 (file)
@@ -1,13 +1,13 @@
+import { heicFileToJpegDataURL, isHeicMimeType } from './heic-to-jpeg';
+import { convertPDFToText } from './pdf-processing';
 import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
 import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
-import { heicFileToJpegDataURL, isHeicMimeType } from './heic-to-jpeg';
-import { FileTypeCategory } from '$lib/enums';
 import { SETTINGS_KEYS } from '$lib/constants';
+import { FileTypeCategory } from '$lib/enums';
 import { modelsStore } from '$lib/stores/models.svelte';
 import { settingsStore } from '$lib/stores/settings.svelte';
-import { toast } from 'svelte-sonner';
 import { getFileTypeCategory } from '$lib/utils';
-import { convertPDFToText } from './pdf-processing';
+import { toast } from 'svelte-sonner';
 
 /**
  * Read a file as a data URL (base64 encoded)
@@ -17,6 +17,7 @@ import { convertPDFToText } from './pdf-processing';
 function readFileAsDataURL(file: File): Promise<string> {
        return new Promise((resolve, reject) => {
                const reader = new FileReader();
+
                reader.onload = () => resolve(reader.result as string);
                reader.onerror = () => reject(reader.error);
                reader.readAsDataURL(file);
@@ -31,6 +32,7 @@ function readFileAsDataURL(file: File): Promise<string> {
 function readFileAsUTF8(file: File): Promise<string> {
        return new Promise((resolve, reject) => {
                const reader = new FileReader();
+
                reader.onload = () => resolve(reader.result as string);
                reader.onerror = () => reject(reader.error);
                reader.readAsText(file);
@@ -58,11 +60,11 @@ export async function processFilesToChatUploaded(
        for (const file of files) {
                const id = Date.now().toString() + Math.random().toString(36).substr(2, 9);
                const base: ChatUploadedFile = {
+                       file,
                        id,
                        name: file.name,
                        size: file.size,
-                       type: file.type,
-                       file
+                       type: file.type
                };
 
                try {
@@ -87,6 +89,7 @@ export async function processFilesToChatUploaded(
                                                preview = await heicFileToJpegDataURL(file);
                                        } catch (err) {
                                                console.error('Failed to convert HEIC to PNG:', err);
+
                                                continue;
                                        }
                                }
@@ -96,6 +99,7 @@ export async function processFilesToChatUploaded(
                                // Extract text content from PDF for preview
                                try {
                                        const textContent = await convertPDFToText(file);
+
                                        results.push({ ...base, textContent });
                                } catch (err) {
                                        console.warn('Failed to extract text from PDF, adding without content:', err);
@@ -107,9 +111,9 @@ export async function processFilesToChatUploaded(
                                        ? modelsStore.modelSupportsVision(activeModelId)
                                        : false;
                                const currentConfig = settingsStore.config;
+
                                if (hasVisionSupport && !currentConfig.pdfAsImage) {
                                        toast.info(`You can enable parsing PDF as images with vision models.`, {
-                                               duration: 8000,
                                                action: {
                                                        label: 'Enable PDF as Images',
                                                        onClick: () => {
@@ -118,21 +122,25 @@ export async function processFilesToChatUploaded(
                                                                        duration: 3000
                                                                });
                                                        }
-                                               }
+                                               },
+                                               duration: 8000
                                        });
                                }
                        } else if (getFileTypeCategory(file.type) === FileTypeCategory.AUDIO) {
                                // Generate preview URL for audio files
                                const preview = await readFileAsDataURL(file);
+
                                results.push({ ...base, preview });
                        } else if (getFileTypeCategory(file.type) === FileTypeCategory.VIDEO) {
                                // Generate preview URL for video files
                                const preview = await readFileAsDataURL(file);
+
                                results.push({ ...base, preview });
                        } else {
                                // Fallback: treat unknown files as text
                                try {
                                        const textContent = await readFileAsUTF8(file);
+
                                        results.push({ ...base, textContent });
                                } catch (err) {
                                        console.warn('Failed to read file as text, adding without content:', err);
index a88213904bb906579d02782c757be57644662cb4..ed1d6c29c7b7cc8b63d8e0a03e6e46ea9063259f 100644 (file)
@@ -21,7 +21,7 @@ export function modelLoadFraction(progress: ModelLoadProgress | null): number {
 
        // The server may emit a progress event before the stage plan is known, so
        // `stages` can be absent. Fall back to the raw value in that case.
-       const { stages = [], current, value } = progress;
+       const { current, stages = [], value } = progress;
        const tailCount = Math.max(stages.length - 1, 0);
        const textCeiling = 1 - tailCount * MODEL_LOAD_TAIL_SHARE;
        const idx = stages.indexOf(current);
@@ -41,6 +41,7 @@ export function modelLoadProgressText(progress: ModelLoadProgress | null): strin
        if (!progress) return null;
 
        const label = modelLoadStageLabel(progress.current);
+
        if (!label) return null;
 
        return `${label} ${Math.round(modelLoadFraction(progress) * 100)}%`;
index e5a9493efe24e23650c447156fe3af239436c423..452ea91aca26f69edc7250bbc8d0982ed83d7724 100644 (file)
@@ -1,5 +1,5 @@
-import DOMPurify from 'dompurify';
 import { SVG_MAX_BYTES, SVG_SANITIZE_CONFIG, SVG_TAG_PREFIX } from '$lib/constants';
+import DOMPurify from 'dompurify';
 
 /**
  * Sanitizes a raw svg string for safe inline rendering.
index 6078ecdf73f4dd95b77b635e11b69da6b731508e..613000faa036cb0012d1bedf63b0575be19d497b 100644 (file)
@@ -1,8 +1,8 @@
 import {
        KEY_VALUE_PAIR_KEY_MAX_LENGTH,
-       KEY_VALUE_PAIR_VALUE_MAX_LENGTH,
        KEY_VALUE_PAIR_UNSAFE_KEY_RE,
-       KEY_VALUE_PAIR_UNSAFE_VALUE_RE
+       KEY_VALUE_PAIR_UNSAFE_VALUE_RE,
+       KEY_VALUE_PAIR_VALUE_MAX_LENGTH
 } from '$lib/constants';
 
 /**
index d090e6dc4bfb33ce532bd856a51b15b63d7fe0da..af04e7b07a1d9de411cf942c977f3f1157e79151 100644 (file)
@@ -26,32 +26,26 @@ export type SearchResult = {
 
 const SEPARATOR_LINE_RE = /^\s*---\s*$/;
 const URL_SCHEME_RE = /^https?:\/\//i;
-
 // Match either Unix or Windows line endings so chunking/parsing handles
 // payloads written by either scheme without off-by-one mismatches.
 const LINE_BREAK_RE = /\r?\n/;
-
 // Sentinel the search-result wire format uses when a field is absent
 // (e.g. `Author: N/A`). Treated identically to a missing field so the
 // rendered card hides the row either way.
 const NOT_AVAILABLE_VALUE = 'N/A';
-
 // Section header that announces the start of the multi-line Highlights
 // block. Everything from that line onward (until the next `---`
 // separator or end of chunk) is captured verbatim as highlight text
 // instead of being re-scanned for `Title:`/`URL:`/... field lines.
 const HIGHLIGHTS_SECTION_HEADER = 'Highlights:';
-
 // Field name conventionally used by web-search tools (Exa etc.) as the
 // user-supplied query parameter. Extracted so future tool schemas that
 // adopt the same convention stay grep-compatible with this parser.
 const SEARCH_TOOL_QUERY_FIELD = 'query';
-
 // URL schemes the favicon helper will resolve to a hosted favicon. Any
 // other scheme (e.g. data:, blob:) intentionally returns null so the UI
 // can fall back to a generic globe icon.
 const RESOLVABLE_URL_PROTOCOLS: readonly string[] = ['https:', 'http:'];
-
 // Conventional favicon path served by virtually every web host.
 // Appended to the URL origin as a best-effort lookup target; ignore
 // 404s at render time.
@@ -83,7 +77,9 @@ const FIELD_PREFIXES: ReadonlyArray<{ key: FieldKey; prefix: string }> = [
 function splitChunks(text: string): string[] {
        const lines = text.split(LINE_BREAK_RE);
        const chunks: string[] = [];
+
        let buffer: string[] = [];
+
        for (const line of lines) {
                if (SEPARATOR_LINE_RE.test(line)) {
                        if (buffer.length > 0) {
@@ -94,7 +90,9 @@ function splitChunks(text: string): string[] {
                        buffer.push(line);
                }
        }
+
        if (buffer.length > 0) chunks.push(buffer.join('\n'));
+
        return chunks;
 }
 
@@ -106,36 +104,42 @@ function splitChunks(text: string): string[] {
  */
 function parseChunk(chunk: string): SearchResult | null {
        const trimmed = chunk.trim();
+
        if (!trimmed) return null;
 
        const lines = chunk.split(LINE_BREAK_RE);
-
        const fields: Record<FieldKey, string | undefined> = {
-               [FieldKey.TITLE]: undefined,
-               [FieldKey.URL]: undefined,
+               [FieldKey.AUTHOR]: undefined,
                [FieldKey.PUBLISHED]: undefined,
-               [FieldKey.AUTHOR]: undefined
+               [FieldKey.TITLE]: undefined,
+               [FieldKey.URL]: undefined
        };
        const highlightLines: string[] = [];
+
        let inHighlights = false;
 
        for (const line of lines) {
                if (!inHighlights && line.trim() === HIGHLIGHTS_SECTION_HEADER) {
                        inHighlights = true;
+
                        continue;
                }
 
                if (inHighlights) {
                        highlightLines.push(line);
+
                        continue;
                }
 
                for (const { key, prefix } of FIELD_PREFIXES) {
                        if (!line.startsWith(prefix)) continue;
+
                        const value = line.slice(prefix.length).trim();
+
                        if (value && value !== NOT_AVAILABLE_VALUE) {
                                fields[key] = value;
                        }
+
                        break;
                }
        }
@@ -144,14 +148,17 @@ function parseChunk(chunk: string): SearchResult | null {
                return null;
 
        const highlights = highlightLines.join('\n').trim();
-
        const result: SearchResult = {
                title: fields[FieldKey.TITLE],
                url: fields[FieldKey.URL]
        };
+
        if (fields[FieldKey.PUBLISHED]) result.published = fields[FieldKey.PUBLISHED];
+
        if (fields[FieldKey.AUTHOR]) result.author = fields[FieldKey.AUTHOR];
+
        if (highlights) result.highlights = highlights;
+
        return result;
 }
 
@@ -170,17 +177,21 @@ export function extractSearchResults(text: string | undefined | null): SearchRes
        if (!text) return [];
 
        const cached = searchResultsCache.get(text);
+
        if (cached) return cached;
 
        const results: SearchResult[] = [];
+
        for (const chunk of splitChunks(text)) {
                const parsed = parseChunk(chunk);
+
                if (parsed) results.push(parsed);
        }
 
        if (searchResultsCache.size >= SEARCH_RESULTS_CACHE_MAX_SIZE) {
                searchResultsCache.delete(searchResultsCache.keys().next().value!);
        }
+
        searchResultsCache.set(text, results);
 
        return results;
@@ -201,13 +212,17 @@ export function extractSearchQuery(toolArgs: string | undefined | null): string
        if (!toolArgs) return '';
 
        const cached = searchQueryCache.get(toolArgs);
+
        if (cached !== undefined) return cached;
 
        let result = '';
+
        try {
                const parsed: unknown = JSON.parse(toolArgs);
+
                if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                        const candidate = (parsed as Record<string, unknown>)[SEARCH_TOOL_QUERY_FIELD];
+
                        if (typeof candidate === 'string') result = candidate.trim();
                }
        } catch {
@@ -217,6 +232,7 @@ export function extractSearchQuery(toolArgs: string | undefined | null): string
        if (searchQueryCache.size >= SEARCH_QUERY_CACHE_MAX_SIZE) {
                searchQueryCache.delete(searchQueryCache.keys().next().value!);
        }
+
        searchQueryCache.set(toolArgs, result);
 
        return result;
@@ -231,7 +247,9 @@ export function extractSearchQuery(toolArgs: string | undefined | null): string
 export function faviconForUrl(url: string): string | null {
        try {
                const parsed = new URL(url);
+
                if (!RESOLVABLE_URL_PROTOCOLS.includes(parsed.protocol)) return null;
+
                return `${parsed.protocol}//${parsed.host}${FAVICON_PATH}`;
        } catch {
                return null;
@@ -255,5 +273,6 @@ export const SUPPORTED_WEB_SEARCH_TOOL_NAMES: readonly string[] = ['web_search_e
  */
 export function isWebSearchToolName(toolName: string | undefined | null): boolean {
        if (!toolName) return false;
+
        return SUPPORTED_WEB_SEARCH_TOOL_NAMES.includes(toolName);
 }
index 3ad2d129875539fa73468560d53f112fdfcd6261..12b273b243212c1ba9130d5653f8049934dccb43 100644 (file)
@@ -24,25 +24,33 @@ export class SourceHistory {
        push(entry: SourceHistoryEntry, now: number, newGroup = false): void {
                if (newGroup || now - this.lastPush >= this.groupWindowMs || this.undoStack.length === 0) {
                        this.undoStack.push(entry);
+
                        if (this.undoStack.length > this.limit) this.undoStack.shift();
                }
+
                this.lastPush = now;
                this.redoStack = [];
        }
 
        undo(current: SourceHistoryEntry): SourceHistoryEntry | null {
                const entry = this.undoStack.pop();
+
                if (!entry) return null;
+
                this.redoStack.push(current);
                this.lastPush = 0; // the next edit after an undo starts a new group
+
                return entry;
        }
 
        redo(current: SourceHistoryEntry): SourceHistoryEntry | null {
                const entry = this.redoStack.pop();
+
                if (!entry) return null;
+
                this.undoStack.push(current);
                this.lastPush = 0;
+
                return entry;
        }
 }
index 80c9090b0c111bc8b695adad37703df8ce53621e..41d9a1152a0174aced51735a051b2b47f059bac4 100644 (file)
@@ -30,9 +30,11 @@ export async function* parseSseJsonStream<T = unknown>(
        signal?: AbortSignal
 ): AsyncGenerator<SseJsonEvent<T>> {
        const reader = response.body?.getReader();
+
        if (!reader) return;
 
        const decoder = new TextDecoder();
+
        let buffer = '';
 
        try {
@@ -40,19 +42,26 @@ export async function* parseSseJsonStream<T = unknown>(
                        if (signal?.aborted) return;
 
                        const { done, value } = await reader.read();
+
                        if (done) break;
 
                        buffer += decoder.decode(value, { stream: true });
                        const records = buffer.split(SSE_RECORD_SEPARATOR);
+
                        buffer = records.pop() ?? '';
 
                        for (const record of records) {
                                if (!record) continue;
+
                                for (const line of record.split(SSE_LINE_SEPARATOR)) {
                                        if (!line.startsWith(SSE_DATA_PREFIX)) continue;
+
                                        const payload = line.slice(SSE_DATA_PREFIX.length).trim();
+
                                        if (payload === SSE_DONE_MARKER) return;
+
                                        if (!payload) continue;
+
                                        try {
                                                yield { data: JSON.parse(payload) as T };
                                        } catch {
index ce88df00744300f98c2f1f76a3ebafd5b7e849ca..253786a7e3019ae9cf4328094f45aa0f807335b0 100644 (file)
@@ -8,6 +8,8 @@
  */
 export function streamIdentity(conversationId: string, model?: string | null): string {
        if (!conversationId) return '';
+
        if (!model) return conversationId;
+
        return `${conversationId}::${model}`;
 }
index 71caff8c24ce134bd585bd55592ccdbac2d57fc5..38f1ef92dfdacb1f98467f53e4c67f1cc931bf38 100644 (file)
@@ -6,5 +6,6 @@
  */
 export function mountSvgShadow(host: HTMLElement, markup: string, style: string): void {
        const root = host.shadowRoot ?? host.attachShadow({ mode: 'open' });
+
        root.innerHTML = markup ? `<style>${style}</style>${markup}` : '';
 }
index d5a7f7d8340d159e2e14fddf15352181b01b3ad4..07b84b3f841d8e618bcf6d6fb89d1a82162a7161 100644 (file)
@@ -20,6 +20,7 @@ export function svgBase64UrlToPngDataURL(
 
                                if (!ctx) {
                                        reject(new Error('Failed to get 2D canvas context.'));
+
                                        return;
                                }
 
@@ -33,6 +34,7 @@ export function svgBase64UrlToPngDataURL(
                                        ctx.fillStyle = backgroundColor;
                                        ctx.fillRect(0, 0, canvas.width, canvas.height);
                                }
+
                                ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
 
                                resolve(canvas.toDataURL(MimeTypeImage.PNG));
@@ -46,6 +48,7 @@ export function svgBase64UrlToPngDataURL(
                } catch (error) {
                        const message = error instanceof Error ? error.message : String(error);
                        const errorMessage = `Error converting SVG to PNG: ${message}`;
+
                        console.error(errorMessage, error);
                        reject(new Error(errorMessage));
                }
index 3f7a55ebc26da161e72e8b627ef84e371ea803bf..f770940479e64a3ec2613412c08ddec6794aa505 100644 (file)
@@ -4,8 +4,8 @@
  */
 
 import { DEFAULT_BINARY_DETECTION_OPTIONS } from '$lib/constants';
-import type { BinaryDetectionOptions } from '$lib/types';
 import { FileExtensionText } from '$lib/enums';
+import type { BinaryDetectionOptions } from '$lib/types';
 
 /**
  * Check if a filename indicates a text file based on its extension
index 18a36eb8a26545050f47cec57a980177b767b854..32bf1f38f69918023379640404802789f37816a5 100644 (file)
@@ -15,6 +15,7 @@ export function getPreviewText(content: string, max = 150): string {
 export function generateConversationTitle(content: string, useFirstLine: boolean = false): string {
        if (useFirstLine) {
                const firstLine = content.split(NEWLINE).find((line) => line.trim().length > 0);
+
                return firstLine ? firstLine.trim() : content.trim();
        }
 
index 798ba7b2597731d4c95dd8228d22d858b397e4a1..b64bca7868e7dff32b24a6fe65f094941bddec56 100644 (file)
@@ -15,11 +15,14 @@ export function tryParseToolResultObject(
        toolResultString: string | undefined
 ): Record<string, unknown> | null {
        if (!toolResultString) return null;
+
        try {
                const parsed: unknown = JSON.parse(toolResultString);
+
                if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
                        return parsed as Record<string, unknown>;
                }
+
                return null;
        } catch {
                return null;
index eb8dbfb3632c297f2c1f13d33992a8ae87d03fa3..8dc7b97eb91956dbb19ec5d0ac693a47ceb7ab94 100644 (file)
@@ -1,11 +1,11 @@
 import {
+       LEADING_SLASHES_REGEX,
        TEMPLATE_EXPRESSION_REGEX,
        URI_SCHEME_SEPARATOR,
        URI_TEMPLATE_OPERATORS,
        URI_TEMPLATE_SEPARATORS,
        VARIABLE_EXPLODE_MODIFIER_REGEX,
-       VARIABLE_PREFIX_MODIFIER_REGEX,
-       LEADING_SLASHES_REGEX
+       VARIABLE_PREFIX_MODIFIER_REGEX
 } from '../constants';
 
 /**
@@ -25,6 +25,7 @@ import {
  */
 export function normalizeResourceUri(uri: string): string {
        const schemeEnd = uri.indexOf(URI_SCHEME_SEPARATOR);
+
        if (schemeEnd === -1) return uri;
 
        const scheme = uri.substring(0, schemeEnd);
@@ -65,6 +66,7 @@ export function extractTemplateVariables(template: string): UriTemplateVariable[
        const seen = new Set<string>();
 
        let match;
+
        TEMPLATE_EXPRESSION_REGEX.lastIndex = 0;
 
        while ((match = TEMPLATE_EXPRESSION_REGEX.exec(template)) !== null) {
@@ -117,7 +119,6 @@ export function expandTemplate(template: string, values: Record<string, string>)
                                                .replace(VARIABLE_PREFIX_MODIFIER_REGEX, '')
                                                .trim()
                                );
-
                        const expandedParts = varNames
                                .map((name: string) => values[name] ?? '')
                                .filter((v: string) => v !== '');
index f1bf9ecb8bbd601689a6f51e707d1808f30b7383..1d44720e1e5be71f90b4b19e32916ba36951ba83 100644 (file)
@@ -28,6 +28,7 @@ function isIpAddress(hostname: string): boolean {
  */
 export function extractRootDomain(url: URL): string | null {
        const hostname = url.hostname.toLowerCase();
+
        if (!hostname || isIpAddress(hostname)) return null;
 
        const parts = hostname.split('.');
@@ -95,7 +96,6 @@ export function canonicalizeServerUrl(raw: string): string {
        try {
                const parsed = new URL(trimmed);
                const pathname = parsed.pathname.replace(TRAILING_SLASHES_REGEX, '');
-
                // Aggressive: drop the port unconditionally. We only use this for
                // equality checks between user-typed URLs and a hard-coded list of
                // recommendations, where the port can never carry distinguishing
index ea51838029e5c714e0d0cc823d3cd17764027e09..8c61ecf851f669fdc4d883afb4bdc5763c1c8bea 100644 (file)
@@ -20,6 +20,7 @@ export function webpBase64UrlToPngDataURL(
 
                                if (!ctx) {
                                        reject(new Error('Failed to get 2D canvas context.'));
+
                                        return;
                                }
 
@@ -33,6 +34,7 @@ export function webpBase64UrlToPngDataURL(
                                        ctx.fillStyle = backgroundColor;
                                        ctx.fillRect(0, 0, canvas.width, canvas.height);
                                }
+
                                ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
 
                                resolve(canvas.toDataURL(MimeTypeImage.PNG));
@@ -46,6 +48,7 @@ export function webpBase64UrlToPngDataURL(
                } catch (error) {
                        const message = error instanceof Error ? error.message : String(error);
                        const errorMessage = `Error converting WebP to PNG: ${message}`;
+
                        console.error(errorMessage, error);
                        reject(new Error(errorMessage));
                }
index 175a4f9f9201416987b351bfc5f7a6a4c4ffdd68..ae545301921525abf90b45389a80f58548860ef3 100644 (file)
@@ -5,8 +5,7 @@
  * the last segment); anything else glob-matches home-relative entries.
  */
 
-import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
-import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
+import { lastPathSegment } from './path-display';
 import {
        DRIVE_PREFIX_REGEX,
        DRIVE_ROOT_REGEX,
@@ -20,7 +19,8 @@ import {
        UNC_ROOT_REGEX,
        WINDOWS_SEPARATOR
 } from '$lib/constants';
-import { lastPathSegment } from './path-display';
+import { PATH_SEPARATOR } from '$lib/constants/mcp-resource';
+import { TRAILING_SLASHES_REGEX } from '$lib/constants/url';
 
 export interface GlobEntry {
        path: string;
@@ -38,14 +38,19 @@ export interface PathQuery {
  */
 function toPosixSeparators(query: string): string {
        if (!DRIVE_PREFIX_REGEX.test(query) && !query.startsWith(WINDOWS_SEPARATOR)) return query;
+
        return query.split(WINDOWS_SEPARATOR).join(PATH_SEPARATOR);
 }
 
 export function rootPrefixLength(path: string): number {
        const unc = path.match(UNC_ROOT_REGEX);
+
        if (unc) return unc[0].length;
+
        const drive = path.match(DRIVE_ROOT_REGEX);
+
        if (drive) return drive[0].length;
+
        return path.startsWith(PATH_SEPARATOR) ? PATH_SEPARATOR.length : 0;
 }
 
@@ -53,6 +58,7 @@ export function rootPrefixLength(path: string): number {
 export function splitPathQuery(query: string): PathQuery | null {
        const normalized = toPosixSeparators(query);
        const rootLength = rootPrefixLength(normalized);
+
        if (rootLength === 0 && !normalized.startsWith(HOME_TILDE)) return null;
 
        // a root keeps its trailing separator so it stays absolute on its own
@@ -60,33 +66,36 @@ export function splitPathQuery(query: string): PathQuery | null {
                rootLength > 0
                        ? normalized.slice(0, rootLength).replace(TRAILING_SLASHES_REGEX, '') + PATH_SEPARATOR
                        : HOME_TILDE;
-
        const rest = normalized
                .slice(rootLength > 0 ? rootLength : HOME_TILDE.length)
                .replace(LEADING_SLASHES_REGEX, '')
                .replace(TRAILING_SLASHES_REGEX, '');
-
        const parentOf = (dirs: string) =>
                rootLength > 0 ? root + dirs : HOME_TILDE + PATH_SEPARATOR + dirs;
 
-       if (!rest) return { parent: root, last: '' };
+       if (!rest) return { last: '', parent: root };
 
        const idx = rest.lastIndexOf(PATH_SEPARATOR);
-       if (idx === -1) return { parent: root, last: rest };
-       return { parent: parentOf(rest.slice(0, idx)), last: rest.slice(idx + 1) };
+
+       if (idx === -1) return { last: rest, parent: root };
+
+       return { last: rest.slice(idx + 1), parent: parentOf(rest.slice(0, idx)) };
 }
 
 export function buildCaseInsensitiveGlob(query: string): string {
        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;
                // 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 out += c;
        }
+
        return out + GLOB_WILDCARD;
 }
 
@@ -114,7 +123,8 @@ export function buildGlobSearchArgs(
                        : GLOB_WILDCARD
                : buildCaseInsensitiveGlob(query);
        const maxDepth = pathQuery ? PATH_NAV_MAX_DEPTH : searchDepth;
-       return { path, include, maxDepth, rankQuery: pathQuery?.last ?? query, last: pathQuery?.last };
+
+       return { include, last: pathQuery?.last, maxDepth, path, rankQuery: pathQuery?.last ?? query };
 }
 
 const RANK_EXACT = 0;
@@ -125,9 +135,13 @@ const RANK_OTHER = 3;
 function rankScore(path: string, query: string): number {
        const name = lastPathSegment(path).toLowerCase();
        const q = query.toLowerCase();
+
        if (name === q) return RANK_EXACT;
+
        if (name.startsWith(q)) return RANK_PREFIX;
+
        if (name.includes(q)) return RANK_SUBSTRING;
+
        return RANK_OTHER;
 }
 
@@ -142,24 +156,33 @@ export function rankEntries(entries: GlobEntry[], query: string): GlobEntry[] {
 
 export function joinPath(base: string, rel: string): string {
        if (!base) return rel;
+
        return base.replace(TRAILING_SLASHES_REGEX, '') + PATH_SEPARATOR + rel;
 }
 
 export function highlightMatch(text: string, query: string): { text: string; match: boolean }[] {
-       if (!query) return [{ text, match: false }];
+       if (!query) return [{ match: false, text }];
+
        const segments: { text: string; match: boolean }[] = [];
        const lowerText = text.toLowerCase();
        const lowerQuery = query.toLowerCase();
+
        let i = 0;
+
        while (i < text.length) {
                const idx = lowerText.indexOf(lowerQuery, i);
+
                if (idx < 0) {
-                       segments.push({ text: text.slice(i), match: false });
+                       segments.push({ match: false, text: text.slice(i) });
+
                        break;
                }
-               if (idx > i) segments.push({ text: text.slice(i, idx), match: false });
-               segments.push({ text: text.slice(idx, idx + query.length), match: true });
+
+               if (idx > i) segments.push({ match: false, text: text.slice(i, idx) });
+
+               segments.push({ match: true, text: text.slice(idx, idx + query.length) });
                i = idx + query.length;
        }
+
        return segments;
 }
index fef9b9f53f5e96c6be93e3c5e241a4f7a5692d6f..c1e49102b7a3bd3ca6c63bf4d991b2485497adfc 100644 (file)
@@ -1,13 +1,13 @@
 <script lang="ts">
+       import { replaceState } from '$app/navigation';
+       import { page } from '$app/state';
        import { DialogModelNotAvailable } from '$lib/components/app';
+       import { APP_NAME, URL_PARAMS } from '$lib/constants';
        import { chatStore } from '$lib/stores/chat.svelte';
        import { conversationsStore, isConversationsInitialized } from '$lib/stores/conversations.svelte';
-       import { modelsStore, modelOptions } from '$lib/stores/models.svelte';
+       import { modelOptions, modelsStore } from '$lib/stores/models.svelte';
        import { isRouterMode } from '$lib/stores/server.svelte';
        import { onMount } from 'svelte';
-       import { page } from '$app/state';
-       import { replaceState } from '$app/navigation';
-       import { APP_NAME, URL_PARAMS } from '$lib/constants';
 
        let qParam = $derived(page.url.searchParams.get(URL_PARAMS.QUERY));
        let modelParam = $derived(page.url.searchParams.get(URL_PARAMS.MODEL));
index f5fe42eaa010350842f17cc0e430a74e994b6e8c..677bbf03d9c07fd14b967b72b06de5b070c7c67c 100644 (file)
@@ -1,12 +1,12 @@
 <script lang="ts">
        import { goto, replaceState } from '$app/navigation';
-       import { page } from '$app/state';
        import { afterNavigate } from '$app/navigation';
+       import { page } from '$app/state';
        import { DialogModelNotAvailable } from '$lib/components/app';
        import { APP_NAME, ROUTES, URL_PARAMS } from '$lib/constants';
        import { chatStore } from '$lib/stores/chat.svelte';
-       import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
-       import { modelsStore, modelOptions } from '$lib/stores/models.svelte';
+       import { activeConversation, conversationsStore } from '$lib/stores/conversations.svelte';
+       import { modelOptions, modelsStore } from '$lib/stores/models.svelte';
 
        let chatId = $derived(page.params.id);
        let currentChatId: string | undefined = undefined;
@@ -28,6 +28,7 @@
         */
        function clearUrlParams() {
                const url = new URL(page.url);
+
                url.searchParams.delete(URL_PARAMS.QUERY);
                url.searchParams.delete(URL_PARAMS.MODEL);
                replaceState(url.toString(), {});
@@ -40,6 +41,7 @@
                // Handle model parameter - select model if provided
                if (modelParam) {
                        const model = modelsStore.findModelByName(modelParam);
+
                        if (model) {
                                try {
                                        await modelsStore.selectModelById(model.id);
                                        console.error('Failed to select model:', error);
                                        requestedModelName = modelParam;
                                        showModelNotAvailable = true;
+
                                        return;
                                }
                        } else {
                                // Model not found - show error dialog
                                requestedModelName = modelParam;
                                showModelNotAvailable = true;
+
                                return;
                        }
                }
                        // Skip loading if this conversation is already active (e.g., just created)
                        if (activeConversation()?.id === chatId) {
                                void chatStore.discoverActiveStream(chatId);
+
                                if ((qParam !== null || modelParam !== null) && !urlParamsProcessed) {
                                        handleUrlParams();
                                }
+
                                return;
                        }
 
                        (async () => {
                                const success = await conversationsStore.loadConversation(chatId);
+
                                if (!success) {
                                        await goto(ROUTES.START);
+
                                        return;
                                }
+
                                chatStore.syncLoadingStateForChat(chatId);
                                // server probe (with localStorage fallback) and attach
                                await chatStore.discoverActiveStream(chatId);
                // where the initial mount probe missed an active session
                const onVisibility = () => {
                        if (document.visibilityState !== 'visible') return;
+
                        if (!chatId) return;
+
                        void chatStore.discoverActiveStream(chatId);
                };
+
                document.addEventListener('visibilitychange', onVisibility);
+
                return () => document.removeEventListener('visibilitychange', onVisibility);
        });
 </script>
index 8da9aad16f9d9033c52a6a71c972709454a6b4ef..c867f6de9201b747550d9a46328f6ebc007ca2aa 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { page } from '$app/stores';
        import { goto } from '$app/navigation';
+       import { page } from '$app/stores';
        import { ServerErrorSplash } from '$lib/components/app';
-       import { ROUTES } from '$lib/constants/routes';
        import { APP_NAME } from '$lib/constants';
+       import { ROUTES } from '$lib/constants/routes';
 
        let error = $derived($page.error);
        let status = $derived($page.status);
index ad2dd1560ae9919496c2c21afa5f6b4512b0e477..089958bdba439bf6550002438f6eba2696c69b88 100644 (file)
@@ -1,36 +1,33 @@
 <script lang="ts">
        import '../app.css';
-       import { base } from '$app/paths';
        import { browser } from '$app/environment';
        import { goto } from '$app/navigation';
+       import { base } from '$app/paths';
        import { page } from '$app/state';
-       import { untrack } from 'svelte';
-       import { onMount } from 'svelte';
-
        import { SidebarNavigation } from '$lib/components/app';
        import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
-       import { pwaAssetsHead } from 'virtual:pwa-assets/head';
-
-       import { chatStore } from '$lib/stores/chat.svelte';
        import * as Tooltip from '$lib/components/ui/tooltip';
-       import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
-       import { config, settingsStore } from '$lib/stores/settings.svelte';
-       import { ModeWatcher } from 'mode-watcher';
-       import { ROUTES } from '$lib/constants/routes';
-       import { RouterService } from '$lib/services/router.service';
-       import { Toaster } from 'svelte-sonner';
-       import { modelsStore } from '$lib/stores/models.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
        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 { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
        import { usePwa } from '$lib/hooks/use-pwa.svelte';
+       import { RouterService } from '$lib/services/router.service';
+       import { buildInfoStore } from '$lib/stores/build-info.svelte';
+       import { chatStore } from '$lib/stores/chat.svelte';
        import { conversations } from '$lib/stores/conversations.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { modelsStore } from '$lib/stores/models.svelte';
+       import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
+       import { config, settingsStore } from '$lib/stores/settings.svelte';
        import { theme } from '$lib/stores/theme.svelte';
-       import { buildInfoStore } from '$lib/stores/build-info.svelte';
-
-       import { SETTINGS_KEYS } from '$lib/constants';
+       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { ModeWatcher } from 'mode-watcher';
+       import { untrack } from 'svelte';
+       import { onMount } from 'svelte';
+       import { Toaster } from 'svelte-sonner';
+       import { pwaAssetsHead } from 'virtual:pwa-assets/head';
 
        let { children } = $props();
        let innerHeight = $state<number | undefined>();
                const dark = theme.isSystemDark;
 
                let icoLink = document.querySelector(FAVICON_SELECTORS.ICO_48X48) as HTMLLinkElement | null;
+
                if (icoLink) {
                        icoLink.href = dark ? FAVICON_PATHS.ICO_DARK : FAVICON_PATHS.ICO_LIGHT;
                }
 
                let svgLink = document.querySelector(FAVICON_SELECTORS.SVG_ANY) as HTMLLinkElement | null;
+
                if (svgLink) {
                        svgLink.href = dark ? FAVICON_PATHS.SVG_DARK : FAVICON_PATHS.SVG_LIGHT;
                }
@@ -92,8 +91,8 @@
        // Global keyboard shortcuts
        const { handleKeydown } = useKeyboardShortcuts({
                editActiveConversation: () => chatSidebar?.editActiveConversation?.(),
-               navigateToPrevConversation: () => navigateToConversation(-1),
-               navigateToNextConversation: () => navigateToConversation(1)
+               navigateToNextConversation: () => navigateToConversation(1),
+               navigateToPrevConversation: () => navigateToConversation(-1)
        });
 
        function checkApiKey() {
                                page.status !== 403
                        ) {
                                const headers: Record<string, string> = {
-                                       'Content-Type': 'application/json',
-                                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey.trim()}`
+                                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey.trim()}`,
+                                       'Content-Type': 'application/json'
                                };
 
                                fetch(`${base}/props`, { headers })
        // or ended while it was hidden. snapshot only, no polling
        function handleVisibilityChange() {
                if (document.visibilityState !== 'visible') return;
+
                void chatStore.syncRemoteRunningStreams();
        }
 
        // Live model status and load progress via the /models/sse feed (router mode)
        $effect(() => {
                if (!browser) return;
+
                if (!isRouterMode()) return;
 
                untrack(() => {
                if (!browser) return;
 
                const mcpServers = mcpStore.getServers();
-
                const serversWithUrls = mcpServers.filter((s) => s.url.trim());
 
                if (serversWithUrls.length > 0) {
index d99dc1f2fab307701063d18bc71df1fd1f0618b8..feb36058a32fe2908e256d7e37d875e6e541c276 100644 (file)
@@ -1,13 +1,12 @@
 <script lang="ts">
+       import { browser } from '$app/environment';
        import { goto } from '$app/navigation';
        import { page } from '$app/state';
-       import { browser } from '$app/environment';
-
        import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app';
        import { ROUTES } from '$lib/constants/routes';
        import { RouterService } from '$lib/services/router.service';
-       import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
        import { chatStore } from '$lib/stores/chat.svelte';
+       import { conversations, conversationsStore } from '$lib/stores/conversations.svelte';
        import { isMobile } from '$lib/stores/viewport.svelte';
 
        let searchQuery = $state('');
@@ -17,7 +16,9 @@
 
        let filteredConversations = $derived.by(() => {
                const query = searchQuery.trim().toLowerCase();
+
                if (query.length === 0) return [];
+
                return conversations().filter((c) => c.name.toLowerCase().includes(query));
        });
 
 
        async function handleEditConversation(id: string) {
                const conversation = conversations().find((c) => c.id === id);
+
                if (!conversation) return;
 
                const newName = window.prompt('Rename conversation', conversation.name);
+
                if (newName && newName.trim()) {
                        await conversationsStore.updateConversationName(id, newName.trim());
                }
 
        async function handleDeleteConversation(id: string) {
                const conversation = conversations().find((c) => c.id === id);
+
                if (!conversation) return;
 
                const confirmed = window.confirm(
                        `Delete "${conversation.name}"? This action cannot be undone.`
                );
+
                if (!confirmed) return;
 
                await conversationsStore.deleteConversation(id, { deleteWithForks: false });
index c2b587862f257d149d2f57b2b007d80be8020d5d..4e191dce72bc5f556c99e6a0fd6acdd6db793975 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { X } from '@lucide/svelte';
-       import { goto } from '$app/navigation';
        import { browser } from '$app/environment';
+       import { goto } from '$app/navigation';
        import { page } from '$app/state';
        import { ActionIcon } from '$lib/components/app';
        import { SETTINGS_FALLBACK_EXIT_ROUTE } from '$lib/constants';
@@ -12,6 +12,7 @@
 
        $effect(() => {
                const currentId = page.route.id;
+
                return () => {
                        previousRouteId = currentId;
                };
@@ -19,6 +20,7 @@
 
        function handleClose() {
                const prevIsSettings = previousRouteId?.startsWith('/settings');
+
                if (browser && window.history.length > 1 && !prevIsSettings) {
                        history.back();
                } else {
index 90fe6107012fae5650554d9685bd4e71ed1418f1..d4faae044909c683372002c7b3411148b700c63f 100644 (file)
@@ -1,9 +1,9 @@
 <script lang="ts">
-       import { SettingsChat } from '$lib/components/app/settings';
-       import { page } from '$app/state';
        import { afterNavigate, replaceState } from '$app/navigation';
-       import { RouterService } from '$lib/services';
+       import { page } from '$app/state';
+       import { SettingsChat } from '$lib/components/app/settings';
        import { SETTINGS_SECTION_SLUGS } from '$lib/constants';
+       import { RouterService } from '$lib/services';
 
        afterNavigate(() => {
                if (!page.params.section) {
index 897c7267f9cf049545e444d9b0892d600b837e51..cd7fed2bacf1e33e5ddb48a2efcbb730a00e5e9e 100644 (file)
@@ -1,38 +1,37 @@
-import { mdsvex } from 'mdsvex';
 import adapter from '@sveltejs/adapter-static';
 import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+import { mdsvex } from 'mdsvex';
 
 // CMake sets LLAMA_UI_OUT_DIR to the staging dir under the build tree; manual
 // `npm run build` runs without the env var default to ./dist.
 const outDir = process.env.LLAMA_UI_OUT_DIR ?? './dist';
-
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
-       // Consult https://svelte.dev/docs/kit/integrations
-       // for more information about preprocessors
-       preprocess: [vitePreprocess(), mdsvex()],
+       extensions: ['.svelte', '.svx'],
 
        kit: {
-               paths: {
-                       relative: true
-               },
-               router: { type: 'hash' },
                adapter: adapter({
-                       pages: outDir,
                        assets: outDir,
                        fallback: 'index.html',
+                       pages: outDir,
                        precompress: false,
                        strict: true
                }),
+               alias: {
+                       $styles: 'src/styles'
+               },
                output: {
                        bundleStrategy: 'single'
                },
-               alias: {
-                       $styles: 'src/styles'
-               }
+               paths: {
+                       relative: true
+               },
+               router: { type: 'hash' }
        },
 
-       extensions: ['.svelte', '.svx']
+       // Consult https://svelte.dev/docs/kit/integrations
+       // for more information about preprocessors
+       preprocess: [vitePreprocess(), mdsvex()]
 };
 
 export default config;
index c13f98cfc38acb174336ef35b716bdd18a13a774..0b06d57a5b4169fcab6c7fe35d5f5c142acfaa94 100644 (file)
 //
 // Run: npx vitest --project=client --run tests/client/agentic-stream.perf.svelte.test.ts
 
-import { describe, it } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
+import { perfState } from './components/agentic-perf-state.svelte';
 import AgenticPerfWrapper from './components/AgenticPerfWrapper.svelte';
 import ChatMessagesPerfWrapper from './components/ChatMessagesPerfWrapper.svelte';
-import { perfState } from './components/agentic-perf-state.svelte';
+import { MessageRole } from '$lib/enums';
 import { conversationsStore } from '$lib/stores/conversations.svelte';
 import type { DatabaseMessage } from '$lib/types';
-import { MessageRole } from '$lib/enums';
+import { tick } from 'svelte';
+import { describe, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 // --- fixture construction -------------------------------------------------
 
@@ -40,24 +40,28 @@ interface FixtureOpts {
 }
 
 const DEFAULTS: FixtureOpts = {
-       priorToolCalls: 0,
-       toolResultBytes: 1024,
        editFileEdits: 0,
        openCodeFence: false,
-       paragraphEvery: 0
+       paragraphEvery: 0,
+       priorToolCalls: 0,
+       toolResultBytes: 1024
 };
 
 function blob(bytes: number, seed: string): string {
        const line = `${seed} output line with some representative width to it`;
        const n = Math.max(1, Math.ceil(bytes / (line.length + 1)));
        const out: string[] = [];
+
        for (let i = 0; i < n; i++) out.push(`${line} ${i}`);
+
        return out.join('\n');
 }
 
 function diffLines(n: number, seed: string): string {
        const out: string[] = [];
+
        for (let i = 0; i < n; i++) out.push(`${seed} line ${i} const value_${i} = compute(${i});`);
+
        return out.join('\n');
 }
 
@@ -65,14 +69,14 @@ let msgSeq = 0;
 
 function baseMessage(overrides: Partial<DatabaseMessage>): DatabaseMessage {
        return {
-               id: `m${msgSeq++}`,
-               convId: 'perf-conv',
-               type: 'text',
-               timestamp: 0,
-               role: MessageRole.ASSISTANT,
+               children: [],
                content: '',
+               convId: 'perf-conv',
+               id: `m${msgSeq++}`,
                parent: null,
-               children: [],
+               role: MessageRole.ASSISTANT,
+               timestamp: 0,
+               type: 'text',
                ...overrides
        } as DatabaseMessage;
 }
@@ -86,48 +90,50 @@ function buildFixture(opts: FixtureOpts): {
 
        for (let i = 0; i < opts.priorToolCalls; i++) {
                const id = `call_${i}`;
+
                toolCalls.push({
-                       id,
-                       type: 'function',
                        function: {
-                               name: 'exec_shell_command',
-                               arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` })
-                       }
+                               arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` }),
+                               name: 'exec_shell_command'
+                       },
+                       id,
+                       type: 'function'
                });
                toolMessages.push(
                        baseMessage({
+                               content: `${blob(opts.toolResultBytes, `t${i}`)}\n[exit code: 0]`,
                                role: MessageRole.TOOL,
-                               toolCallId: id,
-                               content: `${blob(opts.toolResultBytes, `t${i}`)}\n[exit code: 0]`
+                               toolCallId: id
                        })
                );
        }
 
        for (let i = 0; i < opts.editFileEdits; i++) {
                const id = `edit_${i}`;
+
                toolCalls.push({
-                       id,
-                       type: 'function',
                        function: {
-                               name: 'edit_file',
                                arguments: JSON.stringify({
-                                       path: `/src/file_${i}.ts`,
-                                       edits: [{ old_text: diffLines(400, 'old'), new_text: diffLines(400, 'new') }]
-                               })
-                       }
+                                       edits: [{ new_text: diffLines(400, 'new'), old_text: diffLines(400, 'old') }],
+                                       path: `/src/file_${i}.ts`
+                               }),
+                               name: 'edit_file'
+                       },
+                       id,
+                       type: 'function'
                });
                toolMessages.push(
                        baseMessage({
+                               content: JSON.stringify({ edits_applied: 1, result: 'ok' }),
                                role: MessageRole.TOOL,
-                               toolCallId: id,
-                               content: JSON.stringify({ result: 'ok', edits_applied: 1 })
+                               toolCallId: id
                        })
                );
        }
 
        const message = baseMessage({
-               toolCalls: toolCalls.length > 0 ? JSON.stringify(toolCalls) : undefined,
-               content: ''
+               content: '',
+               toolCalls: toolCalls.length > 0 ? JSON.stringify(toolCalls) : undefined
        });
 
        return { message, toolMessages };
@@ -174,18 +180,21 @@ async function measure(label: string, partial: Partial<FixtureOpts>, tokens = 60
        await tick();
 
        const CHUNK = 'The quick brown fox jumps over the lazy dog. ';
+
        let accumulated = opts.openCodeFence ? '```notalanguage\n' : '';
-       const durations: number[] = [];
 
+       const durations: number[] = [];
        const wallStart = performance.now();
 
        for (let i = 0; i < tokens; i++) {
                accumulated += CHUNK;
+
                if (opts.paragraphEvery > 0 && (i + 1) % opts.paragraphEvery === 0) {
                        accumulated += '\n\n';
                }
 
                const t0 = performance.now();
+
                // Mirrors updateMessageAtIndex: a brand-new object identity per chunk.
                perfState.message = { ...perfState.message!, content: accumulated };
                await tick();
@@ -211,10 +220,10 @@ async function measure(label: string, partial: Partial<FixtureOpts>, tokens = 60
 
        results.push({
                label,
-               tokens,
+               max: durations[durations.length - 1],
                mean: total / durations.length,
                p95: durations[Math.floor(durations.length * 0.95)],
-               max: durations[durations.length - 1],
+               tokens,
                total,
                wall
        });
@@ -223,7 +232,6 @@ async function measure(label: string, partial: Partial<FixtureOpts>, tokens = 60
 function report() {
        const pad = (s: string, n: number) => s.padEnd(n);
        const num = (n: number) => n.toFixed(2).padStart(8);
-
        const header = `${pad('fixture', 40)}${pad('tok', 5)}${'mean'.padStart(8)}${'p95'.padStart(8)}${'max'.padStart(8)}${'sync'.padStart(9)}${'wall'.padStart(9)}`;
        const lines = [
                '',
@@ -268,56 +276,63 @@ async function measureConversation(
        agentic = false
 ) {
        const history: DatabaseMessage[] = [];
+
        for (let i = 0; i < priorMessages; i++) {
                const isAssistant = i % 2 !== 0;
 
                if (isAssistant && agentic) {
                        const id = `prior_call_${i}`;
+
                        history.push(
                                baseMessage({
-                                       role: MessageRole.ASSISTANT,
                                        content: `Message ${i}`,
+                                       role: MessageRole.ASSISTANT,
                                        toolCalls: JSON.stringify([
                                                {
-                                                       id,
-                                                       type: 'function',
                                                        function: {
-                                                               name: 'exec_shell_command',
-                                                               arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` })
-                                                       }
+                                                               arguments: JSON.stringify({ command: `grep -rn "thing_${i}" src/` }),
+                                                               name: 'exec_shell_command'
+                                                       },
+                                                       id,
+                                                       type: 'function'
                                                }
                                        ])
                                })
                        );
                        history.push(
                                baseMessage({
+                                       content: `${blob(1024, `r${i}`)}\n[exit code: 0]`,
                                        role: MessageRole.TOOL,
-                                       toolCallId: id,
-                                       content: `${blob(1024, `r${i}`)}\n[exit code: 0]`
+                                       toolCallId: id
                                })
                        );
+
                        continue;
                }
 
                history.push(
                        baseMessage({
-                               role: isAssistant ? MessageRole.ASSISTANT : MessageRole.USER,
-                               content: `Message ${i}: ${blob(512, `m${i}`)}`
+                               content: `Message ${i}: ${blob(512, `m${i}`)}`,
+                               role: isAssistant ? MessageRole.ASSISTANT : MessageRole.USER
                        })
                );
        }
 
-       const streaming = baseMessage({ role: MessageRole.ASSISTANT, content: '' });
+       const streaming = baseMessage({ content: '', role: MessageRole.ASSISTANT });
+
        history.push(streaming);
 
        conversationsStore.activeMessages = history;
 
        const { unmount } = render(ChatMessagesPerfWrapper);
+
        await tick();
 
        const idx = conversationsStore.findMessageIndex(streaming.id);
        const CHUNK = 'The quick brown fox jumps over the lazy dog. ';
+
        let accumulated = '';
+
        const durations: number[] = [];
        const wallStart = performance.now();
 
@@ -325,6 +340,7 @@ async function measureConversation(
                accumulated += CHUNK;
 
                const t0 = performance.now();
+
                // The real path: chat.svelte.ts -> conversations.svelte.ts.
                conversationsStore.updateMessageAtIndex(idx, { content: accumulated });
                await tick();
@@ -346,10 +362,10 @@ async function measureConversation(
 
        results.push({
                label,
-               tokens,
+               max: durations[durations.length - 1],
                mean: total / durations.length,
                p95: durations[Math.floor(durations.length * 0.95)],
-               max: durations[durations.length - 1],
+               tokens,
                total,
                wall
        });
index 39389f3d39eea72e8647d8bf10445b7efe8acfcb..061a38a61f65e36c65c88cb87d78ae94ffda6bb3 100644 (file)
@@ -1,11 +1,12 @@
-import { beforeEach, describe, expect, it } from 'vitest';
-import { validateApiKey } from '$lib/utils/api-key-validation';
-import { settingsStore } from '$lib/stores/settings.svelte';
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { settingsStore } from '$lib/stores/settings.svelte';
+import { validateApiKey } from '$lib/utils/api-key-validation';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 function fakeFetch(status: number, capture: { auth?: string | null } = {}) {
        return (async (_url: RequestInfo | URL, init?: RequestInit) => {
                capture.auth = (init?.headers as Record<string, string>)?.['Authorization'] ?? null;
+
                return new Response(status === 200 ? '{}' : 'Unauthorized', { status });
        }) as typeof globalThis.fetch;
 }
@@ -35,6 +36,7 @@ describe('api key validation surfaces the splash', () => {
        it('valid stored key: passes and sends the bearer header', async () => {
                settingsStore.updateConfig('apiKey', 'sk-good');
                const capture: { auth?: string | null } = {};
+
                await expect(validateApiKey(fakeFetch(200, capture))).resolves.toBeUndefined();
                expect(capture.auth).toBe('Bearer sk-good');
        });
index d704942086c00d98cea609cdb61c0bec1e4d59a9..e4b7d4a1509c0dab83b7dd44ceb980421fe4357a 100644 (file)
@@ -1,6 +1,6 @@
-import { describe, expect, it } from 'vitest';
 import { capImageDataURLSize } from '$lib/utils/cap-img-size';
 import { getJpegOrientationFromDataURL } from '$lib/utils/jpeg-orientation';
+import { describe, expect, it } from 'vitest';
 
 // Real 64x32 jpegs generated with Pillow, quality 90. The upright picture is
 // four solid quadrants: top left red, top right green, bottom left blue,
@@ -13,15 +13,12 @@ const EXIF5 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU
 const EXIF6 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDCooor+Tz+Hz7qooor+ej/AE9PhWiiiv6FP8wj7qooor+ej/T0/Keiiiv7CP72PAqKKK/3GP8AHQ99ooor/Dk/2LPAqKKK/wBxj/HQ/9k=`;
 const EXIF8 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAgAAAAAAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCABAACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD896KKK/1TPhz32iiiv8OT/Ys8Cooor/cY/wAdD32iiiv8OT/Ys/Viiiiv49P4JPhWiiiv6FP8wj7qooor+ej/AE9PhWiiiv6FP8wj/9k=`;
 const NOEXIF = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAgAEADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD50ooor8MP9UwooooA9uooor4I/wCcwKKKKAPhSiiiv+gM/qgKKKKAP3Vooor/AJdz+lQooooA/9k=`;
-
 const RED: Rgb = [255, 0, 0];
 const GREEN: Rgb = [0, 200, 0];
 const BLUE: Rgb = [0, 0, 255];
 const YELLOW: Rgb = [255, 220, 0];
-
 // Wide tolerance per channel, jpeg compression shifts solid colors a bit
 const COLOR_TOLERANCE = 70;
-
 // 0.000512 megapixels is 512 pixels, a quarter of the area of the 2048 pixel fixtures
 const QUARTER_AREA_MEGAPIXELS = 0.000512;
 
@@ -30,6 +27,7 @@ type Rgb = [number, number, number];
 function loadImage(dataUrl: string): Promise<HTMLImageElement> {
        return new Promise((resolve, reject) => {
                const img = new Image();
+
                img.onload = () => resolve(img);
                img.onerror = () => reject(new Error('Failed to decode image.'));
                img.src = dataUrl;
@@ -40,9 +38,11 @@ function loadImage(dataUrl: string): Promise<HTMLImageElement> {
 async function quadrantColors(dataUrl: string): Promise<Rgb[]> {
        const img = await loadImage(dataUrl);
        const canvas = document.createElement('canvas');
+
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        const ctx = canvas.getContext('2d')!;
+
        ctx.drawImage(img, 0, 0);
        const points = [
                [0.25, 0.25],
@@ -50,6 +50,7 @@ async function quadrantColors(dataUrl: string): Promise<Rgb[]> {
                [0.25, 0.75],
                [0.75, 0.75]
        ];
+
        return points.map(([fx, fy]) => {
                const d = ctx.getImageData(
                        Math.floor(canvas.width * fx),
@@ -57,12 +58,14 @@ async function quadrantColors(dataUrl: string): Promise<Rgb[]> {
                        1,
                        1
                ).data;
+
                return [d[0], d[1], d[2]];
        });
 }
 
 function expectUpright(colors: Rgb[]) {
        const targets = [RED, GREEN, BLUE, YELLOW];
+
        for (let i = 0; i < 4; i++) {
                for (let c = 0; c < 3; c++) {
                        expect(Math.abs(colors[i][c] - targets[i][c])).toBeLessThan(COLOR_TOLERANCE);
index 4560a7a9cbf9e270fcc47c1f4fc810343aa56746..602879b991606e49b53aff753ba874587567783f 100644 (file)
@@ -3,16 +3,18 @@
 // serialization must fold those back into `\n` so the emitted value never
 // diverges from what is on screen.
 
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import ChatFormContenteditableHarness from './components/ChatFormContenteditableHarness.svelte';
+import { tick } from 'svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 const SOURCE = 'see [docs](file:///a/b) here';
 
 function editableIn(container: HTMLElement): HTMLElement {
        const el = container.querySelector('[role="textbox"]');
+
        if (!(el instanceof HTMLElement)) throw new Error('contenteditable not rendered');
+
        return el;
 }
 
@@ -22,10 +24,13 @@ function fireInput(root: HTMLElement) {
 
 function setCaret(node: Node, offset: number) {
        const range = document.createRange();
+
        range.setStart(node, offset);
        range.setEnd(node, offset);
        const selection = window.getSelection();
+
        if (!selection) throw new Error('no selection');
+
        selection.removeAllRanges();
        selection.addRange(range);
 }
@@ -33,10 +38,12 @@ function setCaret(node: Node, offset: number) {
 describe('ChatFormContenteditable browser newline shapes', () => {
        it('serializes a Chromium Enter <div> wrapper as a newline', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(screen.container);
                const div = document.createElement('div');
+
                div.textContent = 'second line';
                root.appendChild(div);
                fireInput(root);
@@ -47,12 +54,15 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('serializes a Firefox full <div> wrap as lines, badge included', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(screen.container);
                const first = document.createElement('div');
+
                while (root.firstChild) first.appendChild(root.firstChild);
                const second = document.createElement('div');
+
                second.textContent = 'second line';
                root.appendChild(first);
                root.appendChild(second);
@@ -64,9 +74,11 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('serializes a <br> as a newline', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'here' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                root.appendChild(document.createElement('br'));
                root.appendChild(document.createTextNode('second line'));
                fireInput(root);
@@ -77,9 +89,11 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('ignores a trailing <br> (browser caret placeholder)', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                root.appendChild(document.createElement('br'));
                fireInput(root);
                await tick();
@@ -89,11 +103,14 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('serializes one newline per empty-line <div><br></div>', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                for (let i = 0; i < 2; i++) {
                        const div = document.createElement('div');
+
                        div.appendChild(document.createElement('br'));
                        root.appendChild(div);
                }
@@ -105,10 +122,12 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('treats a <div><br></div>-only buffer as empty for the placeholder', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
                const div = document.createElement('div');
+
                div.appendChild(document.createElement('br'));
                root.replaceChildren(div);
                fireInput(root);
@@ -120,12 +139,14 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
        it('maps the caret across block boundaries in both directions', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc\ndef' });
+
                await tick();
 
                // Rebuild into the Chromium block shape; the source is unchanged,
                // so no re-render fires.
                const root = editableIn(screen.container);
                const div = document.createElement('div');
+
                div.textContent = 'def';
                root.replaceChildren(document.createTextNode('abc'), div);
                fireInput(root);
@@ -133,6 +154,7 @@ describe('ChatFormContenteditable browser newline shapes', () => {
                expect(screen.component.getValue()).toBe('abc\ndef');
 
                const divText = div.firstChild;
+
                if (!divText) throw new Error('div text missing');
 
                setCaret(divText, 2);
@@ -140,6 +162,7 @@ describe('ChatFormContenteditable browser newline shapes', () => {
 
                screen.component.setCaretOffset(6);
                const selection = window.getSelection();
+
                expect(selection?.anchorNode).toBe(divText);
                expect(selection?.anchorOffset).toBe(2);
 
index a412bb1668a553dc88167da5d96c920992091e6a..17406f95eaa9a59874f386ddeef4c5fb548eefd6 100644 (file)
@@ -3,16 +3,18 @@
 // the native undo stack), and Tab is NOT intercepted (WCAG 2.1.2 no
 // keyboard trap), matching the plain textarea.
 
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import ChatFormContenteditableHarness from './components/ChatFormContenteditableHarness.svelte';
+import { tick } from 'svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 const SOURCE = 'see [docs](file:///a/b)';
 
 function editableIn(container: HTMLElement): HTMLElement {
        const el = container.querySelector('[role="textbox"]');
+
        if (!(el instanceof HTMLElement)) throw new Error('contenteditable not rendered');
+
        return el;
 }
 
@@ -23,26 +25,32 @@ function type(root: HTMLElement, text: string, inputType = 'insertText') {
 
 function keydown(root: HTMLElement, init: KeyboardEventInit) {
        const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init });
+
        root.dispatchEvent(event);
+
        return event;
 }
 
 describe('ChatFormContenteditable undo/redo', () => {
        it('undoes and redoes an edit across a badge-containing buffer', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                type(root, ' more');
                await tick();
                expect(screen.component.getValue()).toBe(`${SOURCE} more`);
 
-               const undoEvent = keydown(root, { key: 'z', ctrlKey: true });
+               const undoEvent = keydown(root, { ctrlKey: true, key: 'z' });
+
                await tick();
                expect(undoEvent.defaultPrevented).toBe(true);
                expect(screen.component.getValue()).toBe(SOURCE);
 
-               const redoEvent = keydown(root, { key: 'z', ctrlKey: true, shiftKey: true });
+               const redoEvent = keydown(root, { ctrlKey: true, key: 'z', shiftKey: true });
+
                await tick();
                expect(redoEvent.defaultPrevented).toBe(true);
                expect(screen.component.getValue()).toBe(`${SOURCE} more`);
@@ -50,60 +58,68 @@ describe('ChatFormContenteditable undo/redo', () => {
 
        it('redoes with Ctrl+Y as well', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                type(root, ' more');
                await tick();
                keydown(root, { key: 'z', metaKey: true });
                await tick();
                expect(screen.component.getValue()).toBe(SOURCE);
 
-               keydown(root, { key: 'y', ctrlKey: true });
+               keydown(root, { ctrlKey: true, key: 'y' });
                await tick();
                expect(screen.component.getValue()).toBe(`${SOURCE} more`);
        });
 
        it('coalesces a typing burst into one undo step', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                type(root, 'd');
                type(root, 'e');
                await tick();
                expect(screen.component.getValue()).toBe('abcde');
 
-               keydown(root, { key: 'z', ctrlKey: true });
+               keydown(root, { ctrlKey: true, key: 'z' });
                await tick();
                expect(screen.component.getValue()).toBe('abc');
        });
 
        it('keeps a newline as its own undo step', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                type(root, 'd');
                type(root, '\n', 'insertLineBreak');
                await tick();
                expect(screen.component.getValue()).toBe('abcd\n');
 
-               keydown(root, { key: 'z', ctrlKey: true });
+               keydown(root, { ctrlKey: true, key: 'z' });
                await tick();
                expect(screen.component.getValue()).toBe('abcd');
 
-               keydown(root, { key: 'z', ctrlKey: true });
+               keydown(root, { ctrlKey: true, key: 'z' });
                await tick();
                expect(screen.component.getValue()).toBe('abc');
        });
 
        it('is a no-op when there is nothing to undo', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
-               const event = keydown(root, { key: 'z', ctrlKey: true });
+               const event = keydown(root, { ctrlKey: true, key: 'z' });
+
                await tick();
 
                expect(event.defaultPrevented).toBe(true);
@@ -112,18 +128,20 @@ describe('ChatFormContenteditable undo/redo', () => {
 
        it('abandons the redo branch after a fresh edit', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: 'abc' });
+
                await tick();
 
                const root = editableIn(screen.container);
+
                type(root, 'd');
                await tick();
-               keydown(root, { key: 'z', ctrlKey: true });
+               keydown(root, { ctrlKey: true, key: 'z' });
                await tick();
                expect(screen.component.getValue()).toBe('abc');
 
                type(root, 'e');
                await tick();
-               keydown(root, { key: 'z', ctrlKey: true, shiftKey: true });
+               keydown(root, { ctrlKey: true, key: 'z', shiftKey: true });
                await tick();
                expect(screen.component.getValue()).toBe('abce');
        });
@@ -132,6 +150,7 @@ describe('ChatFormContenteditable undo/redo', () => {
 describe('ChatFormContenteditable Tab key', () => {
        it('does not trap Tab (focus can leave the editable)', async () => {
                const screen = render(ChatFormContenteditableHarness, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(screen.container);
index c90f8a83d9be1e5caae85433fc00ce2911f2f6af..d10b15c827980f6a1fb30170dca139d15e014050 100644 (file)
@@ -3,47 +3,58 @@
 // contributes its full `[name](file://...)` link) and pasting such
 // markdown re-renders the badges.
 
-import { describe, it, expect, vi } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { userEvent } from 'vitest/browser';
-import { tick } from 'svelte';
-import { rangeToTextOffset, serializeContent, textOffsetToRange } from '$lib/utils';
 import ChatFormContenteditable from '$lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte';
+import { rangeToTextOffset, serializeContent, textOffsetToRange } from '$lib/utils';
+import { tick } from 'svelte';
+import { describe, expect, it, vi } from 'vitest';
+import { userEvent } from 'vitest/browser';
+import { render } from 'vitest-browser-svelte';
 
 const SOURCE = 'hello [docs](file:///a/b) world';
 const BADGE_SELECTOR = '[data-mention-badge="true"]';
 
 function editableIn(container: HTMLElement): HTMLElement {
        const el = container.querySelector('[role="textbox"]');
+
        if (!(el instanceof HTMLElement)) throw new Error('contenteditable not rendered');
+
        return el;
 }
 
 function setSelection(root: HTMLElement, place: (range: Range, root: HTMLElement) => void) {
        const range = document.createRange();
+
        place(range, root);
        const selection = window.getSelection();
+
        if (!selection) throw new Error('no selection');
+
        selection.removeAllRanges();
        selection.addRange(range);
 }
 
 function clipboardEvent(type: 'copy' | 'cut' | 'paste', text = '') {
        const data = new DataTransfer();
+
        if (text) data.setData('text/plain', text);
-       const event = new ClipboardEvent(type, { clipboardData: data, bubbles: true, cancelable: true });
-       return { event, data };
+
+       const event = new ClipboardEvent(type, { bubbles: true, cancelable: true, clipboardData: data });
+
+       return { data, event };
 }
 
 describe('ChatFormContenteditable clipboard', () => {
        it('copy exposes the markdown source of the selection', async () => {
                const { container } = render(ChatFormContenteditable, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                setSelection(root, (range) => range.selectNodeContents(root));
 
-               const { event, data } = clipboardEvent('copy');
+               const { data, event } = clipboardEvent('copy');
+
                root.dispatchEvent(event);
 
                expect(event.defaultPrevented).toBe(true);
@@ -52,17 +63,22 @@ describe('ChatFormContenteditable clipboard', () => {
 
        it('cut exposes the markdown source and removes the slice', async () => {
                const { container } = render(ChatFormContenteditable, { value: SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                setSelection(root, (range) => {
                        const badge = root.querySelector(BADGE_SELECTOR);
+
                        if (!badge) throw new Error('badge not rendered');
+
                        range.setStartBefore(badge);
                        range.setEndAfter(badge);
                });
 
-               const { event, data } = clipboardEvent('cut');
+               const { data, event } = clipboardEvent('cut');
+
                root.dispatchEvent(event);
 
                expect(event.defaultPrevented).toBe(true);
@@ -73,9 +89,11 @@ describe('ChatFormContenteditable clipboard', () => {
 
        it('paste of markdown mention links re-renders badges', async () => {
                const { container } = render(ChatFormContenteditable, { value: 'hello ' });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        range.selectNodeContents(root);
@@ -83,11 +101,13 @@ describe('ChatFormContenteditable clipboard', () => {
                });
 
                const { event } = clipboardEvent('paste', '[docs](file:///a/b) world');
+
                root.dispatchEvent(event);
                await tick();
 
                expect(event.defaultPrevented).toBe(true);
                const badge = root.querySelector(BADGE_SELECTOR);
+
                expect(badge).not.toBeNull();
                expect(badge!.getAttribute('data-mention-name')).toBe('docs');
                expect(root.textContent).toContain('world');
@@ -95,17 +115,19 @@ describe('ChatFormContenteditable clipboard', () => {
 
        it('paste without mention links keeps the DOM untouched', async () => {
                const { container } = render(ChatFormContenteditable, { value: 'hello ' });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        range.selectNodeContents(root);
                        range.collapse(false);
                });
                const firstChild = root.firstChild;
-
                const { event } = clipboardEvent('paste', 'plain text');
+
                root.dispatchEvent(event);
                await tick();
 
@@ -119,10 +141,12 @@ describe('ChatFormContenteditable clipboard', () => {
 describe('ChatFormContenteditable code spans', () => {
        it('renders inline code from the initial value', async () => {
                const { container } = render(ChatFormContenteditable, { value: 'run `npm test` now' });
+
                await tick();
 
                const root = editableIn(container);
                const code = root.querySelector('code[data-code-token="inline"]');
+
                expect(code).not.toBeNull();
                expect(code!.textContent).toBe('`npm test`');
        });
@@ -130,10 +154,12 @@ describe('ChatFormContenteditable code spans', () => {
        it('renders a fenced code block with a language', async () => {
                const source = 'before\n```js\nconst a = 1;\n```\nafter';
                const { container } = render(ChatFormContenteditable, { value: source });
+
                await tick();
 
                const root = editableIn(container);
                const code = root.querySelector('code[data-code-token="block"]');
+
                expect(code).not.toBeNull();
                expect(code!.textContent).toBe('```js\nconst a = 1;\n```');
        });
@@ -141,12 +167,15 @@ describe('ChatFormContenteditable code spans', () => {
        it('copy exposes the markdown source of a selection spanning code', async () => {
                const source = 'run `npm test` now';
                const { container } = render(ChatFormContenteditable, { value: source });
+
                await tick();
 
                const root = editableIn(container);
+
                setSelection(root, (range) => range.selectNodeContents(root));
 
-               const { event, data } = clipboardEvent('copy');
+               const { data, event } = clipboardEvent('copy');
+
                root.dispatchEvent(event);
 
                expect(event.defaultPrevented).toBe(true);
@@ -155,9 +184,11 @@ describe('ChatFormContenteditable code spans', () => {
 
        it('paste of a code span renders the styled element', async () => {
                const { container } = render(ChatFormContenteditable, { value: 'run ' });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        range.selectNodeContents(root);
@@ -165,11 +196,13 @@ describe('ChatFormContenteditable code spans', () => {
                });
 
                const { event } = clipboardEvent('paste', '`npm test` now');
+
                root.dispatchEvent(event);
                await tick();
 
                expect(event.defaultPrevented).toBe(true);
                const code = root.querySelector('code[data-code-token="inline"]');
+
                expect(code).not.toBeNull();
                expect(code!.textContent).toBe('`npm test`');
                expect(root.textContent).toContain('now');
@@ -178,10 +211,12 @@ describe('ChatFormContenteditable code spans', () => {
        it('highlights a fenced block content and stays byte-exact', async () => {
                const source = '```js\nconst a = 1;\n```';
                const { container } = render(ChatFormContenteditable, { value: source });
+
                await tick();
 
                const root = editableIn(container);
                const code = root.querySelector('code[data-code-token="block"]');
+
                expect(code).not.toBeNull();
                expect(code!.querySelector('.hljs-keyword')).not.toBeNull();
                expect(code!.textContent).toBe(source);
@@ -189,9 +224,11 @@ describe('ChatFormContenteditable code spans', () => {
 
        it('does not highlight inline code', async () => {
                const { container } = render(ChatFormContenteditable, { value: 'run `const` now' });
+
                await tick();
 
                const root = editableIn(container);
+
                expect(root.querySelector('[class*="hljs-"]')).toBeNull();
        });
 });
@@ -202,7 +239,9 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        function blockIn(root: HTMLElement): HTMLElement {
                const el = root.querySelector(BLOCK_SELECTOR);
+
                if (!(el instanceof HTMLElement)) throw new Error('code block not rendered');
+
                return el;
        }
 
@@ -210,11 +249,15 @@ describe('ChatFormContenteditable code block escape hatches', () => {
        function placeCaretInBlock(root: HTMLElement, where: 'start' | 'end') {
                const code = blockIn(root);
                const walker = document.createTreeWalker(code, NodeFilter.SHOW_TEXT);
+
                let target: Node | null = null;
+
                for (let n = walker.nextNode(); n; n = walker.nextNode()) {
                        target = where === 'start' ? (target ?? n) : n;
                }
+
                if (!target) throw new Error('no text inside code block');
+
                setSelection(root, (range) => {
                        range.setStart(target!, where === 'start' ? 0 : (target!.textContent ?? '').length);
                        range.collapse(true);
@@ -223,21 +266,26 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        function caretContainer(): Node {
                const selection = window.getSelection();
+
                if (!selection || selection.rangeCount === 0) throw new Error('no selection');
+
                return selection.getRangeAt(0).startContainer;
        }
 
        it('pads a trailing code block with a br hatch that stays invisible to copy', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                // no permanent empty line above a leading block
                expect(root.firstChild).toBe(blockIn(root));
                expect(root.lastChild?.nodeName).toBe('BR');
 
                setSelection(root, (range) => range.selectNodeContents(root));
-               const { event, data } = clipboardEvent('copy');
+               const { data, event } = clipboardEvent('copy');
+
                root.dispatchEvent(event);
 
                expect(data.getData('text/plain')).toBe(BLOCK_SOURCE);
@@ -245,9 +293,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('escapes a trailing code block with ArrowDown and types after it', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
@@ -269,9 +319,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('escapes a leading code block with ArrowUp and types before it', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'start');
 
@@ -292,9 +344,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('escapes a leading code block with ArrowLeft from its first character', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'start');
 
@@ -305,9 +359,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('removes the transient leading hatch when the caret moves back into the block', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'start');
 
@@ -323,15 +379,18 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('extends the selection out of the block with Shift+ArrowDown', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
                await userEvent.keyboard('{Shift>}{ArrowDown}{/Shift}');
 
                const selection = window.getSelection();
+
                expect(selection).not.toBeNull();
                expect(selection!.isCollapsed).toBe(false);
                expect(blockIn(root).contains(selection!.getRangeAt(0).endContainer)).toBe(false);
@@ -339,9 +398,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('line-separates text typed right after the closing fence', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
@@ -359,9 +420,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('does not double the newline when Shift+Enter already added one', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
@@ -377,9 +440,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                const { container } = render(ChatFormContenteditable, {
                        value: BLOCK_SOURCE + '\ntext after the code block'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
 
                // post-break DOM some browsers produce: the inserted newline plus
@@ -392,10 +457,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                        range.collapse(true);
                });
 
-               root.dispatchEvent(new InputEvent('input', { inputType: 'insertLineBreak', bubbles: true }));
+               root.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertLineBreak' }));
                await tick();
 
                const selection = window.getSelection();
+
                expect(rangeToTextOffset(root, selection!.getRangeAt(0))).toBe(
                        (BLOCK_SOURCE + '\ntext after the code block\n').length
                );
@@ -406,9 +472,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                const { container } = render(ChatFormContenteditable, {
                        value: BLOCK_SOURCE + '\ntext after the code block'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
 
                // post-break DOM some browsers produce: a lone trailing \n (or a
@@ -421,10 +489,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                        range.collapse(true);
                });
 
-               root.dispatchEvent(new InputEvent('input', { inputType: 'insertLineBreak', bubbles: true }));
+               root.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertLineBreak' }));
                await tick();
 
                const selection = window.getSelection();
+
                expect(rangeToTextOffset(root, selection!.getRangeAt(0))).toBe(
                        (BLOCK_SOURCE + '\ntext after the code block\n').length
                );
@@ -435,12 +504,15 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                const { container } = render(ChatFormContenteditable, {
                        value: BLOCK_SOURCE + '\ntext after the code block'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        const text = root.childNodes[1];
+
                        range.setStart(text, (text.textContent ?? '').length);
                        range.collapse(true);
                });
@@ -449,6 +521,7 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                await tick();
 
                const selection = window.getSelection();
+
                expect(rangeToTextOffset(root, selection!.getRangeAt(0))).toBe(
                        (BLOCK_SOURCE + '\ntext after the code block\n').length
                );
@@ -462,9 +535,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('lets Backspace at the text start move into the block without a source fight', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
@@ -486,9 +561,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
 
        it('lets forward Delete eat the text after a block normally', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                placeCaretInBlock(root, 'end');
 
@@ -507,9 +584,11 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                const { container } = render(ChatFormContenteditable, {
                        value: BLOCK_SOURCE + '\nhello'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                expect(root.textContent).toBe(BLOCK_SOURCE + 'hello');
                expect(serializeContent(root)).toBe(BLOCK_SOURCE + '\nhello');
        });
@@ -518,23 +597,28 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                const { container } = render(ChatFormContenteditable, {
                        value: BLOCK_SOURCE + '\n\nhello'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                expect(root.textContent).toBe(BLOCK_SOURCE + '\nhello');
                expect(serializeContent(root)).toBe(BLOCK_SOURCE + '\n\nhello');
        });
 
        it('re-highlights while typing inside a block and keeps the caret', async () => {
                const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
 
                // caret at the start of the block content (after the opening fence)
                setSelection(root, (range) => {
                        const target = textOffsetToRange(root, 6);
+
                        range.setStart(target.startContainer, target.startOffset);
                        range.collapse(true);
                });
@@ -543,11 +627,13 @@ describe('ChatFormContenteditable code block escape hatches', () => {
                await tick();
 
                const code = blockIn(root);
+
                expect(serializeContent(root)).toBe('```js\nxconst a = 1;\n```');
                expect(code.textContent).toBe('```js\nxconst a = 1;\n```');
                expect(code.querySelector('.hljs-number')).not.toBeNull();
 
                const selection = window.getSelection();
+
                expect(code.contains(selection!.getRangeAt(0).startContainer)).toBe(true);
                expect(rangeToTextOffset(root, selection!.getRangeAt(0))).toBe(7);
        });
@@ -559,17 +645,20 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('adds a line instead of submitting on plain Enter inside a block', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: BLOCK_SOURCE,
-                       onKeydown
+                       onKeydown,
+                       value: BLOCK_SOURCE
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
 
                // caret at the start of the block content (after the opening fence)
                setSelection(root, (range) => {
                        const target = textOffsetToRange(root, 6);
+
                        range.setStart(target.startContainer, target.startOffset);
                        range.collapse(true);
                });
@@ -583,6 +672,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
 
                const code = root.querySelector('code[data-code-token="block"]');
                const selection = window.getSelection();
+
                expect(code!.contains(selection!.getRangeAt(0).startContainer)).toBe(true);
                expect(rangeToTextOffset(root, selection!.getRangeAt(0))).toBe(7);
        });
@@ -590,17 +680,20 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('adds a line after a still-open fence (no closing ``` yet)', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: '```js\nconst a = 1;',
-                       onKeydown
+                       onKeydown,
+                       value: '```js\nconst a = 1;'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
 
                // caret at the start of the block content (after the opening fence)
                setSelection(root, (range) => {
                        const target = textOffsetToRange(root, 6);
+
                        range.setStart(target.startContainer, target.startOffset);
                        range.collapse(true);
                });
@@ -615,12 +708,14 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('forwards plain Enter to the parent when the caret is outside a block', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: BLOCK_SOURCE + '\nafter',
-                       onKeydown
+                       onKeydown,
+                       value: BLOCK_SOURCE + '\nafter'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        range.selectNodeContents(root);
@@ -636,12 +731,14 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('forwards plain Enter on the trailing hatch line after a block', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: BLOCK_SOURCE,
-                       onKeydown
+                       onKeydown,
+                       value: BLOCK_SOURCE
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                // root-level caret between the block and its trailing br hatch
                setSelection(root, (range) => {
@@ -657,15 +754,18 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('forwards Ctrl+Enter inside a block so explicit submit survives', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: BLOCK_SOURCE,
-                       onKeydown
+                       onKeydown,
+                       value: BLOCK_SOURCE
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                setSelection(root, (range) => {
                        const target = textOffsetToRange(root, 6);
+
                        range.setStart(target.startContainer, target.startOffset);
                        range.collapse(true);
                });
@@ -673,7 +773,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
                await userEvent.keyboard('{Control>}{Enter}{/Control}');
 
                expect(onKeydown).toHaveBeenCalledWith(
-                       expect.objectContaining({ key: 'Enter', ctrlKey: true })
+                       expect.objectContaining({ ctrlKey: true, key: 'Enter' })
                );
                expect(serializeContent(root)).toBe(BLOCK_SOURCE);
        });
@@ -681,14 +781,17 @@ describe('ChatFormContenteditable Enter in code blocks', () => {
        it('forwards Enter inside an inline code span', async () => {
                const onKeydown = vi.fn();
                const { container } = render(ChatFormContenteditable, {
-                       value: 'run `npm test` now',
-                       onKeydown
+                       onKeydown,
+                       value: 'run `npm test` now'
                });
+
                await tick();
 
                const root = editableIn(container);
+
                root.focus();
                const code = root.querySelector('code[data-code-token="inline"]')!;
+
                setSelection(root, (range) => {
                        range.setStart(code.firstChild!, 3);
                        range.collapse(true);
index c5d2f126caafa2157a362bb3c47de9ef5a2e435b..9e684a7635b533ac6bda9d0e866346657427a686 100644 (file)
@@ -5,17 +5,19 @@
 // The textarea path is covered here end-to-end (the contenteditable
 // consumes the same case locally; see chat-form-contenteditable).
 
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { userEvent } from 'vitest/browser';
-import { tick } from 'svelte';
+import ChatFormTestWrapper from './components/ChatFormTestWrapper.svelte';
 import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
 import { settingsStore } from '$lib/stores/settings.svelte';
-import ChatFormTestWrapper from './components/ChatFormTestWrapper.svelte';
+import { tick } from 'svelte';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { userEvent } from 'vitest/browser';
+import { render } from 'vitest-browser-svelte';
 
 function textareaIn(container: HTMLElement): HTMLTextAreaElement {
        const el = container.querySelector('textarea');
+
        if (!(el instanceof HTMLTextAreaElement)) throw new Error('textarea not rendered');
+
        return el;
 }
 
@@ -27,9 +29,11 @@ describe('ChatForm Enter in code blocks', () => {
        it('adds a line after a still-open fence instead of submitting', async () => {
                const onSubmit = vi.fn();
                const { container } = render(ChatFormTestWrapper, { onSubmit });
+
                await tick();
 
                const textarea = textareaIn(container);
+
                await userEvent.click(textarea);
                await userEvent.keyboard('```');
                await tick();
@@ -44,9 +48,11 @@ describe('ChatForm Enter in code blocks', () => {
        it('keeps adding lines while the block stays open', async () => {
                const onSubmit = vi.fn();
                const { container } = render(ChatFormTestWrapper, { onSubmit });
+
                await tick();
 
                const textarea = textareaIn(container);
+
                await userEvent.click(textarea);
                await userEvent.keyboard('```js');
                await tick();
@@ -63,9 +69,11 @@ describe('ChatForm Enter in code blocks', () => {
        it('submits when the caret is before the opening fence', async () => {
                const onSubmit = vi.fn();
                const { container } = render(ChatFormTestWrapper, { onSubmit });
+
                await tick();
 
                const textarea = textareaIn(container);
+
                await userEvent.click(textarea);
                await userEvent.keyboard('```');
                await tick();
@@ -81,9 +89,11 @@ describe('ChatForm Enter in code blocks', () => {
        it('submits on Enter outside a code block', async () => {
                const onSubmit = vi.fn();
                const { container } = render(ChatFormTestWrapper, { onSubmit });
+
                await tick();
 
                const textarea = textareaIn(container);
+
                await userEvent.click(textarea);
                await userEvent.keyboard('hello');
                await tick();
@@ -97,9 +107,11 @@ describe('ChatForm Enter in code blocks', () => {
        it('submits on Ctrl+Enter even inside a code block', async () => {
                const onSubmit = vi.fn();
                const { container } = render(ChatFormTestWrapper, { onSubmit });
+
                await tick();
 
                const textarea = textareaIn(container);
+
                await userEvent.click(textarea);
                await userEvent.keyboard('```');
                await tick();
index 1a5b97877cde28ef56eaf31de792ee81c0113b40..aaee18c991c690a2295db5e4caca14ebcae86e87 100644 (file)
@@ -3,20 +3,19 @@
 // it, the picker still opens but explains why instead of firing searches
 // that would only fail with "Search failed".
 
-import { describe, it, expect, afterEach } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import ChatFormMentionPicker from '$lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte';
-import { toolsStore } from '$lib/stores/tools.svelte';
-import { BuiltInTool } from '$lib/enums';
 import { DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY } from '$lib/constants';
+import { BuiltInTool } from '$lib/enums';
+import { toolsStore } from '$lib/stores/tools.svelte';
 import type { OpenAIToolDefinition } from '$lib/types';
+import { tick } from 'svelte';
+import { afterEach, describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 const FILE_SEARCH_DEF: OpenAIToolDefinition = {
-       type: 'function',
-       function: { name: BuiltInTool.FILE_GLOB_SEARCH, description: '', parameters: {} }
+       function: { description: '', name: BuiltInTool.FILE_GLOB_SEARCH, parameters: {} },
+       type: 'function'
 };
-
 const FILE_SEARCH_KEY = `builtin:${BuiltInTool.FILE_GLOB_SEARCH}`;
 
 // The store keeps its builtin tool list private; tests inject it through
@@ -28,9 +27,9 @@ function setBuiltinTools(defs: OpenAIToolDefinition[]) {
 function renderPicker() {
        return render(ChatFormMentionPicker, {
                isOpen: true,
-               query: 'main',
                onClose: () => {},
-               onSelect: () => {}
+               onSelect: () => {},
+               query: 'main'
        });
 }
 
index 1cd27c27eea8834e00d949af22d78d914d54ba25..d053f7f60cff3c0254ed2c338308c383cc838802 100644 (file)
@@ -3,20 +3,22 @@
 // Typing `/model is broken` is prose until the command is picked - the
 // buffer must survive; only an actual selection consumes the token.
 
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import ChatFormPickersHarness from './components/ChatFormPickersHarness.svelte';
+import { tick } from 'svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 describe('slash command dispatch', () => {
        it('does not dispatch or clear the buffer when a space follows the name', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                screen.component.type('/model is broken');
                await tick();
 
                const pickers = screen.component.getPickers();
+
                expect(screen.component.getValue()).toBe('/model is broken');
                expect(screen.component.getCalls()).not.toContain('openModelSelector');
                expect(screen.component.getCalls().some((c) => c.startsWith('setValue:'))).toBe(false);
@@ -26,6 +28,7 @@ describe('slash command dispatch', () => {
 
        it('dispatches /model on explicit selection and consumes the token', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                screen.component.type('/model is broken');
@@ -33,7 +36,9 @@ describe('slash command dispatch', () => {
 
                const pickers = screen.component.getPickers();
                const model = pickers.availableCommands.find((c) => c.name === 'model');
+
                if (!model) throw new Error('model command missing');
+
                pickers.handleCommandSelect(model);
                await tick();
 
@@ -44,16 +49,20 @@ describe('slash command dispatch', () => {
 
        it('seeds the prompt picker search from the token args on selection', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                screen.component.type('/prompt weather');
                await tick();
 
                const pickers = screen.component.getPickers();
+
                expect(pickers.isPromptPickerOpen).toBe(false);
 
                const prompt = pickers.availableCommands.find((c) => c.name === 'prompt');
+
                if (!prompt) throw new Error('prompt command missing');
+
                pickers.handleCommandSelect(prompt);
                await tick();
 
@@ -64,16 +73,20 @@ describe('slash command dispatch', () => {
 
        it('normalizes a partial /cwd token on selection and keeps it in the buffer', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                screen.component.type('/cw docs');
                await tick();
 
                const pickers = screen.component.getPickers();
+
                expect(pickers.isWorkingDirectoryPickerOpen).toBe(false);
 
                const cwd = pickers.availableCommands.find((c) => c.name === 'cwd');
+
                if (!cwd) throw new Error('cwd command missing');
+
                pickers.handleCommandSelect(cwd);
                await tick();
 
@@ -84,10 +97,12 @@ describe('slash command dispatch', () => {
 
        it('syncs the /cwd token into the picker search while the picker is open', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                const pickers = screen.component.getPickers();
                const cwd = pickers.availableCommands.find((c) => c.name === 'cwd');
+
                if (!cwd) throw new Error('cwd command missing');
 
                screen.component.type('/cwd docs');
@@ -104,10 +119,12 @@ describe('slash command dispatch', () => {
 
        it('abandons the /cwd picker when the token is edited away from /cwd', async () => {
                const screen = render(ChatFormPickersHarness);
+
                await tick();
 
                const pickers = screen.component.getPickers();
                const cwd = pickers.availableCommands.find((c) => c.name === 'cwd');
+
                if (!cwd) throw new Error('cwd command missing');
 
                screen.component.type('/cwd docs');
index 9b29bea7366efdfde554395570ec06c73057ef79..c82210176a1d22e03ea2e1cb4d4de581c35f58fc 100644 (file)
@@ -6,29 +6,30 @@
 // ~41ms/section/token for a 200KB result). These tests pin the fix: closed means
 // not rendered, and opening still mounts the body.
 
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import CollapsibleLazyBodyHarness from './components/CollapsibleLazyBodyHarness.svelte';
+import { tick } from 'svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 const MARKER = 'collapsible-body-marker';
 
 describe('collapsible wrappers render their body lazily', () => {
        for (const variant of ['content', 'terminal'] as const) {
                it(`${variant}: body is absent while closed and present once open`, async () => {
-                       const screen = render(CollapsibleLazyBodyHarness, { variant, open: false });
+                       const screen = render(CollapsibleLazyBodyHarness, { open: false, variant });
+
                        await tick();
 
                        expect(document.body.textContent).not.toContain(MARKER);
 
-                       await screen.rerender({ variant, open: true });
+                       await screen.rerender({ open: true, variant });
                        await tick();
 
                        expect(document.body.textContent).toContain(MARKER);
 
                        // And it unmounts again on close, so a collapsed block stops costing
                        // anything during streaming.
-                       await screen.rerender({ variant, open: false });
+                       await screen.rerender({ open: false, variant });
                        await tick();
 
                        expect(document.body.textContent).not.toContain(MARKER);
index 8f95860b72a78d9ed1dcd065d473b2d09a13b9c2..15c390a5ecb078d04aab27250a321925ba34a14c 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
-       import * as Tooltip from '$lib/components/ui/tooltip';
-       import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
        import { perfState } from './agentic-perf-state.svelte';
+       import ChatMessageAgenticContent from '$lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte';
+       import * as Tooltip from '$lib/components/ui/tooltip';
 </script>
 
 <Tooltip.Provider>
index bfc567e62ba55894d8ef560837a760a4f2c8b666..fac773756c1175d2a629e9a27b38e152e6abaac2 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { untrack } from 'svelte';
        import ChatFormContenteditable from '$lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte';
+       import { untrack } from 'svelte';
 
        interface Props {
                value?: string;
index 8e5e56c20b06d23c2cf190d06345b5594ec36c5e..e8ef6465b9db0b3b4ada84de91d9d59accc3378b 100644 (file)
@@ -9,25 +9,25 @@
        const calls: string[] = [];
 
        const pickers = useChatFormPickers({
-               getValue: () => value,
-               setValue: (v) => {
-                       value = v;
-                       calls.push(`setValue:${v}`);
-               },
-               getCaretOffset: () => caretOffset,
-               setCaretOffset: (o) => {
-                       caretOffset = o;
-               },
                focusInput: () => {},
-               getShowModelSelector: () => true,
-               hasPrompts: () => true,
-               hasCwdTools: () => true,
+               getCaretOffset: () => caretOffset,
                getCwd: () => null,
+               getPickersRef: () => undefined,
                getServerHome: () => null,
+               getShowModelSelector: () => true,
+               getValue: () => value,
+               hasCwdTools: () => true,
+               hasPrompts: () => true,
                openModelSelector: () => {
                        calls.push('openModelSelector');
                },
-               getPickersRef: () => undefined
+               setCaretOffset: (o) => {
+                       caretOffset = o;
+               },
+               setValue: (v) => {
+                       value = v;
+                       calls.push(`setValue:${v}`);
+               }
        });
 
        // Simulate the user typing: update the buffer and run the input flow.
index 78a431157c43254ae6d0c9c855790933af92e25c..7ec8bf7f8b6e5e4fca3f057c485816b1e0218393 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as Tooltip from '$lib/components/ui/tooltip';
        import ChatForm from '$lib/components/app/chat/ChatForm/ChatForm.svelte';
+       import * as Tooltip from '$lib/components/ui/tooltip';
 
        let { onSubmit }: { onSubmit?: () => void } = $props();
 
index 14d93897876ec61a6cb87c337935dd10e662a646..504f685973c9001dc5a53a460562d343a23418f6 100644 (file)
@@ -2,8 +2,8 @@
        // Mounts the real ChatMessages list against the real conversations store, so
        // the harness exercises `displayMessages` (which rebuilds every message's
        // toolMessages array) rather than a single message subtree.
-       import * as Tooltip from '$lib/components/ui/tooltip';
        import ChatMessages from '$lib/components/app/chat/ChatMessages/ChatMessages.svelte';
+       import * as Tooltip from '$lib/components/ui/tooltip';
        import { conversationsStore } from '$lib/stores/conversations.svelte';
 </script>
 
index 53d3b9cfd10e453312a57563a8e4e5812cc36436..b11ea0f100f071b2e921a6ac3dad6e049f6c862d 100644 (file)
@@ -7,7 +7,7 @@
                open: boolean;
        }
 
-       let { variant, open }: Props = $props();
+       let { open, variant }: Props = $props();
 </script>
 
 {#if variant === 'content'}
index fe2cc958bc408dd93e4655de8d18b944e6775159..7bbabbc82a31d0129e24e464c9217515df43d77b 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import { untrack } from 'svelte';
        import McpServerForm from '$lib/components/app/mcp/McpServerForm.svelte';
+       import { untrack } from 'svelte';
 
        interface Props {
                headers?: string;
index 1380ec851bd7fdef05a188641e5377d5a0e3700e..3c874276adfedca4ffddf33b38ab86e755701d3e 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
-       import * as Tooltip from '$lib/components/ui/tooltip';
        import Page from '../../../src/routes/(chat)/+page.svelte';
+       import * as Tooltip from '$lib/components/ui/tooltip';
 </script>
 
 <!--
index f0cc8c3b88f51c352b5cbbbcebc6ee56559d827d..abf989da27420462b1840ace6eb877c00cf45f0c 100644 (file)
@@ -11,7 +11,7 @@ export const perfState = $state<{
        toolMessages: DatabaseMessage[];
        isStreaming: boolean;
 }>({
+       isStreaming: true,
        message: null,
-       toolMessages: [],
-       isStreaming: true
+       toolMessages: []
 });
index 2a27be9b0cb744150ff241a807d3cf00f68a7ca0..de331f1314f780db013d83c7d0ca363b0d3a7d38 100644 (file)
@@ -1,21 +1,21 @@
-import { afterEach, describe, expect, it } from 'vitest';
-import { DatabaseService } from '$lib/services/database.service';
 import { MessageRole, MessageType } from '$lib/enums';
+import { DatabaseService } from '$lib/services/database.service';
 import type { ExportedConversation } from '$lib/types/database';
+import { afterEach, describe, expect, it } from 'vitest';
 
 function makeSession(id: string): ExportedConversation {
        return {
-               conv: { id, currNode: `${id}-msg`, lastModified: 0, name: `Chat ${id}` },
+               conv: { currNode: `${id}-msg`, id, lastModified: 0, name: `Chat ${id}` },
                messages: [
                        {
-                               id: `${id}-msg`,
-                               convId: id,
-                               type: MessageType.TEXT,
-                               timestamp: 0,
-                               role: MessageRole.USER,
+                               children: [],
                                content: `hello from ${id}`,
+                               convId: id,
+                               id: `${id}-msg`,
                                parent: null,
-                               children: []
+                               role: MessageRole.USER,
+                               timestamp: 0,
+                               type: MessageType.TEXT
                        }
                ]
        } as unknown as ExportedConversation;
@@ -23,6 +23,7 @@ function makeSession(id: string): ExportedConversation {
 
 afterEach(async () => {
        const conversations = await DatabaseService.getAllConversations();
+
        await DatabaseService.bulkDeleteConversations(conversations.map((conv) => conv.id));
 });
 
index d0b213b2c3ce5ffbb85909b7c0e49f90fe67cece..f17e08cf1b513e1c505d5f07f2d4d1b47ded74af 100644 (file)
@@ -1,8 +1,8 @@
-import { beforeEach, describe, expect, it } from 'vitest';
-import { render } from 'vitest-browser-svelte';
 import { McpServerForm } from '$lib/components/app/mcp';
 import { mcpStore } from '$lib/stores/mcp.svelte';
 import { settingsStore } from '$lib/stores/settings.svelte';
+import { beforeEach, describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 describe('mcp server display name', () => {
        beforeEach(() => {
@@ -11,44 +11,48 @@ describe('mcp server display name', () => {
 
        it('custom display name wins over the url fallback', () => {
                const server = mcpStore.addServer({
+                       displayName: 'My Tools',
                        enabled: false,
-                       url: 'https://mcp.example.com/a',
-                       displayName: 'My Tools'
+                       url: 'https://mcp.example.com/a'
                });
+
                expect(mcpStore.getServerLabel(server)).toBe('My Tools');
        });
 
        it('without a custom name the url is the label', () => {
                const server = mcpStore.addServer({ enabled: false, url: 'https://mcp.example.com/a' });
+
                expect(mcpStore.getServerLabel(server)).toBe('https://mcp.example.com/a');
        });
 
        it('identical labels get positional suffixes', () => {
                const a = mcpStore.addServer({
+                       displayName: 'GitHub',
                        enabled: false,
-                       url: 'https://mcp.example.com/a',
-                       displayName: 'GitHub'
+                       url: 'https://mcp.example.com/a'
                });
                const b = mcpStore.addServer({
+                       displayName: 'GitHub',
                        enabled: false,
-                       url: 'https://mcp.example.com/b',
-                       displayName: 'GitHub'
+                       url: 'https://mcp.example.com/b'
                });
+
                expect(mcpStore.getServerLabel(a)).toBe('GitHub (1)');
                expect(mcpStore.getServerLabel(b)).toBe('GitHub (2)');
        });
 
        it('renaming one twin dissolves the suffixes', () => {
                const a = mcpStore.addServer({
+                       displayName: 'GitHub',
                        enabled: false,
-                       url: 'https://mcp.example.com/a',
-                       displayName: 'GitHub'
+                       url: 'https://mcp.example.com/a'
                });
                const b = mcpStore.addServer({
+                       displayName: 'GitHub',
                        enabled: false,
-                       url: 'https://mcp.example.com/b',
-                       displayName: 'GitHub'
+                       url: 'https://mcp.example.com/b'
                });
+
                mcpStore.updateServer(b.id, { displayName: 'GitHub Work' });
                expect(mcpStore.getServerLabel(a)).toBe('GitHub');
                expect(mcpStore.getServerLabel(mcpStore.getServerById(b.id)!)).toBe('GitHub Work');
@@ -56,16 +60,17 @@ describe('mcp server display name', () => {
 
        it('the form exposes an editable display name field', async () => {
                let captured = '';
+
                const screen = await render(McpServerForm, {
-                       url: 'https://mcp.example.com/a',
                        headers: '',
                        name: '',
-                       onUrlChange: () => {},
                        onHeadersChange: () => {},
-                       onNameChange: (v: string) => (captured = v)
+                       onNameChange: (v: string) => (captured = v),
+                       onUrlChange: () => {},
+                       url: 'https://mcp.example.com/a'
                });
-
                const input = screen.getByLabelText('Display name');
+
                await expect.element(input).toBeVisible();
                await input.fill('My Custom Server');
                expect(captured).toBe('My Custom Server');
index b4bd892347f7baa352fee14f0fe38c4d50ef5032..99b7a1354a90d3b3ceac0ec5bf9517084bbb691e 100644 (file)
@@ -1,6 +1,6 @@
+import McpServerFormWrapper from './components/McpServerFormWrapper.svelte';
 import { describe, expect, it } from 'vitest';
 import { render } from 'vitest-browser-svelte';
-import McpServerFormWrapper from './components/McpServerFormWrapper.svelte';
 
 const AUTHORIZATION_HEADER = 'Authorization';
 const BEARER_PREFIX = 'Bearer ';
@@ -48,9 +48,11 @@ describe('McpServerForm - Authorization / bearer UI', () => {
                await screen.getByRole('switch', { name: /authorization/i }).click();
 
                const token = 'super-secret';
+
                await bearerInput(screen).fill(token);
 
                const expected = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${token}` });
+
                await expect
                        .element(capturedHeaders(screen))
                        .toHaveAttribute('data-captured-headers', expected);
@@ -58,10 +60,9 @@ describe('McpServerForm - Authorization / bearer UI', () => {
 
        it('pre-existing Bearer header pre-fills the bearer input with the token stripped', async () => {
                const existing = JSON.stringify({
-                       'X-Trace-Id': 'abc',
-                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}preexisting`
+                       [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}preexisting`,
+                       'X-Trace-Id': 'abc'
                });
-
                const screen = await render(McpServerFormWrapper, { headers: existing });
 
                await expect.element(bearerInput(screen)).toBeVisible();
@@ -70,24 +71,24 @@ describe('McpServerForm - Authorization / bearer UI', () => {
 
        it('non-Bearer Authorization is ignored by the dedicated UI and stays in the KV section', async () => {
                const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: 'Basic czNjcjpwYXNz' });
-
                const screen = await render(McpServerFormWrapper, { headers: existing });
 
                await expect.element(bearerInput(screen)).not.toBeInTheDocument();
 
                const headerKeyInput = screen.getByPlaceholder('Header name');
+
                await expect.element(headerKeyInput).toBeVisible();
        });
 
        it('engaging the token UI replaces a non-Bearer Authorization with the Bearer scheme', async () => {
                const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: 'Basic old' });
-
                const screen = await render(McpServerFormWrapper, { headers: existing });
 
                await screen.getByRole('switch', { name: /authorization/i }).click();
                await bearerInput(screen).fill('new');
 
                const expected = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}new` });
+
                await expect
                        .element(capturedHeaders(screen))
                        .toHaveAttribute('data-captured-headers', expected);
@@ -126,8 +127,8 @@ describe('McpServerForm - Authorization / bearer UI', () => {
        it('does not surface Bearer Authorization in the KV section even when pre-existing', async () => {
                const existing = JSON.stringify({ [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}xyz` });
                const screen = await render(McpServerFormWrapper, { headers: existing });
-
                const headerKeyInput = screen.getByPlaceholder('Header name');
+
                await expect.element(headerKeyInput).not.toBeInTheDocument();
        });
 });
index 32e333d7f5194487443b74a3b298493a21d57c37..37d7e08c4983ce65b52e909aa28ba382221eadb8 100644 (file)
@@ -1,6 +1,6 @@
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
 import TestWrapper from './components/TestWrapper.svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 describe('/+page.svelte', () => {
        it('should render page without throwing', async () => {
index 1ce01998514e95bdbc50268100d195fcdd1a3d43..469b626e60c3f2eba1baaabeaa9dd416f151275e 100644 (file)
@@ -3,19 +3,21 @@
 // scrollIntoView on the initial mount, before the popover was positioned,
 // so the browser scrolled every scrollable ancestor to reveal the row.
 
-import { describe, it, expect } from 'vitest';
-import { render } from 'vitest-browser-svelte';
-import { tick } from 'svelte';
 import PickerListScrollHarness from './components/PickerListScrollHarness.svelte';
+import { tick } from 'svelte';
+import { describe, expect, it } from 'vitest';
+import { render } from 'vitest-browser-svelte';
 
 describe('ChatFormPickerList mount scroll', () => {
        it('does not scroll documentElement when the picker mounts', async () => {
                const screen = render(PickerListScrollHarness);
+
                await tick();
 
                document.documentElement.scrollTop = document.documentElement.scrollHeight;
                await tick();
                const before = document.documentElement.scrollTop;
+
                expect(before).toBeGreaterThan(0);
 
                screen.component.openPicker();
@@ -24,6 +26,7 @@ describe('ChatFormPickerList mount scroll', () => {
                await tick();
 
                const after = document.documentElement.scrollTop;
+
                expect(after).toBe(before);
        });
 });
index 8f2e3df20a924719979be0734f073f01082173f3..7c0d7926f845bc68af7a1e93992f84f6218dd18f 100644 (file)
@@ -1,6 +1,6 @@
-import { beforeEach, describe, expect, it } from 'vitest';
-import { SandboxService } from '$lib/services/sandbox.service';
 import { SANDBOX_TOOL_NAME } from '$lib/constants';
+import { SandboxService } from '$lib/services/sandbox.service';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 const run = (code: string, timeoutMs?: number) =>
        SandboxService.executeTool(SANDBOX_TOOL_NAME, {
@@ -11,6 +11,7 @@ const run = (code: string, timeoutMs?: number) =>
 describe('sandbox service', () => {
        beforeEach(async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.config = {
                        ...settingsStore.config,
                        symbolicMathEnabled: true
@@ -19,18 +20,21 @@ describe('sandbox service', () => {
 
        it('executes plain JavaScript', async () => {
                const reply = await run('return 1 + 1;');
+
                expect(reply.isError).toBe(false);
                expect(reply.content).toContain('=> 2');
        });
 
        it('exposes nerdamer for symbolic computation', async () => {
                const reply = await run("return nerdamer.diff('sin(x)/x', 'x').toString();");
+
                expect(reply.isError).toBe(false);
                expect(reply.content).toContain('cos(x)');
        });
 
        it('computes exact rational arithmetic', async () => {
                const reply = await run("return nerdamer('1/3 + 1/6').toString();");
+
                expect(reply.isError).toBe(false);
                expect(reply.content).toContain('=> 1/2');
        });
@@ -39,6 +43,7 @@ describe('sandbox service', () => {
                const reply = await run(
                        "return nerdamer('expand((1+x*y)^3 - (1 + 3*x*y + 3*x^2*y^2 + x^3*y^3))').toString();"
                );
+
                expect(reply.isError).toBe(false);
                expect(reply.content).toContain('=> 0');
        });
@@ -47,12 +52,14 @@ describe('sandbox service', () => {
                const reply = await run(
                        "try { await fetch('https://example.com/'); return 'leaked'; } catch { return 'blocked'; }"
                );
+
                expect(reply.isError).toBe(false);
                expect(reply.content).toContain('=> blocked');
        });
 
        it('enforces the timeout on runaway code', async () => {
                const reply = await run('while (true) {}', 500);
+
                expect(reply.isError).toBe(true);
                expect(reply.content).toContain('timed out');
        });
index 601ebc2b3f41592222283425bfd577eeb9876d17..7e60ccfb9c9e8fffbc4690fc2c966245d9219edb 100644 (file)
@@ -4,9 +4,9 @@
 // winning when the legacy keys disagree. Legacy keys are removed from the
 // persisted config so they do not stay orphaned in localStorage.
 
-import { beforeEach, describe, expect, it } from 'vitest';
-import { settingsStore, config } from '$lib/stores/settings.svelte';
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 function seedConfig(stored: Record<string, unknown>) {
        localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(stored));
@@ -39,7 +39,7 @@ describe('renderContentAsRawText migration', () => {
        });
 
        it('lets any explicit raw-text preference win when the legacy keys disagree', () => {
-               seedConfig({ renderUserContentAsMarkdown: true, renderThinkingAsMarkdown: false });
+               seedConfig({ renderThinkingAsMarkdown: false, renderUserContentAsMarkdown: true });
                expect(config().renderContentAsRawText).toBe(true);
        });
 
@@ -53,6 +53,7 @@ describe('renderContentAsRawText migration', () => {
                expect(config().renderContentAsRawText).toBe(false);
 
                const stored = persisted();
+
                expect(stored.renderUserContentAsMarkdown).toBeUndefined();
                expect(stored.renderThinkingAsMarkdown).toBeUndefined();
                expect(stored.renderUserContentAsRawText).toBeUndefined();
index 74817234eb14e3fcc00dcda3ac7b7363f529c35b..6e169702cfe240e8c902f649617a0044ca29d15c 100644 (file)
@@ -1,10 +1,10 @@
-import { beforeEach, describe, expect, it } from 'vitest';
-import { settingsStore, config } from '$lib/stores/settings.svelte';
-import { serverStore } from '$lib/stores/server.svelte';
-import { ParameterSyncService } from '$lib/services/parameter-sync.service';
 import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-registry';
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { ParameterSyncService } from '$lib/services/parameter-sync.service';
+import { serverStore } from '$lib/stores/server.svelte';
+import { config, settingsStore } from '$lib/stores/settings.svelte';
 import type { SettingsConfigType } from '$lib/types';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 type Primitive = string | number | boolean;
 
@@ -14,13 +14,17 @@ const KEYS = Object.keys(SETTING_CONFIG_DEFAULT).filter(
 
 function divergent(key: string, base: Primitive): Primitive {
        if (typeof base === 'boolean') return !base;
+
        if (typeof base === 'number') return base + 7;
+
        return `user-${key}`;
 }
 
 function baselineFor(key: string, base: Primitive): Primitive {
        if (typeof base === 'boolean') return !base;
+
        if (typeof base === 'number') return base + 42;
+
        return `admin-${key}`;
 }
 
@@ -37,7 +41,6 @@ function mockProps(uiSettings: Record<string, Primitive>) {
 
 const setUser = (key: string, value: Primitive) =>
        settingsStore.updateConfig(key as keyof SettingsConfigType, value as never);
-
 const current = (key: string) => (config() as Record<string, unknown>)[key];
 
 describe('registry-wide invariants', () => {
@@ -48,6 +51,7 @@ describe('registry-wide invariants', () => {
        it('I1: no load ever modifies a stored user value, for any key of any type', () => {
                settingsStore.initialize();
                const userValues: Record<string, Primitive> = {};
+
                for (const key of KEYS) {
                        userValues[key] = divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
                        setUser(key, userValues[key]);
@@ -56,6 +60,7 @@ describe('registry-wide invariants', () => {
                // simulated F5 + adverse admin baseline on every key, synced twice
                settingsStore.initialize();
                const adverse: Record<string, Primitive> = {};
+
                for (const key of KEYS)
                        adverse[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
                mockProps(adverse);
@@ -70,6 +75,7 @@ describe('registry-wide invariants', () => {
        it('first visit: the baseline applies for every key, false and 0 included', () => {
                settingsStore.initialize();
                const baseline: Record<string, Primitive> = {};
+
                for (const key of KEYS)
                        baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
                mockProps(baseline);
@@ -78,6 +84,7 @@ describe('registry-wide invariants', () => {
 
                for (const key of KEYS) {
                        if (ParameterSyncService.canSyncParameter(key)) continue;
+
                        expect(current(key), key).toBe(baseline[key]);
                }
        });
@@ -87,6 +94,7 @@ describe('registry-wide invariants', () => {
                for (const key of KEYS) setUser(key, divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive));
 
                const baseline: Record<string, Primitive> = {};
+
                KEYS.filter((_, i) => i % 2 === 0).forEach((key) => {
                        baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive);
                });
index e2ba3c2dae78a190ded0e0c094421f85fe91aafc..bef2400df1685234a36e4275027d1d224719ab4b 100644 (file)
@@ -1,7 +1,7 @@
-import { beforeEach, describe, expect, it } from 'vitest';
-import { settingsStore, config } from '$lib/stores/settings.svelte';
-import { serverStore } from '$lib/stores/server.svelte';
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
+import { serverStore } from '$lib/stores/server.svelte';
+import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 function mockProps(uiSettings: Record<string, string | number | boolean>) {
        Object.defineProperty(serverStore, 'props', {
@@ -21,7 +21,7 @@ describe('server ui_settings application semantics', () => {
 
        it('applies the admin defaults once for a new user', () => {
                settingsStore.initialize();
-               mockProps({ theme: 'dark', apiKey: '' });
+               mockProps({ apiKey: '', theme: 'dark' });
 
                settingsStore.syncWithServerDefaults();
 
@@ -35,7 +35,7 @@ describe('server ui_settings application semantics', () => {
 
                // simulated F5: config now exists in localStorage
                settingsStore.initialize();
-               mockProps({ theme: 'dark', apiKey: '' });
+               mockProps({ apiKey: '', theme: 'dark' });
 
                settingsStore.syncWithServerDefaults();
                settingsStore.syncWithServerDefaults();
@@ -43,6 +43,7 @@ describe('server ui_settings application semantics', () => {
                expect(config().theme).toBe('light');
                expect(config().apiKey).toBe('sk-user-key');
                const stored = JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}');
+
                expect(stored.apiKey).toBe('sk-user-key');
        });
 
@@ -50,7 +51,7 @@ describe('server ui_settings application semantics', () => {
                settingsStore.initialize();
                settingsStore.updateConfig('theme', 'light');
                settingsStore.updateConfig('apiKey', 'sk-user-key');
-               mockProps({ theme: 'dark', apiKey: '' });
+               mockProps({ apiKey: '', theme: 'dark' });
 
                settingsStore.forceSyncWithServerDefaults();
 
index 54c215fef59bc05b4d0c76ab28f347301ccddb6e..65298b44b8908d5ed25f6e93d79b396ec8b8c7e1 100644 (file)
@@ -7,21 +7,21 @@
 // made per-token cost scale with conversation length (1.26ms at 1 prior message
 // -> 3.07ms at 40). Mutating in place keeps it flat.
 
-import { describe, it, expect } from 'vitest';
+import { MessageRole } from '$lib/enums';
 import { conversationsStore } from '$lib/stores/conversations.svelte';
 import type { DatabaseMessage } from '$lib/types';
-import { MessageRole } from '$lib/enums';
+import { describe, expect, it } from 'vitest';
 
 function makeMessage(id: string): DatabaseMessage {
        return {
-               id,
-               convId: 'c1',
-               type: 'text',
-               timestamp: 0,
-               role: MessageRole.ASSISTANT,
+               children: [],
                content: '',
+               convId: 'c1',
+               id,
                parent: null,
-               children: []
+               role: MessageRole.ASSISTANT,
+               timestamp: 0,
+               type: 'text'
        } as DatabaseMessage;
 }
 
index c20eec276175dc25a48b4bf4eb5e22968b620087..09442f42927fc097afbe82fc712750bc79a6a421 100644 (file)
@@ -13,6 +13,7 @@ test.describe('PWA Service Worker', () => {
                                        setTimeout(() => reject(new Error('Service worker registration failed: timeout')), 15000)
                                )
                        ]);
+
                        // @ts-expect-error registration is of type unknown
                        return registration.active?.scriptURL;
                });
@@ -30,6 +31,7 @@ test.describe('PWA Service Worker', () => {
 
                const swActive = await page.evaluate(async () => {
                        const reg = await navigator.serviceWorker.ready;
+
                        return reg.active?.scriptURL ?? null;
                });
 
@@ -67,6 +69,7 @@ test.describe('PWA Service Worker', () => {
                await offlinePage.goto('/');
 
                const bodyText = await offlinePage.locator('body').textContent();
+
                expect(bodyText).toBeTruthy();
 
                await context.close();
@@ -74,9 +77,11 @@ test.describe('PWA Service Worker', () => {
 
        test('version.json is accessible and contains version', async ({ page }) => {
                const versionResponse = await page.request.get('/_app/version.json');
+
                expect(versionResponse.ok()).toBeTruthy();
 
                const versionData = await versionResponse.json();
+
                expect(versionData).toHaveProperty('version');
                expect(typeof versionData.version).toBe('string');
                expect(versionData.version.length).toBeGreaterThan(0);
@@ -84,9 +89,11 @@ test.describe('PWA Service Worker', () => {
 
        test('manifest.webmanifest is accessible and valid', async ({ page }) => {
                const response = await page.request.get('/manifest.webmanifest');
+
                expect(response.ok()).toBeTruthy();
 
                const manifest = await response.json();
+
                expect(manifest).toHaveProperty('name', 'llama-ui');
                expect(manifest).toHaveProperty('short_name', 'llama-ui');
                expect(manifest).toHaveProperty('start_url', './');
@@ -97,6 +104,7 @@ test.describe('PWA Service Worker', () => {
 
        test('index.html contains content-hashed bundle references', async ({ page }) => {
                const response = await page.request.get('/');
+
                expect(response.ok()).toBeTruthy();
 
                const html = await response.text();
index e640176a8459b6b8667d167d51dc927d4ba3a758..023ba1ac4fc7e3b490120f8deec7ab333bf79a00 100644 (file)
@@ -3,86 +3,86 @@
        import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte';
 
        const { Story } = defineMeta({
-               title: 'Components/ChatScreen/ChatMessage',
                component: ChatMessage,
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/ChatScreen/ChatMessage'
        });
 
        // Mock messages for different scenarios
        const userMessage: DatabaseMessage = {
-               id: '1',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: Date.now() - 1000 * 60 * 5,
-               role: 'user',
+               children: [],
                content: 'What is the meaning of life, the universe, and everything?',
+               convId: 'conv-1',
+               id: '1',
                parent: '',
+               role: 'user',
                thinking: '',
-               children: []
+               timestamp: Date.now() - 1000 * 60 * 5,
+               type: 'message'
        };
 
        const assistantMessage: DatabaseMessage = {
-               id: '2',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: Date.now() - 1000 * 60 * 3,
-               role: 'assistant',
+               children: [],
                content:
                        'The answer to the ultimate question of life, the universe, and everything is **42**.\n\nThis comes from Douglas Adams\' "The Hitchhiker\'s Guide to the Galaxy," where a supercomputer named Deep Thought calculated this answer over 7.5 million years. However, the question itself was never properly formulated, which is why the answer seems meaningless without context.',
+               convId: 'conv-1',
+               id: '2',
                parent: '1',
+               role: 'assistant',
                thinking: '',
-               children: []
+               timestamp: Date.now() - 1000 * 60 * 3,
+               type: 'message'
        };
 
        const assistantWithReasoning: DatabaseMessage = {
-               id: '3',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: Date.now() - 1000 * 60 * 2,
-               role: 'assistant',
+               children: [],
                content: "Here's the concise answer, now that I've thought it through carefully for you.",
+               convId: 'conv-1',
+               id: '3',
                parent: '1',
+               role: 'assistant',
                thinking:
                        "Let's consider the user's question step by step:\\n\\n1. Identify the core problem\\n2. Evaluate relevant information\\n3. Formulate a clear answer\\n\\nFollowing this process ensures the final response stays focused and accurate.",
-               children: []
+               timestamp: Date.now() - 1000 * 60 * 2,
+               type: 'message'
        };
        const rawOutputMessage: DatabaseMessage = {
-               id: '6',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: Date.now() - 1000 * 60,
-               role: 'assistant',
+               children: [],
                content:
                        '<|channel|>analysis<|message|>User greeted me. Initiating overcomplicated analysis: Is this a trap? No, just a normal hello. Respond calmly, act like a helpful assistant, and do not start explaining quantum physics again. Confidence 0.73. Engaging socially acceptable greeting protocol...<|end|>Hello there! How can I help you today?',
+               convId: 'conv-1',
+               id: '6',
                parent: '1',
+               role: 'assistant',
                thinking: '',
-               children: []
+               timestamp: Date.now() - 1000 * 60,
+               type: 'message'
        };
 
        let processingMessage = $state({
-               id: '4',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: 0, // No timestamp = processing
-               role: 'assistant',
+               children: [],
                content: '',
+               convId: 'conv-1',
+               id: '4',
                parent: '1',
+               role: 'assistant',
                thinking: '',
-               children: []
+               timestamp: 0, // No timestamp = processing
+               type: 'message'
        });
 
        let streamingMessage = $state({
-               id: '5',
-               convId: 'conv-1',
-               type: 'message',
-               timestamp: 0, // No timestamp = streaming
-               role: 'assistant',
+               children: [],
                content: '',
+               convId: 'conv-1',
+               id: '5',
                parent: '1',
+               role: 'assistant',
                thinking: '',
-               children: []
+               timestamp: 0, // No timestamp = streaming
+               type: 'message'
        });
 </script>
 
@@ -93,6 +93,7 @@
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', true);
        }}
 />
        asChild
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', false);
                // Phase 1: Stream reasoning content in chunks
                let reasoningText =
                        'I need to think about this carefully. Let me break down the problem:\n\n1. The user is asking for help with something complex\n2. I should provide a thorough and helpful response\n3. I need to consider multiple approaches\n4. The best solution would be to explain step by step\n\nThis approach will ensure clarity and understanding.';
-
                let reasoningChunk = 'I';
                let i = 0;
+
                while (i < reasoningText.length) {
                        const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters
                        const chunk = reasoningText.slice(i, i + chunkSize);
+
                        reasoningChunk += chunk;
 
                        // Update the reactive state directly
                        "Based on my analysis, here's the solution:\n\n**Step 1:** First, we need to understand the requirements clearly.\n\n**Step 2:** Then we can implement the solution systematically.\n\n**Step 3:** Finally, we test and validate the results.\n\nThis approach ensures we cover all aspects of the problem effectively.";
 
                let contentChunk = '';
+
                i = 0;
 
                while (i < regularText.length) {
                        const chunkSize = Math.floor(Math.random() * 5) + 3; // Random 3-7 characters
                        const chunk = regularText.slice(i, i + chunkSize);
+
                        contentChunk += chunk;
 
                        // Update the reactive state directly
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
+
                settingsStore.updateConfig('showRawOutputSwitch', false);
                // Import the chat store to simulate loading state
                const { chatStore } = await import('$lib/stores/chat.svelte');
index 4c1734345955d7da3440d303614564c4c74af5af..4aeddd6be3cbf1fb16e9985da9eba4c910eb728c 100644 (file)
@@ -1,42 +1,42 @@
 <script module lang="ts">
+       import jpgAsset from './fixtures/assets/1.jpg?url';
+       import pdfAsset from './fixtures/assets/example.pdf?raw';
+       import svgAsset from './fixtures/assets/hf-logo.svg?url';
        import { defineMeta } from '@storybook/addon-svelte-csf';
        import ChatScreenForm from '$lib/components/app/chat/ChatScreen/ChatScreenForm.svelte';
        import { expect } from 'storybook/test';
-       import jpgAsset from './fixtures/assets/1.jpg?url';
-       import svgAsset from './fixtures/assets/hf-logo.svg?url';
-       import pdfAsset from './fixtures/assets/example.pdf?raw';
 
        const { Story } = defineMeta({
-               title: 'Components/ChatScreen/ChatScreenForm',
                component: ChatScreenForm,
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/ChatScreen/ChatScreenForm'
        });
 
        let fileAttachments = $state([
                {
+                       file: new File([''], '1.jpg', { type: 'image/jpeg' }),
                        id: '1',
                        name: '1.jpg',
-                       type: 'image/jpeg',
-                       size: 44891,
                        preview: jpgAsset,
-                       file: new File([''], '1.jpg', { type: 'image/jpeg' })
+                       size: 44891,
+                       type: 'image/jpeg'
                },
                {
+                       file: new File([''], 'hf-logo.svg', { type: 'image/svg+xml' }),
                        id: '2',
                        name: 'hf-logo.svg',
-                       type: 'image/svg+xml',
-                       size: 1234,
                        preview: svgAsset,
-                       file: new File([''], 'hf-logo.svg', { type: 'image/svg+xml' })
+                       size: 1234,
+                       type: 'image/svg+xml'
                },
                {
+                       file: new File([pdfAsset], 'example.pdf', { type: 'application/pdf' }),
                        id: '3',
                        name: 'example.pdf',
-                       type: 'application/pdf',
                        size: 351048,
-                       file: new File([pdfAsset], 'example.pdf', { type: 'application/pdf' })
+                       type: 'application/pdf'
                }
        ]);
 </script>
@@ -62,6 +62,7 @@
                await expect(textarea).toHaveValue(text);
 
                const fileInput = document.querySelector('input[type="file"]');
+
                await expect(fileInput).not.toHaveAttribute('accept');
        }}
 />
index 04f270a432e86be1a707553bf4bfe8018dfe246f..094573291f5c661219908d35a83395a29669aa27 100644 (file)
@@ -1,59 +1,60 @@
 <script module lang="ts">
-       import { defineMeta } from '@storybook/addon-svelte-csf';
-       import { expect } from 'storybook/test';
-       import { MarkdownContent } from '$lib/components/app';
        import { AI_TUTORIAL_MD } from './fixtures/ai-tutorial.js';
        import { API_DOCS_MD } from './fixtures/api-docs.js';
        import { BLOG_POST_MD } from './fixtures/blog-post.js';
        import { DATA_ANALYSIS_MD } from './fixtures/data-analysis.js';
-       import { README_MD } from './fixtures/readme.js';
-       import { MATH_FORMULAS_MD } from './fixtures/math-formulas.js';
        import { EMPTY_MD } from './fixtures/empty.js';
+       import { MATH_FORMULAS_MD } from './fixtures/math-formulas.js';
+       import { README_MD } from './fixtures/readme.js';
+       import { defineMeta } from '@storybook/addon-svelte-csf';
+       import { MarkdownContent } from '$lib/components/app';
+       import { expect } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/MarkdownContent',
                component: MarkdownContent,
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/MarkdownContent'
        });
 </script>
 
-<Story name="Empty" args={{ content: EMPTY_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }} />
+<Story name="Empty" args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: EMPTY_MD }} />
 
 <Story
        name="AI Tutorial"
-       args={{ content: AI_TUTORIAL_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: AI_TUTORIAL_MD }}
 />
 
 <Story
        name="API Documentation"
-       args={{ content: API_DOCS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: API_DOCS_MD }}
 />
 
 <Story
        name="Technical Blog"
-       args={{ content: BLOG_POST_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: BLOG_POST_MD }}
 />
 
 <Story
        name="Data Analysis"
-       args={{ content: DATA_ANALYSIS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: DATA_ANALYSIS_MD }}
 />
 
 <Story
        name="README file"
-       args={{ content: README_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: README_MD }}
 />
 
 <Story
        name="Math Formulas"
-       args={{ content: MATH_FORMULAS_MD, class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
+       args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]', content: MATH_FORMULAS_MD }}
 />
 
 <Story
        name="URL Links"
        args={{
+               class: 'max-w-[56rem] w-[calc(100vw-2rem)]',
                content: `# URL Links Test
 
 Here are some example URLs that should open in new tabs:
@@ -65,11 +66,11 @@ Here are some example URLs that should open in new tabs:
 
 You can also test inline links like https://example.com or https://docs.python.org.
 
-All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attributes for security.`,
-               class: 'max-w-[56rem] w-[calc(100vw-2rem)]'
+All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attributes for security.`
        }}
        play={async (context) => {
                const { canvasElement } = context;
+
                // Wait for component to render
                await new Promise((resolve) => setTimeout(resolve, 100));
 
@@ -97,22 +98,26 @@ All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attr
                const hugginFaceLink = linkList.find(
                        (link) => link.getAttribute('href') === 'https://huggingface.co'
                );
+
                expect(hugginFaceLink).toBeTruthy();
                expect(hugginFaceLink?.textContent).toBe('Hugging Face Homepage');
 
                const githubLink = linkList.find(
                        (link) => link.getAttribute('href') === 'https://github.com/ggml-org/llama.cpp'
                );
+
                expect(githubLink).toBeTruthy();
                expect(githubLink?.textContent).toBe('GitHub Repository');
 
                const openaiLink = linkList.find((link) => link.getAttribute('href') === 'https://openai.com');
+
                expect(openaiLink).toBeTruthy();
                expect(openaiLink?.textContent).toBe('OpenAI Website');
 
                const googleLink = linkList.find(
                        (link) => link.getAttribute('href') === 'https://www.google.com'
                );
+
                expect(googleLink).toBeTruthy();
                expect(googleLink?.textContent).toBe('Google Search');
 
@@ -120,11 +125,13 @@ All links should have \`target="_blank"\` and \`rel="noopener noreferrer"\` attr
                const exampleLink = linkList.find(
                        (link) => link.getAttribute('href') === 'https://example.com'
                );
+
                expect(exampleLink).toBeTruthy();
 
                const pythonDocsLink = linkList.find(
                        (link) => link.getAttribute('href') === 'https://docs.python.org'
                );
+
                expect(pythonDocsLink).toBeTruthy();
 
                console.log(`✅ URL Links test passed - Found ${links.length} links with proper attributes`);
index 3aba0dc78e452e4caf302efd30efe525b29bb864..d63300cb211d8cccebd509b9f70f272019a7b575 100644 (file)
@@ -3,39 +3,39 @@
        import ModelsSelectorList from '$lib/components/app/models/ModelsSelectorList.svelte';
        import ModelsSelectorOption from '$lib/components/app/models/ModelsSelectorOption.svelte';
        import type { GroupedModelOptions, ModelItem } from '$lib/components/app/models/utils';
-       import { modelsStore } from '$lib/stores/models.svelte';
        import { ServerModelStatus } from '$lib/enums';
+       import { modelsStore } from '$lib/stores/models.svelte';
 
        const { Story } = defineMeta({
-               title: 'Components/ModelsSelector',
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/ModelsSelector'
        });
 
        const mockModel = (id: string, name: string, orgName?: string, tags?: string[]): ModelOption => ({
+               capabilities: [],
                id,
-               name,
                model: orgName ? `${orgName}/${name}` : name,
-               capabilities: [],
+               name,
                parsedId: {
-                       raw: orgName ? `${orgName}/${name}` : name,
-                       orgName: orgName ?? null,
+                       activatedParams: null,
                        modelName: name,
+                       orgName: orgName ?? null,
                        params: null,
-                       activatedParams: null,
                        quantization: null,
+                       raw: orgName ? `${orgName}/${name}` : name,
                        tags: tags ?? []
                },
                tags
        });
 
        const mockRouterEntry = (modelName: string, status: ServerModelStatus): ApiModelDataEntry => ({
+               created: Date.now(),
                id: modelName,
+               in_cache: true,
                object: 'model',
                owned_by: 'llamacpp',
-               created: Date.now(),
-               in_cache: true,
                path: `/models/${modelName}`,
                status: { value: status }
        });
        mockModelsStore();
 
        const loadedModels: ModelItem[] = [
-               { option: mockModel('llama3.1-8b', 'Llama-3.1-8B-Instruct', 'meta'), flatIndex: 0 },
-               { option: mockModel('mistral-7b', 'Mistral-7B-v0.3', 'mistralai'), flatIndex: 1 }
+               { flatIndex: 0, option: mockModel('llama3.1-8b', 'Llama-3.1-8B-Instruct', 'meta') },
+               { flatIndex: 1, option: mockModel('mistral-7b', 'Mistral-7B-v0.3', 'mistralai') }
        ];
 
        const favoriteModels: ModelItem[] = [
-               { option: mockModel('qwen2.5-7b', 'Qwen2.5-7B-Instruct', 'Qwen'), flatIndex: 2 },
-               { option: mockModel('llama3.2-3b', 'Llama-3.2-3B-Instruct', 'meta'), flatIndex: 3 }
+               { flatIndex: 2, option: mockModel('qwen2.5-7b', 'Qwen2.5-7B-Instruct', 'Qwen') },
+               { flatIndex: 3, option: mockModel('llama3.2-3b', 'Llama-3.2-3B-Instruct', 'meta') }
        ];
 
        const availableModels: ModelItem[] = [
                {
-                       option: mockModel('deepseek-coder-6.7b', 'DeepSeek-Coder-6.7B', 'deepseek', ['coding']),
-                       flatIndex: 4
+                       flatIndex: 4,
+                       option: mockModel('deepseek-coder-6.7b', 'DeepSeek-Coder-6.7B', 'deepseek', ['coding'])
                },
-               { option: mockModel('gemma-2-9b', 'Gemma-2-9B-IT', 'google'), flatIndex: 5 },
-               { option: mockModel('phi-3-mini', 'Phi-3-mini-4k', 'microsoft'), flatIndex: 6 },
-               { option: mockModel('codellama-7b', 'CodeLlama-7B', 'codellama', ['coding']), flatIndex: 7 },
-               { option: mockModel('neural-chat-7b', 'Neural-Chat-7B-v3-3', 'intel'), flatIndex: 8 }
+               { flatIndex: 5, option: mockModel('gemma-2-9b', 'Gemma-2-9B-IT', 'google') },
+               { flatIndex: 6, option: mockModel('phi-3-mini', 'Phi-3-mini-4k', 'microsoft') },
+               { flatIndex: 7, option: mockModel('codellama-7b', 'CodeLlama-7B', 'codellama', ['coding']) },
+               { flatIndex: 8, option: mockModel('neural-chat-7b', 'Neural-Chat-7B-v3-3', 'intel') }
        ];
 
        const groupedOptions: GroupedModelOptions = {
-               loaded: loadedModels,
-               favorites: favoriteModels,
                available: [
                        {
-                               orgName: 'deepseek',
-                               items: [availableModels[0]]
+                               items: [availableModels[0]],
+                               orgName: 'deepseek'
                        },
                        {
-                               orgName: 'google',
-                               items: [availableModels[1]]
+                               items: [availableModels[1]],
+                               orgName: 'google'
                        },
                        {
-                               orgName: 'microsoft',
-                               items: [availableModels[2]]
+                               items: [availableModels[2]],
+                               orgName: 'microsoft'
                        },
                        {
-                               orgName: 'codellama',
-                               items: [availableModels[3]]
+                               items: [availableModels[3]],
+                               orgName: 'codellama'
                        },
                        {
-                               orgName: 'intel',
-                               items: [availableModels[4]]
+                               items: [availableModels[4]],
+                               orgName: 'intel'
                        }
-               ]
+               ],
+               favorites: favoriteModels,
+               loaded: loadedModels
        };
 
        function handleSelect(modelId: string) {
                const opt = [...loadedModels, ...favoriteModels, ...availableModels].find(
                        (m) => m.option.id === modelId
                );
+
                if (opt) {
                        selectedModel = opt.option.model;
                        activeId = modelId;
        <div class="w-80 rounded-lg border border-border bg-popover p-2 shadow-md">
                <ModelsSelectorList
                        groups={{
-                               loaded: [loadedModels[0]],
+                               available: [],
                                favorites: [],
-                               available: []
+                               loaded: [loadedModels[0]]
                        }}
                        currentModel={null}
                        activeId={null}
        <div class="w-80 rounded-lg border border-border bg-popover p-2 shadow-md">
                <ModelsSelectorList
                        groups={{
-                               loaded: [],
+                               available: [],
                                favorites: favoriteModels,
-                               available: []
+                               loaded: []
                        }}
                        currentModel={null}
                        activeId={null}
index 26aa57323264cce93163254e659c2d0cc5d31887..bb2d830a279a3baf013370362b87ed888b151427 100644 (file)
@@ -4,11 +4,11 @@
        import { expect } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/PwaRefreshAlert',
                component: PwaRefreshAlert,
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/PwaRefreshAlert'
        });
 </script>
 
        args={{ needRefresh: true, updateServiceWorker: () => console.log('reload') }}
        play={async ({ canvas }) => {
                const title = canvas.getByText('Update available');
+
                await expect(title).toBeInTheDocument();
 
                const description = canvas.getByText(/A new version is available/);
+
                await expect(description).toBeInTheDocument();
 
                const button = canvas.getByRole('button', { name: 'Reload' });
+
                await expect(button).toBeInTheDocument();
        }}
 />
@@ -32,6 +35,7 @@
        args={{ needRefresh: false, updateServiceWorker: () => console.log('reload') }}
        play={async ({ canvas }) => {
                const title = canvas.queryByText('Update available');
+
                await expect(title).not.toBeInTheDocument();
        }}
 />
        }}
        play={async ({ canvas, userEvent }) => {
                const button = canvas.getByRole('button', { name: 'Reload' });
+
                await expect(button).toBeInTheDocument();
 
                await userEvent.click(button);
 
                const title = canvas.queryByText('Update available');
+
                await expect(title).not.toBeInTheDocument();
 
                const reloadBtn = canvas.queryByRole('button', { name: 'Reload' });
+
                await expect(reloadBtn).not.toBeInTheDocument();
        }}
 />
index 7fbcef3ed5bd1c717e3cba4308f65a521a7efe1c..635992601200a799a301929c35311d4e94c8df16 100644 (file)
@@ -5,11 +5,11 @@
        import { screen } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/SidebarNavigation',
                component: SidebarNavigation,
                parameters: {
                        layout: 'centered'
-               }
+               },
+               title: 'Components/SidebarNavigation'
        });
 </script>
 
        // Mock conversations for the sidebar
        const mockConversations: DatabaseConversation[] = [
                {
+                       currNode: 'msg-1',
                        id: 'conv-1',
-                       name: 'Getting Started with AI',
                        lastModified: Date.now() - 1000 * 60 * 5, // 5 minutes ago
-                       currNode: 'msg-1'
+                       name: 'Getting Started with AI'
                },
                {
+                       currNode: 'msg-2',
                        id: 'conv-2',
-                       name: 'Python Programming Help',
                        lastModified: Date.now() - 1000 * 60 * 60 * 2, // 2 hours ago
-                       currNode: 'msg-2'
+                       name: 'Python Programming Help'
                },
                {
+                       currNode: 'msg-3',
                        id: 'conv-3',
-                       name: 'Creative Writing Ideas',
                        lastModified: Date.now() - 1000 * 60 * 60 * 24, // 1 day ago
-                       currNode: 'msg-3'
+                       name: 'Creative Writing Ideas'
                },
                {
+                       currNode: 'msg-4',
                        id: 'conv-4',
-                       name: 'This is a very long conversation title that should be truncated properly when displayed',
                        lastModified: Date.now() - 1000 * 60 * 60 * 24 * 3, // 3 days ago
-                       currNode: 'msg-4'
+                       name: 'This is a very long conversation title that should be truncated properly when displayed'
                },
                {
+                       currNode: 'msg-5',
                        id: 'conv-5',
-                       name: 'Math Problem Solving',
                        lastModified: Date.now() - 1000 * 60 * 60 * 24 * 7, // 1 week ago
-                       currNode: 'msg-5'
+                       name: 'Math Problem Solving'
                }
        ];
 </script>
 
                // Expand sidebar first, then click Search in the expanded button list
                const logoTrigger = screen.getByRole('button', { name: /expand navigation/i });
+
                await userEvent.click(logoTrigger);
                const searchTrigger = screen.getByText('Search');
+
                userEvent.click(searchTrigger);
        }}
 >
@@ -97,6 +99,7 @@
        play={async () => {
                // Mock empty conversations store
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.conversations = [];
        }}
 >
index 20f5e057b0c7eba1de28b79e3694efffd18fba49..5ac1f7db86b466cce4d3576b32869f5b1047a45d 100644 (file)
@@ -1,16 +1,16 @@
 <script module lang="ts">
-       import { defineMeta } from '@storybook/addon-svelte-csf';
        import { Copy } from '@lucide/svelte';
+       import { defineMeta } from '@storybook/addon-svelte-csf';
        import ActionIcon from '$lib/components/app/actions/ActionIcon.svelte';
        import { expect } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/ActionIcon/Accessibility',
                component: ActionIcon,
                parameters: {
                        layout: 'centered'
                },
-               tags: ['!dev']
+               tags: ['!dev'],
+               title: 'Components/ActionIcon/Accessibility'
        });
 </script>
 
index 4aaf60cd656027c86d5d3e12a04af42561dfe44c..c757da51f537dcc3da59c071359434a844a59e56 100644 (file)
@@ -4,30 +4,30 @@
        import { expect } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/ChatMessageStatistics/Accessibility',
                component: ChatMessageStatistics,
                parameters: {
                        layout: 'centered'
                },
-               tags: ['!dev']
+               tags: ['!dev'],
+               title: 'Components/ChatMessageStatistics/Accessibility'
        });
 </script>
 
 <Story
        name="ViewButtonsSingleTabStop"
        args={{
-               promptTokens: 100,
-               promptMs: 500,
-               predictedTokens: 200,
-               predictedMs: 1000,
                agenticTimings: {
-                       turns: 1,
+                       llm: { predicted_ms: 1000, predicted_n: 200, prompt_ms: 500, prompt_n: 100 },
                        toolCallsCount: 1,
                        toolsMs: 500,
-                       llm: { predicted_n: 200, predicted_ms: 1000, prompt_n: 100, prompt_ms: 500 }
+                       turns: 1
                },
                hideSummary: false,
-               isLive: false
+               isLive: false,
+               predictedMs: 1000,
+               predictedTokens: 200,
+               promptMs: 500,
+               promptTokens: 100
        }}
        play={async ({ canvas, userEvent }) => {
                const reading = await canvas.findByRole('button', { name: 'Reading' });
index 46f852c06641cbb7f63c1079f5c3ecbb2257767c..a162f78d47a459d5cfcf4fcae02041652100a21a 100644 (file)
@@ -1,16 +1,16 @@
 <script module lang="ts">
        import { defineMeta } from '@storybook/addon-svelte-csf';
        import ChatScreenForm from '$lib/components/app/chat/ChatScreen/ChatScreenForm.svelte';
-       import { expect, screen, waitFor } from 'storybook/test';
        import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
+       import { expect, screen, waitFor } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/ChatScreen/ChatScreenForm/Accessibility',
                component: ChatScreenForm,
                parameters: {
                        layout: 'centered'
                },
-               tags: ['!dev']
+               tags: ['!dev'],
+               title: 'Components/ChatScreen/ChatScreenForm/Accessibility'
        });
 </script>
 
@@ -19,6 +19,7 @@
        args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }}
        play={async ({ canvas, userEvent }) => {
                const textarea = await canvas.findByRole('textbox');
+
                await userEvent.clear(textarea);
                await userEvent.type(textarea, 'What is the meaning of life?');
 
index 937d7ab109497cc18cab3cd902e762cbb0bf7673..ef5abeafaaa1890fca40480cf61ee7971db0028c 100644 (file)
@@ -4,12 +4,12 @@
        import { expect, waitFor } from 'storybook/test';
 
        const { Story } = defineMeta({
-               title: 'Components/HorizontalScrollCarousel/Accessibility',
                component: HorizontalScrollCarousel,
                parameters: {
                        layout: 'centered'
                },
-               tags: ['!dev']
+               tags: ['!dev'],
+               title: 'Components/HorizontalScrollCarousel/Accessibility'
        });
 </script>
 
index 1fc42608f720cb756143044e4e74d42cd0d527b8..2a80ec5305d409c7d356601bd03febbb5a4b8959 100644 (file)
@@ -4,20 +4,20 @@
        import { expect } from 'storybook/test';
 
        const mockForkedConversation: DatabaseConversation = {
+               currNode: 'msg-2',
+               forkedFromConversationId: 'conv-1',
                id: 'conv-2',
-               name: 'Forked Conversation',
                lastModified: Date.now(),
-               currNode: 'msg-2',
-               forkedFromConversationId: 'conv-1'
+               name: 'Forked Conversation'
        };
 
        const { Story } = defineMeta({
-               title: 'Components/SidebarNavigationConversationItem/Accessibility',
                component: SidebarNavigationConversationItem,
                parameters: {
                        layout: 'centered'
                },
-               tags: ['!dev']
+               tags: ['!dev'],
+               title: 'Components/SidebarNavigationConversationItem/Accessibility'
        });
 </script>
 
index ecfcdf09ddd45dff248419492358c10980833e75..736674690409c58723842643b7f72287525077f8 100644 (file)
@@ -1,5 +1,5 @@
-import { serverStore } from '$lib/stores/server.svelte';
 import { modelsStore } from '$lib/stores/models.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
 
 /**
  * Mock server properties for Storybook testing
@@ -8,16 +8,17 @@ import { modelsStore } from '$lib/stores/models.svelte';
 export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
        // Reset any pointer-events from previous tests (dropdown cleanup)
        const body = document.querySelector('body');
+
        if (body) body.style.pointerEvents = '';
 
        // Directly set the props for testing purposes
        (serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
-               model_path: props.model_path || 'test-model',
                modalities: {
-                       vision: props.modalities?.vision ?? false,
                        audio: props.modalities?.audio ?? false,
-                       video: props.modalities?.video ?? false
+                       video: props.modalities?.video ?? false,
+                       vision: props.modalities?.vision ?? false
                },
+               model_path: props.model_path || 'test-model',
                ...props
        } as ApiLlamaCppServerProps;
 
@@ -41,8 +42,8 @@ export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
        (modelsStore as any).models = [
                {
                        id: 'test-model',
-                       name: 'Test Model',
-                       model: 'test-model'
+                       model: 'test-model',
+                       name: 'Test Model'
                }
        ];
 
@@ -56,12 +57,12 @@ export function mockServerProps(props: Partial<ApiLlamaCppServerProps>): void {
  */
 export function resetServerStore(): void {
        (serverStore as unknown as { props: ApiLlamaCppServerProps }).props = {
-               model_path: '',
                modalities: {
-                       vision: false,
                        audio: false,
-                       video: false
-               }
+                       video: false,
+                       vision: false
+               },
+               model_path: ''
        } as ApiLlamaCppServerProps;
        (serverStore as unknown as { error: string }).error = '';
        (serverStore as unknown as { loading: boolean }).loading = false;
@@ -71,16 +72,16 @@ export function resetServerStore(): void {
  * Common mock configurations for Storybook stories
  */
 export const mockConfigs = {
-       visionOnly: {
-               modalities: { vision: true, audio: false }
-       },
        audioOnly: {
-               modalities: { vision: false, audio: true }
+               modalities: { audio: true, vision: false }
        },
        bothModalities: {
-               modalities: { vision: true, audio: true }
+               modalities: { audio: true, vision: true }
        },
        noModalities: {
-               modalities: { vision: false, audio: false, video: false }
+               modalities: { audio: false, video: false, vision: false }
+       },
+       visionOnly: {
+               modalities: { audio: false, vision: true }
        }
 } as const;
index 306e71ca15d7c7aa33051a3305806f3a87931250..b8b41667d2c8c8a06af1dfae94b1e8abaf514009 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { isAbortError } from '$lib/utils/abort';
+import { describe, expect, it } from 'vitest';
 
 describe('isAbortError', () => {
        it('returns false for null, undefined and non-error values', () => {
@@ -12,11 +12,13 @@ describe('isAbortError', () => {
 
        it('returns true for DOMException with AbortError name', () => {
                const err = new DOMException('Operation was aborted', 'AbortError');
+
                expect(isAbortError(err)).toBe(true);
        });
 
        it('returns true for plain Error with AbortError name', () => {
                const err = new Error('aborted');
+
                err.name = 'AbortError';
                expect(isAbortError(err)).toBe(true);
        });
index 51b99c31f6dfa995b183eaa5148dd8781fdacbfd..8bdaa095a89db66068ccacb3733576e7e08abaef 100644 (file)
@@ -6,57 +6,52 @@
 //
 // Run: npx vitest bench --project=unit tests/unit/agentic-hotpath.bench.ts
 
-import { bench, describe } from 'vitest';
-
+import { classifyToolResult, parseToolResultWithImages } from '$lib/utils/agentic';
+import { detectIncompleteCodeBlock, highlightCode } from '$lib/utils/code';
+import { computeLineDiff } from '$lib/utils/compute-line-diff';
+import { preprocessLaTeX } from '$lib/utils/latex-protection';
+import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
+import { extractSearchQuery, extractSearchResults } from '$lib/utils/search-results';
+import { all as lowlightAll } from 'lowlight';
+import rehypeHighlight from 'rehype-highlight';
+import rehypeKatex from 'rehype-katex';
+import rehypeStringify from 'rehype-stringify';
 import { remark } from 'remark';
 import remarkBreaks from 'remark-breaks';
 import remarkGfm from 'remark-gfm';
 import remarkMath from 'remark-math';
 import remarkRehype from 'remark-rehype';
-import rehypeKatex from 'rehype-katex';
-import rehypeHighlight from 'rehype-highlight';
-import rehypeStringify from 'rehype-stringify';
-import { all as lowlightAll } from 'lowlight';
-
-import { computeLineDiff } from '$lib/utils/compute-line-diff';
-import { extractSearchResults, extractSearchQuery } from '$lib/utils/search-results';
-import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
-import { highlightCode, detectIncompleteCodeBlock } from '$lib/utils/code';
-import { classifyToolResult, parseToolResultWithImages } from '$lib/utils/agentic';
-import { preprocessLaTeX } from '$lib/utils/latex-protection';
+import { bench, describe } from 'vitest';
 
 // --- fixtures -------------------------------------------------------------
 
 function lines(n: number, seed: string): string {
        const out: string[] = [];
+
        for (let i = 0; i < n; i++) out.push(`${seed} line ${i} const value_${i} = compute(${i});`);
+
        return out.join('\n');
 }
 
 const SHELL_OUTPUT_1KB = lines(12, 'out');
 const SHELL_OUTPUT_200KB = lines(2600, 'out');
 const SHELL_OUTPUT_2MB = lines(26000, 'out');
-
 // Realistic exec_shell_command result: the exit-code marker is the final line,
 // which is what the un-anchored EXIT_CODE regex has to scan the whole blob for.
 const SHELL_2MB_WITH_EXIT = `${SHELL_OUTPUT_2MB}\n[exit code: 0]`;
-
 const EDIT_OLD_400 = lines(400, 'old');
 const EDIT_NEW_400 = lines(400, 'new');
 const EDIT_OLD_50 = lines(50, 'old');
 const EDIT_NEW_50 = lines(50, 'new');
-
 const WRITE_FILE_ARGS = JSON.stringify({
-       path: '/src/lib/thing.ts',
-       content: lines(1500, 'src')
+       content: lines(1500, 'src'),
+       path: '/src/lib/thing.ts'
 });
-
 const MARKDOWN_50KB = Array.from(
        { length: 400 },
        (_, i) =>
                `## Section ${i}\n\nSome **bold** prose with a [link](https://example.com) and \`inline\` code.\n\n- bullet one\n- bullet two\n\n\`\`\`ts\nconst x${i} = ${i};\n\`\`\`\n`
 ).join('\n');
-
 const CODE_BLOCK_5KB = lines(60, 'code');
 
 // --- A: computeLineDiff (O(m*n) LCS, allocates a full matrix) --------------
@@ -79,7 +74,9 @@ describe('computeLineDiff', () => {
 function buildProcessor() {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let proc: any = remark().use(remarkGfm);
+
        proc = proc.use(remarkMath).use(remarkBreaks).use(remarkRehype).use(rehypeKatex);
+
        return proc.use(rehypeHighlight, { languages: lowlightAll }).use(rehypeStringify, {
                allowDangerousHtml: true
        });
@@ -109,7 +106,6 @@ describe('markdown parse scaling (whole-string reparse per frame)', () => {
                        { length: Math.ceil((kb * 1024) / 64) },
                        (_, i) => `The quick brown fox jumps over the lazy dog. Sentence ${i}.`
                ).join(' ');
-
        const MD_3KB = prose(3);
        const MD_11KB = prose(11);
        const MD_26KB = prose(26);
@@ -139,7 +135,6 @@ describe('other whole-string passes per frame', () => {
                        { length: Math.ceil((kb * 1024) / 64) },
                        (_, i) => `The quick brown fox jumps over the lazy dog. Sentence ${i}.`
                ).join(' ');
-
        const MD_3KB = prose(3);
        const MD_11KB = prose(11);
        const MD_26KB = prose(26);
index 0efe5968ce83f1dce01ac42f12f4ab4c478a47ad..4096a17107093c7c76a12e2b5119f25321daf607 100644 (file)
@@ -1,34 +1,34 @@
-import { describe, it, expect } from 'vitest';
-import { deriveAgenticSections, hasAgenticContent } from '$lib/utils/agentic';
 import { AgenticSectionType, MessageRole } from '$lib/enums';
-import type { DatabaseMessage } from '$lib/types/database';
 import type { ApiChatCompletionToolCall } from '$lib/types/api';
+import type { DatabaseMessage } from '$lib/types/database';
+import { deriveAgenticSections, hasAgenticContent } from '$lib/utils/agentic';
+import { describe, expect, it } from 'vitest';
 
 function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
        return {
-               id: overrides.id ?? 'ast-1',
-               convId: 'conv-1',
-               type: 'text',
-               timestamp: Date.now(),
-               role: MessageRole.ASSISTANT,
+               children: [],
                content: overrides.content ?? '',
+               convId: 'conv-1',
+               id: overrides.id ?? 'ast-1',
                parent: null,
-               children: [],
+               role: MessageRole.ASSISTANT,
+               timestamp: Date.now(),
+               type: 'text',
                ...overrides
        } as DatabaseMessage;
 }
 
 function makeToolMsg(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
        return {
-               id: overrides.id ?? 'tool-1',
-               convId: 'conv-1',
-               type: 'text',
-               timestamp: Date.now(),
-               role: MessageRole.TOOL,
+               children: [],
                content: overrides.content ?? 'tool result',
+               convId: 'conv-1',
+               id: overrides.id ?? 'tool-1',
                parent: null,
-               children: [],
+               role: MessageRole.TOOL,
+               timestamp: Date.now(),
                toolCallId: overrides.toolCallId ?? 'call_1',
+               type: 'text',
                ...overrides
        } as DatabaseMessage;
 }
@@ -37,12 +37,14 @@ describe('deriveAgenticSections', () => {
        it('returns empty array for assistant with no content', () => {
                const msg = makeAssistant({ content: '' });
                const sections = deriveAgenticSections(msg);
+
                expect(sections).toEqual([]);
        });
 
        it('returns text section for simple assistant message', () => {
                const msg = makeAssistant({ content: 'Hello world' });
                const sections = deriveAgenticSections(msg);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe(AgenticSectionType.TEXT);
                expect(sections[0].content).toBe('Hello world');
@@ -54,6 +56,7 @@ describe('deriveAgenticSections', () => {
                        reasoningContent: 'Let me think...'
                });
                const sections = deriveAgenticSections(msg);
+
                expect(sections).toHaveLength(2);
                expect(sections[0].type).toBe(AgenticSectionType.REASONING);
                expect(sections[0].content).toBe('Let me think...');
@@ -65,17 +68,18 @@ describe('deriveAgenticSections', () => {
                        content: 'Let me check.',
                        toolCalls: JSON.stringify([
                                {
+                                       function: { arguments: '{"q":"test"}', name: 'search' },
                                        id: 'call_1',
-                                       type: 'function',
-                                       function: { name: 'search', arguments: '{"q":"test"}' }
+                                       type: 'function'
                                }
                        ])
                });
                const toolResult = makeToolMsg({
-                       toolCallId: 'call_1',
-                       content: 'Found 3 results'
+                       content: 'Found 3 results',
+                       toolCallId: 'call_1'
                });
                const sections = deriveAgenticSections(msg, [toolResult]);
+
                expect(sections).toHaveLength(2);
                expect(sections[0].type).toBe(AgenticSectionType.TEXT);
                expect(sections[1].type).toBe(AgenticSectionType.TOOL_CALL);
@@ -86,10 +90,11 @@ describe('deriveAgenticSections', () => {
        it('single turn: pending tool call without result', () => {
                const msg = makeAssistant({
                        toolCalls: JSON.stringify([
-                               { id: 'call_1', type: 'function', function: { name: 'bash', arguments: '{}' } }
+                               { function: { arguments: '{}', name: 'bash' }, id: 'call_1', type: 'function' }
                        ])
                });
                const sections = deriveAgenticSections(msg, [], [], true);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL_PENDING);
                expect(sections[0].toolName).toBe('bash');
@@ -107,10 +112,11 @@ describe('deriveAgenticSections', () => {
                const partialArgs = '{"path":"/Users/fifa2026.html","content":"<!DOCTYPE h';
                const msg = makeAssistant({
                        toolCalls: JSON.stringify([
-                               { id: 'call_1', type: 'function', function: { name: 'write_file', arguments: partialArgs } }
+                               { function: { arguments: partialArgs, name: 'write_file' }, id: 'call_1', type: 'function' }
                        ])
                });
                const sections = deriveAgenticSections(msg, [], [], true);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL_PENDING);
                expect(sections[0].type).not.toBe(AgenticSectionType.TOOL_CALL_STREAMING);
@@ -120,24 +126,24 @@ describe('deriveAgenticSections', () => {
 
        it('multi-turn: two assistant turns grouped as one session', () => {
                const assistant1 = makeAssistant({
-                       id: 'ast-1',
                        content: 'Turn 1 text',
+                       id: 'ast-1',
                        toolCalls: JSON.stringify([
                                {
+                                       function: { arguments: '{"q":"foo"}', name: 'search' },
                                        id: 'call_1',
-                                       type: 'function',
-                                       function: { name: 'search', arguments: '{"q":"foo"}' }
+                                       type: 'function'
                                }
                        ])
                });
-               const tool1 = makeToolMsg({ id: 'tool-1', toolCallId: 'call_1', content: 'result 1' });
+               const tool1 = makeToolMsg({ content: 'result 1', id: 'tool-1', toolCallId: 'call_1' });
                const assistant2 = makeAssistant({
-                       id: 'ast-2',
-                       content: 'Final answer based on results.'
+                       content: 'Final answer based on results.',
+                       id: 'ast-2'
                });
-
                // toolMessages contains both tool result and continuation assistant
                const sections = deriveAgenticSections(assistant1, [tool1, assistant2]);
+
                expect(sections).toHaveLength(3);
                // Turn 1
                expect(sections[0].type).toBe(AgenticSectionType.TEXT);
@@ -152,40 +158,40 @@ describe('deriveAgenticSections', () => {
 
        it('multi-turn: three turns with tool calls', () => {
                const assistant1 = makeAssistant({
-                       id: 'ast-1',
                        content: '',
+                       id: 'ast-1',
                        toolCalls: JSON.stringify([
                                {
+                                       function: { arguments: '{}', name: 'list_files' },
                                        id: 'call_1',
-                                       type: 'function',
-                                       function: { name: 'list_files', arguments: '{}' }
+                                       type: 'function'
                                }
                        ])
                });
-               const tool1 = makeToolMsg({ id: 'tool-1', toolCallId: 'call_1', content: 'file1 file2' });
+               const tool1 = makeToolMsg({ content: 'file1 file2', id: 'tool-1', toolCallId: 'call_1' });
                const assistant2 = makeAssistant({
-                       id: 'ast-2',
                        content: 'Reading file1...',
+                       id: 'ast-2',
                        toolCalls: JSON.stringify([
                                {
+                                       function: { arguments: '{"path":"file1"}', name: 'read_file' },
                                        id: 'call_2',
-                                       type: 'function',
-                                       function: { name: 'read_file', arguments: '{"path":"file1"}' }
+                                       type: 'function'
                                }
                        ])
                });
                const tool2 = makeToolMsg({
+                       content: 'contents of file1',
                        id: 'tool-2',
-                       toolCallId: 'call_2',
-                       content: 'contents of file1'
+                       toolCallId: 'call_2'
                });
                const assistant3 = makeAssistant({
-                       id: 'ast-3',
                        content: 'Here is the analysis.',
+                       id: 'ast-3',
                        reasoningContent: 'The file contains...'
                });
-
                const sections = deriveAgenticSections(assistant1, [tool1, assistant2, tool2, assistant3]);
+
                // Turn 1: tool_call (no text since content is empty)
                // Turn 2: text + tool_call
                // Turn 3: reasoning + text
@@ -206,6 +212,7 @@ describe('deriveAgenticSections', () => {
                        reasoningContent: 'Let me think about this...'
                });
                const sections = deriveAgenticSections(msg, [], [], true);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe(AgenticSectionType.REASONING_PENDING);
                expect(sections[0].content).toBe('Let me think about this...');
@@ -217,6 +224,7 @@ describe('deriveAgenticSections', () => {
                        reasoningContent: 'Let me think...'
                });
                const sections = deriveAgenticSections(msg, [], [], true);
+
                expect(sections).toHaveLength(2);
                expect(sections[0].type).toBe(AgenticSectionType.REASONING);
                expect(sections[1].type).toBe(AgenticSectionType.TEXT);
@@ -227,6 +235,7 @@ describe('deriveAgenticSections', () => {
                        reasoningContent: 'Let me think...'
                });
                const sections = deriveAgenticSections(msg, [], [], false);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe(AgenticSectionType.REASONING);
        });
@@ -234,17 +243,16 @@ describe('deriveAgenticSections', () => {
        it('multi-turn: streaming tool calls on last turn', () => {
                const assistant1 = makeAssistant({
                        toolCalls: JSON.stringify([
-                               { id: 'call_1', type: 'function', function: { name: 'search', arguments: '{}' } }
+                               { function: { arguments: '{}', name: 'search' }, id: 'call_1', type: 'function' }
                        ])
                });
-               const tool1 = makeToolMsg({ toolCallId: 'call_1', content: 'result' });
-               const assistant2 = makeAssistant({ id: 'ast-2', content: '' });
-
+               const tool1 = makeToolMsg({ content: 'result', toolCallId: 'call_1' });
+               const assistant2 = makeAssistant({ content: '', id: 'ast-2' });
                const streamingToolCalls: ApiChatCompletionToolCall[] = [
-                       { id: 'call_2', type: 'function', function: { name: 'write_file', arguments: '{"pa' } }
+                       { function: { arguments: '{"pa', name: 'write_file' }, id: 'call_2', type: 'function' }
                ];
-
                const sections = deriveAgenticSections(assistant1, [tool1, assistant2], streamingToolCalls);
+
                // Turn 1: tool_call
                // Turn 2 (streaming): streaming tool call
                expect(sections.some((s) => s.type === AgenticSectionType.TOOL_CALL)).toBe(true);
@@ -255,26 +263,30 @@ describe('deriveAgenticSections', () => {
 describe('hasAgenticContent', () => {
        it('returns false for plain assistant', () => {
                const msg = makeAssistant({ content: 'Just text' });
+
                expect(hasAgenticContent(msg)).toBe(false);
        });
 
        it('returns true when message has toolCalls', () => {
                const msg = makeAssistant({
                        toolCalls: JSON.stringify([
-                               { id: 'call_1', type: 'function', function: { name: 'test', arguments: '{}' } }
+                               { function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
                        ])
                });
+
                expect(hasAgenticContent(msg)).toBe(true);
        });
 
        it('returns true when toolMessages are provided', () => {
                const msg = makeAssistant();
                const tool = makeToolMsg();
+
                expect(hasAgenticContent(msg, [tool])).toBe(true);
        });
 
        it('returns false for empty toolCalls JSON', () => {
                const msg = makeAssistant({ toolCalls: '[]' });
+
                expect(hasAgenticContent(msg)).toBe(false);
        });
 });
index 86867f8a9ddcd7e6234a643243ac3702e8669cfd..f4c2e8a4761b53af71bb864bc9168fa8937e6c5f 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
 import { LEGACY_AGENTIC_REGEX } from '$lib/constants/agentic';
+import { describe, expect, it } from 'vitest';
 
 /**
  * Tests for legacy marker stripping (used in migration).
@@ -25,7 +25,6 @@ const COMPLETE_BLOCK =
        '<<<TOOL_ARGS_END>>>\n' +
        'file1.txt\nfile2.txt\n' +
        '<<<AGENTIC_TOOL_CALL_END>>>\n';
-
 // Partial block: streaming was cut before END arrived.
 const OPEN_BLOCK =
        '\n\n<<<AGENTIC_TOOL_CALL_START>>>\n' +
@@ -39,6 +38,7 @@ describe('legacy agentic marker stripping (for migration)', () => {
        it('strips a complete tool call block, leaving surrounding text', () => {
                const input = 'Before.' + COMPLETE_BLOCK + 'After.';
                const result = stripLegacyContextMarkers(input);
+
                expect(result).not.toContain('<<<');
                expect(result).toContain('Before.');
                expect(result).toContain('After.');
@@ -47,6 +47,7 @@ describe('legacy agentic marker stripping (for migration)', () => {
        it('strips multiple complete tool call blocks', () => {
                const input = 'A' + COMPLETE_BLOCK + 'B' + COMPLETE_BLOCK + 'C';
                const result = stripLegacyContextMarkers(input);
+
                expect(result).not.toContain('<<<');
                expect(result).toContain('A');
                expect(result).toContain('B');
@@ -56,17 +57,20 @@ describe('legacy agentic marker stripping (for migration)', () => {
        it('strips an open/partial tool call block (no END marker)', () => {
                const input = 'Lead text.' + OPEN_BLOCK;
                const result = stripLegacyContextMarkers(input);
+
                expect(result).toBe('Lead text.');
                expect(result).not.toContain('<<<');
        });
 
        it('does not alter content with no markers', () => {
                const input = 'Just a normal assistant response.';
+
                expect(stripLegacyContextMarkers(input)).toBe(input);
        });
 
        it('strips reasoning block independently', () => {
                const input = '<<<reasoning_content_start>>>think hard<<<reasoning_content_end>>>Answer.';
+
                expect(stripLegacyContextMarkers(input)).toBe('Answer.');
        });
 
@@ -75,6 +79,7 @@ describe('legacy agentic marker stripping (for migration)', () => {
                        '<<<reasoning_content_start>>>plan<<<reasoning_content_end>>>' +
                        'Some text.' +
                        COMPLETE_BLOCK;
+
                expect(stripLegacyContextMarkers(input)).not.toContain('<<<');
                expect(stripLegacyContextMarkers(input)).toContain('Some text.');
        });
index 48a75be5102ca79b581dcdb48d9fefe3cac4573a..ec75a0e492961ed9f80940ff16ed05b0eced5c07 100644 (file)
@@ -1,7 +1,7 @@
-import { describe, expect, it } from 'vitest';
-import { AgenticSectionType } from '$lib/enums';
 import { REASONING_TAGS } from '$lib/constants';
-import { buildAssistantRawOutput, type AgenticSection } from '$lib/utils/agentic';
+import { AgenticSectionType } from '$lib/enums';
+import { type AgenticSection, buildAssistantRawOutput } from '$lib/utils/agentic';
+import { describe, expect, it } from 'vitest';
 
 function makeSection(
        overrides: Partial<AgenticSection> & { type: AgenticSectionType }
@@ -18,26 +18,29 @@ describe('buildAssistantRawOutput', () => {
        });
 
        it('formats a reasoning section with a single newline between tags and content', () => {
-               const sections = [makeSection({ type: AgenticSectionType.REASONING, content: 'thinking...' })];
+               const sections = [makeSection({ content: 'thinking...', type: AgenticSectionType.REASONING })];
+
                expect(buildAssistantRawOutput(sections)).toBe(
                        `${REASONING_TAGS.START}\nthinking...${REASONING_TAGS.END}`
                );
        });
 
        it('formats a text section as-is', () => {
-               const sections = [makeSection({ type: AgenticSectionType.TEXT, content: 'Hello' })];
+               const sections = [makeSection({ content: 'Hello', type: AgenticSectionType.TEXT })];
+
                expect(buildAssistantRawOutput(sections)).toBe('Hello');
        });
 
        it('formats a tool call with JSON args and no result label', () => {
                const sections = [
                        makeSection({
-                               type: AgenticSectionType.TOOL_CALL,
-                               toolName: 'read_file',
                                toolArgs: JSON.stringify({ path: '/tmp/file.txt' }),
-                               toolResult: 'file contents'
+                               toolName: 'read_file',
+                               toolResult: 'file contents',
+                               type: AgenticSectionType.TOOL_CALL
                        })
                ];
+
                expect(buildAssistantRawOutput(sections)).toBe(
                        [
                                '{',
@@ -55,21 +58,23 @@ describe('buildAssistantRawOutput', () => {
 
        it('joins multiple sections with double newlines', () => {
                const sections = [
-                       makeSection({ type: AgenticSectionType.TEXT, content: 'Hello' }),
-                       makeSection({ type: AgenticSectionType.TOOL_CALL, toolName: 'noop' })
+                       makeSection({ content: 'Hello', type: AgenticSectionType.TEXT }),
+                       makeSection({ toolName: 'noop', type: AgenticSectionType.TOOL_CALL })
                ];
+
                expect(buildAssistantRawOutput(sections)).toBe('Hello\n\n{\n  "name": "noop"\n}');
        });
 
        it('falls back to raw string args when JSON parsing fails', () => {
                const sections = [
                        makeSection({
-                               type: AgenticSectionType.TOOL_CALL,
-                               toolName: 'broken',
                                toolArgs: '{not json',
-                               toolResult: 'result'
+                               toolName: 'broken',
+                               toolResult: 'result',
+                               type: AgenticSectionType.TOOL_CALL
                        })
                ];
+
                expect(buildAssistantRawOutput(sections)).toBe(
                        ['{', '  "name": "broken",', '  "arguments": "{not json"', '}', '', '', 'result'].join('\n')
                );
index a1f2c8d115227d71e1bbbeea619c795dc0f15c46..6147dee672679fbc982bd159f9c65619fda51857 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
 import { classifyToolResult } from '$lib/utils/agentic';
+import { describe, expect, it } from 'vitest';
 
 describe('classifyToolResult', () => {
        describe('text', () => {
@@ -43,6 +43,7 @@ describe('classifyToolResult', () => {
 
                it('classifies a deeply nested JSON payload', () => {
                        const nested = JSON.stringify({ items: [{ id: 1, tags: ['a', 'b'] }] }, null, 2);
+
                        expect(classifyToolResult(nested)).toBe('json');
                });
 
@@ -52,6 +53,7 @@ describe('classifyToolResult', () => {
                        // `classifyToolResult` only inspects the top-level shape, not
                        // every nested line marker.
                        const jsonWithLink = '{"docs": "see [docs](https://example.com) for more"}';
+
                        expect(classifyToolResult(jsonWithLink)).toBe('json');
                });
        });
@@ -91,11 +93,13 @@ describe('classifyToolResult', () => {
 
                it('classifies a markdown table', () => {
                        const table = '| a | b |\n| - | - |\n| 1 | 2 |';
+
                        expect(classifyToolResult(table)).toBe('markdown');
                });
 
                it('classifies a markdown table with alignment markers', () => {
                        const table = '| left | center | right |\n| :--- | :---: | ---: |\n| a | b | c |';
+
                        expect(classifyToolResult(table)).toBe('markdown');
                });
 
@@ -116,6 +120,7 @@ describe('classifyToolResult', () => {
                                '| ----- | ----- |',
                                '| a     | b     |'
                        ].join('\n');
+
                        expect(classifyToolResult(md)).toBe('markdown');
                });
        });
@@ -124,6 +129,7 @@ describe('classifyToolResult', () => {
                it('prefers JSON over markdown when both signals are present', () => {
                        // Starts with `[`, parses as JSON - markdown check is skipped.
                        const arr = '[1, 2, "# not-a-heading", "**not-bold**"]';
+
                        expect(classifyToolResult(arr)).toBe('json');
                });
        });
index d8ea4899e2118330e72eb10fe147426217baec69..71ac790e60d9134db8f35e3b6fe85ea91bae81ec 100644 (file)
@@ -1,57 +1,61 @@
-import { describe, it, expect } from 'vitest';
 import { AttachmentType } from '$lib/enums';
 import {
        formatMessageForClipboard,
-       parseClipboardContent,
-       hasClipboardAttachments
+       hasClipboardAttachments,
+       parseClipboardContent
 } from '$lib/utils/clipboard';
+import { describe, expect, it } from 'vitest';
 
 describe('formatMessageForClipboard', () => {
        it('returns plain content when no extras', () => {
                const result = formatMessageForClipboard('Hello world', undefined);
+
                expect(result).toBe('Hello world');
        });
 
        it('returns plain content when extras is empty array', () => {
                const result = formatMessageForClipboard('Hello world', []);
+
                expect(result).toBe('Hello world');
        });
 
        it('handles empty string content', () => {
                const result = formatMessageForClipboard('', undefined);
+
                expect(result).toBe('');
        });
 
        it('returns plain content when extras has only non-text attachments', () => {
                const extras = [
                        {
-                               type: AttachmentType.IMAGE as const,
+                               base64Url: 'data:image/png;base64,...',
                                name: 'image.png',
-                               base64Url: 'data:image/png;base64,...'
+                               type: AttachmentType.IMAGE as const
                        }
                ];
                const result = formatMessageForClipboard('Hello world', extras);
+
                expect(result).toBe('Hello world');
        });
 
        it('filters non-text attachments and keeps only text ones', () => {
                const extras = [
                        {
-                               type: AttachmentType.IMAGE as const,
+                               base64Url: 'data:image/png;base64,...',
                                name: 'image.png',
-                               base64Url: 'data:image/png;base64,...'
+                               type: AttachmentType.IMAGE as const
                        },
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Text content',
                                name: 'file.txt',
-                               content: 'Text content'
+                               type: AttachmentType.TEXT as const
                        },
                        {
-                               type: AttachmentType.PDF as const,
-                               name: 'doc.pdf',
                                base64Data: 'data:application/pdf;base64,...',
                                content: 'PDF content',
-                               processedAsImages: false
+                               name: 'doc.pdf',
+                               processedAsImages: false,
+                               type: AttachmentType.PDF as const
                        }
                ];
                const result = formatMessageForClipboard('Hello', extras);
@@ -64,14 +68,14 @@ describe('formatMessageForClipboard', () => {
        it('formats message with text attachments', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'File 1 content',
                                name: 'file1.txt',
-                               content: 'File 1 content'
+                               type: AttachmentType.TEXT as const
                        },
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'File 2 content',
                                name: 'file2.txt',
-                               content: 'File 2 content'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const result = formatMessageForClipboard('Hello world', extras);
@@ -87,9 +91,9 @@ describe('formatMessageForClipboard', () => {
                const content = 'Hello "world" with\nnewline';
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Test content',
                                name: 'test.txt',
-                               content: 'Test content'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const result = formatMessageForClipboard(content, extras);
@@ -98,15 +102,16 @@ describe('formatMessageForClipboard', () => {
                expect(result.startsWith('"')).toBe(true);
                // The content should be properly escaped
                const parsed = JSON.parse(result.split('\n')[0]);
+
                expect(parsed).toBe(content);
        });
 
        it('converts legacy context type to TEXT type', () => {
                const extras = [
                        {
-                               type: AttachmentType.LEGACY_CONTEXT as const,
+                               content: 'Legacy content',
                                name: 'legacy.txt',
-                               content: 'Legacy content'
+                               type: AttachmentType.LEGACY_CONTEXT as const
                        }
                ];
                const result = formatMessageForClipboard('Hello', extras);
@@ -118,9 +123,9 @@ describe('formatMessageForClipboard', () => {
        it('handles attachment content with special characters', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'const x = "hello\\nworld";\nconst y = `template ${var}`;',
                                name: 'code.js',
-                               content: 'const x = "hello\\nworld";\nconst y = `template ${var}`;'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard('Check this code', extras);
@@ -134,9 +139,9 @@ describe('formatMessageForClipboard', () => {
        it('handles unicode characters in content and attachments', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: '日本語テスト 🎉 émojis',
                                name: 'unicode.txt',
-                               content: '日本語テスト 🎉 émojis'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard('Привет мир 👋', extras);
@@ -149,14 +154,14 @@ describe('formatMessageForClipboard', () => {
        it('formats as plain text when asPlainText is true', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'File 1 content',
                                name: 'file1.txt',
-                               content: 'File 1 content'
+                               type: AttachmentType.TEXT as const
                        },
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'File 2 content',
                                name: 'file2.txt',
-                               content: 'File 2 content'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const result = formatMessageForClipboard('Hello world', extras, true);
@@ -166,15 +171,16 @@ describe('formatMessageForClipboard', () => {
 
        it('returns plain content when asPlainText is true but no attachments', () => {
                const result = formatMessageForClipboard('Hello world', [], true);
+
                expect(result).toBe('Hello world');
        });
 
        it('plain text mode does not use JSON format', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Test content',
                                name: 'test.txt',
-                               content: 'Test content'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const result = formatMessageForClipboard('Hello', extras, true);
@@ -216,7 +222,6 @@ describe('parseClipboardContent', () => {
 
        it('returns original text when JSON array is malformed', () => {
                const input = '"Hello"\n[invalid json';
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('"Hello"\n[invalid json');
@@ -229,7 +234,6 @@ describe('parseClipboardContent', () => {
   {"type":"TEXT","name":"file1.txt","content":"File 1 content"},
   {"type":"TEXT","name":"file2.txt","content":"File 2 content"}
 ]`;
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Hello world');
@@ -245,7 +249,6 @@ describe('parseClipboardContent', () => {
 [
   {"type":"TEXT","name":"file.txt","content":"test"}
 ]`;
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Hello "world" with quotes');
@@ -257,7 +260,6 @@ describe('parseClipboardContent', () => {
 [
   {"type":"TEXT","name":"file.txt","content":"test"}
 ]`;
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Hello\nworld');
@@ -266,7 +268,6 @@ describe('parseClipboardContent', () => {
 
        it('returns message only when no array follows', () => {
                const input = '"Just a quoted string"';
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Just a quoted string');
@@ -281,7 +282,6 @@ describe('parseClipboardContent', () => {
   {"name":"missing-type.txt","content":"missing"},
   {"type":"TEXT","content":"missing name"}
 ]`;
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Hello');
@@ -291,7 +291,6 @@ describe('parseClipboardContent', () => {
 
        it('handles empty attachments array', () => {
                const input = '"Hello"\n[]';
-
                const result = parseClipboardContent(input);
 
                expect(result.message).toBe('Hello');
@@ -302,17 +301,16 @@ describe('parseClipboardContent', () => {
                const originalContent = 'Hello "world" with\nspecial characters';
                const originalExtras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Content with\nnewlines and "quotes"',
                                name: 'file1.txt',
-                               content: 'Content with\nnewlines and "quotes"'
+                               type: AttachmentType.TEXT as const
                        },
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Another file',
                                name: 'file2.txt',
-                               content: 'Another file'
+                               type: AttachmentType.TEXT as const
                        }
                ];
-
                const formatted = formatMessageForClipboard(originalContent, originalExtras);
                const parsed = parseClipboardContent(formatted);
 
@@ -360,9 +358,9 @@ describe('roundtrip edge cases', () => {
        it('preserves empty message with attachments', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'Content only',
                                name: 'file.txt',
-                               content: 'Content only'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard('', extras);
@@ -376,9 +374,9 @@ describe('roundtrip edge cases', () => {
        it('preserves attachment with empty content', () => {
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: '',
                                name: 'empty.txt',
-                               content: ''
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard('Message', extras);
@@ -393,9 +391,9 @@ describe('roundtrip edge cases', () => {
                const content = 'Path: C:\\\\Users\\\\test\\\\file.txt';
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: 'D:\\\\Data\\\\file',
                                name: 'path.txt',
-                               content: 'D:\\\\Data\\\\file'
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard(content, extras);
@@ -409,9 +407,9 @@ describe('roundtrip edge cases', () => {
                const content = 'Line1\t\tTabbed\n  Spaced\r\nCRLF';
                const extras = [
                        {
-                               type: AttachmentType.TEXT as const,
+                               content: '\t\t\n\n   ',
                                name: 'whitespace.txt',
-                               content: '\t\t\n\n   '
+                               type: AttachmentType.TEXT as const
                        }
                ];
                const formatted = formatMessageForClipboard(content, extras);
index 99fad60446d1fb945cb9ec752b9284c76cd9a1e9..6106a2b8f6ca3b98d01396d2e67d56877c9dc31b 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { highlightCode, splitGluedClosingCodeFences, trimCodePadding } from '$lib/utils/code';
+import { describe, expect, it } from 'vitest';
 
 describe('trimCodePadding', () => {
        it('removes a single leading newline', () => {
@@ -60,44 +60,52 @@ describe('highlightCode', () => {
 
        it('does not produce a leading newline in the highlighted html', () => {
                const html = highlightCode('\nfunction multiply(a, b) {\n  return a * b;\n}\n', 'javascript');
+
                expect(html.startsWith('\n')).toBe(false);
                expect(html.startsWith(' ')).toBe(false);
        });
 
        it('does not produce a trailing newline in the highlighted html', () => {
                const html = highlightCode('\nfunction foo() {}\n', 'javascript');
+
                expect(html.endsWith('\n')).toBe(false);
        });
 
        it('preserves internal blank lines in highlighted code', () => {
                const html = highlightCode('\nfunction foo() {\n\n  return 1;\n}\n', 'javascript');
+
                expect(html).toContain('\n\n');
        });
 
        it('produces the same body for framed and unframed input', () => {
                const trimmed = highlightCode('function foo() {}', 'javascript');
                const framed = highlightCode('\nfunction foo() {}\n', 'javascript');
+
                expect(framed).toBe(trimmed);
        });
 
        it('auto-detects an unknown language by default', () => {
                const html = highlightCode('const answer = 42;', 'not-a-language');
+
                expect(html).toContain('hljs-');
        });
 
        it('escapes instead of auto-detecting when autoDetect is false', () => {
                const html = highlightCode('const answer = 42;', 'not-a-language', false);
+
                expect(html).not.toContain('hljs-');
                expect(html).toBe('const answer = 42;');
        });
 
        it('still highlights a known language when autoDetect is false', () => {
                const html = highlightCode('const answer = 42;', 'javascript', false);
+
                expect(html).toContain('hljs-');
        });
 
        it('escapes html metacharacters when falling back to plain text', () => {
                const html = highlightCode('<script>a && b</script>', 'not-a-language', false);
+
                expect(html).toBe('&lt;script&gt;a &amp;&amp; b&lt;/script&gt;');
        });
 });
@@ -105,6 +113,7 @@ describe('highlightCode', () => {
 describe('splitGluedClosingCodeFences', () => {
        it('splits text glued to a closing fence onto its own line', () => {
                const input = "```ts\nlet foo = 'bar';\n```create this file on [Desktop](file:///a/b/)";
+
                expect(splitGluedClosingCodeFences(input)).toBe(
                        "```ts\nlet foo = 'bar';\n```\ncreate this file on [Desktop](file:///a/b/)"
                );
@@ -112,6 +121,7 @@ describe('splitGluedClosingCodeFences', () => {
 
        it('leaves a well-formed code block untouched', () => {
                const input = "```ts\nlet foo = 'bar';\n```\ncreate this file on [Desktop](file:///a/b/)";
+
                expect(splitGluedClosingCodeFences(input)).toBe(input);
        });
 
@@ -121,11 +131,13 @@ describe('splitGluedClosingCodeFences', () => {
 
        it('keeps nested markdown fences inside a block intact', () => {
                const input = '```md\n# Example\n```python\nprint(1)\n```\n```';
+
                expect(splitGluedClosingCodeFences(input)).toBe(input);
        });
 
        it('splits every glued closing fence when several blocks are present', () => {
                const input = '```ts\na\n```first words\n\n```js\nb\n```second words';
+
                expect(splitGluedClosingCodeFences(input)).toBe(
                        '```ts\na\n```\nfirst words\n\n```js\nb\n```\nsecond words'
                );
@@ -133,6 +145,7 @@ describe('splitGluedClosingCodeFences', () => {
 
        it('leaves a still-open fence untouched', () => {
                const input = '```ts\nlet foo = 1;';
+
                expect(splitGluedClosingCodeFences(input)).toBe(input);
        });
 });
index f52097de5d07e6e42359ce7e84da299266aabce0..b4f0802690db9f23962c6315b4118cfdd207bf1b 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { findCommandToken, takeCommandDismissSnapshot } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('findCommandToken', () => {
        it('returns null when the value does not start with a slash', () => {
@@ -9,31 +9,31 @@ describe('findCommandToken', () => {
        });
 
        it('parses a bare slash', () => {
-               expect(findCommandToken('/')).toEqual({ name: '', args: '', end: 1 });
+               expect(findCommandToken('/')).toEqual({ args: '', end: 1, name: '' });
        });
 
        it('parses a command name with no args', () => {
-               expect(findCommandToken('/prompt')).toEqual({ name: 'prompt', args: '', end: 7 });
+               expect(findCommandToken('/prompt')).toEqual({ args: '', end: 7, name: 'prompt' });
        });
 
        it('parses a command name followed by a space', () => {
-               expect(findCommandToken('/prompt ')).toEqual({ name: 'prompt', args: '', end: 8 });
+               expect(findCommandToken('/prompt ')).toEqual({ args: '', end: 8, name: 'prompt' });
        });
 
        it('parses args after the command name', () => {
-               expect(findCommandToken('/prompt rev')).toEqual({ name: 'prompt', args: 'rev', end: 11 });
+               expect(findCommandToken('/prompt rev')).toEqual({ args: 'rev', end: 11, name: 'prompt' });
        });
 
        it('parses multi-word args', () => {
                expect(findCommandToken('/prompt  review  code ')).toEqual({
-                       name: 'prompt',
                        args: ' review  code ',
-                       end: 22
+                       end: 22,
+                       name: 'prompt'
                });
        });
 
        it('treats the whole run as the name when there is no space', () => {
-               expect(findCommandToken('/promptx')).toEqual({ name: 'promptx', args: '', end: 8 });
+               expect(findCommandToken('/promptx')).toEqual({ args: '', end: 8, name: 'promptx' });
        });
 });
 
@@ -44,8 +44,8 @@ describe('takeCommandDismissSnapshot', () => {
 
        it('captures the name and args', () => {
                expect(takeCommandDismissSnapshot('/prompt rev')).toEqual({
-                       name: 'prompt',
-                       args: 'rev'
+                       args: 'rev',
+                       name: 'prompt'
                });
        });
 });
index 4610603213c2ce8c00281828db58f28973b2a6fb..fbe133a3b6533a65f6c5c4e885886f16632acc4d 100644 (file)
@@ -1,6 +1,6 @@
-import { describe, expect, it } from 'vitest';
 import { DiffLineKind } from '$lib/enums';
-import { computeLineDiff, renderUnifiedDiff, type DiffLine } from '$lib/utils';
+import { computeLineDiff, type DiffLine, renderUnifiedDiff } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('computeLineDiff', () => {
        it('returns empty for two empty inputs', () => {
@@ -9,34 +9,35 @@ describe('computeLineDiff', () => {
 
        it('marks every line as removed for an empty new text', () => {
                expect(computeLineDiff('a\nb\nc', '')).toEqual([
-                       { kind: 'remove', text: 'a', oldLine: 1 },
-                       { kind: 'remove', text: 'b', oldLine: 2 },
-                       { kind: 'remove', text: 'c', oldLine: 3 }
+                       { kind: 'remove', oldLine: 1, text: 'a' },
+                       { kind: 'remove', oldLine: 2, text: 'b' },
+                       { kind: 'remove', oldLine: 3, text: 'c' }
                ]);
        });
 
        it('marks every line as added for an empty old text', () => {
                expect(computeLineDiff('', 'a\nb')).toEqual([
-                       { kind: 'add', text: 'a', newLine: 1 },
-                       { kind: 'add', text: 'b', newLine: 2 }
+                       { kind: 'add', newLine: 1, text: 'a' },
+                       { kind: 'add', newLine: 2, text: 'b' }
                ]);
        });
 
        it('detects a single-line replace', () => {
                expect(computeLineDiff('old', 'new')).toEqual([
-                       { kind: 'add', text: 'new', newLine: 1 },
-                       { kind: 'remove', text: 'old', oldLine: 1 }
+                       { kind: 'add', newLine: 1, text: 'new' },
+                       { kind: 'remove', oldLine: 1, text: 'old' }
                ]);
        });
 
        it('preserves interleaved context around additions', () => {
                const oldText = ['a', 'b', 'c'].join('\n');
                const newText = ['a', 'b', 'B', 'c'].join('\n');
+
                expect(computeLineDiff(oldText, newText)).toEqual([
-                       { kind: 'context', text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: 'context', text: 'b', oldLine: 2, newLine: 2 },
-                       { kind: 'add', text: 'B', newLine: 3 },
-                       { kind: 'context', text: 'c', oldLine: 3, newLine: 4 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: 'context', newLine: 2, oldLine: 2, text: 'b' },
+                       { kind: 'add', newLine: 3, text: 'B' },
+                       { kind: 'context', newLine: 4, oldLine: 3, text: 'c' }
                ]);
        });
 
@@ -45,47 +46,50 @@ describe('computeLineDiff', () => {
                // show context flanking the changed line at its natural position.
                const oldText = ['a', 'b', 'c', 'd'].join('\n');
                const newText = ['a', 'b', 'X', 'd'].join('\n');
+
                expect(computeLineDiff(oldText, newText)).toEqual([
-                       { kind: 'context', text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: 'context', text: 'b', oldLine: 2, newLine: 2 },
-                       { kind: 'add', text: 'X', newLine: 3 },
-                       { kind: 'remove', text: 'c', oldLine: 3 },
-                       { kind: 'context', text: 'd', oldLine: 4, newLine: 4 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: 'context', newLine: 2, oldLine: 2, text: 'b' },
+                       { kind: 'add', newLine: 3, text: 'X' },
+                       { kind: 'remove', oldLine: 3, text: 'c' },
+                       { kind: 'context', newLine: 4, oldLine: 4, text: 'd' }
                ]);
        });
 
        it('preserves interleaved context around removals', () => {
                const oldText = ['a', 'b', 'c', 'd'].join('\n');
                const newText = ['a', 'c', 'd'].join('\n');
+
                expect(computeLineDiff(oldText, newText)).toEqual([
-                       { kind: 'context', text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: 'remove', text: 'b', oldLine: 2 },
-                       { kind: 'context', text: 'c', oldLine: 3, newLine: 2 },
-                       { kind: 'context', text: 'd', oldLine: 4, newLine: 3 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: 'remove', oldLine: 2, text: 'b' },
+                       { kind: 'context', newLine: 2, oldLine: 3, text: 'c' },
+                       { kind: 'context', newLine: 3, oldLine: 4, text: 'd' }
                ]);
        });
 
        it('handles purely identical inputs', () => {
                const text = 'x\ny\nz';
                const result = computeLineDiff(text, text);
+
                expect(result).toEqual([
-                       { kind: 'context', text: 'x', oldLine: 1, newLine: 1 },
-                       { kind: 'context', text: 'y', oldLine: 2, newLine: 2 },
-                       { kind: 'context', text: 'z', oldLine: 3, newLine: 3 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'x' },
+                       { kind: 'context', newLine: 2, oldLine: 2, text: 'y' },
+                       { kind: 'context', newLine: 3, oldLine: 3, text: 'z' }
                ]);
        });
 
        it('strips a trailing newline on the old/new inputs', () => {
                expect(computeLineDiff('a\n', 'a\nb\n')).toEqual([
-                       { kind: 'context', text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: 'add', text: 'b', newLine: 2 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: 'add', newLine: 2, text: 'b' }
                ]);
        });
 
        it('normalizes trailing CR on each line', () => {
                expect(computeLineDiff('a\r\nb\r\n', 'a\nb')).toEqual([
-                       { kind: 'context', text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: 'context', text: 'b', oldLine: 2, newLine: 2 }
+                       { kind: 'context', newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: 'context', newLine: 2, oldLine: 2, text: 'b' }
                ]);
        });
 
@@ -99,11 +103,13 @@ describe('computeLineDiff', () => {
                // remove) carry no number on that side.
                let lastOld = 0;
                let lastNew = 0;
+
                for (const line of diff) {
                        if (line.oldLine !== undefined) {
                                expect(line.oldLine).toBeGreaterThan(lastOld);
                                lastOld = line.oldLine;
                        }
+
                        if (line.newLine !== undefined) {
                                expect(line.newLine).toBeGreaterThan(lastNew);
                                lastNew = line.newLine;
@@ -123,14 +129,16 @@ describe('renderUnifiedDiff', () => {
                        { kind: DiffLineKind.ADD, text: 'plus' },
                        { kind: DiffLineKind.REMOVE, text: 'minus' }
                ];
+
                expect(renderUnifiedDiff(lines)).toBe(' ctx\n+plus\n-minus');
        });
 
        it('ignores oldLine/newLine metadata when emitting prefixes', () => {
                const lines: DiffLine[] = [
-                       { kind: DiffLineKind.CONTEXT, text: 'a', oldLine: 1, newLine: 1 },
-                       { kind: DiffLineKind.ADD, text: 'b', newLine: 2 }
+                       { kind: DiffLineKind.CONTEXT, newLine: 1, oldLine: 1, text: 'a' },
+                       { kind: DiffLineKind.ADD, newLine: 2, text: 'b' }
                ];
+
                expect(renderUnifiedDiff(lines)).toBe(' a\n+b');
        });
 });
index 8752415f024de2bc38891c089783dbb0622369c0..27a22aab9a1e276dcc0c8e3af5468972d5f0e9f6 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { containsCodeSpan, isOffsetInCodeBlock, tokenizeContent } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('tokenizeContent', () => {
        it('tokenizes a plain text buffer with no badges', () => {
@@ -36,6 +36,7 @@ describe('tokenizeContent', () => {
        it('recognizes badges whose path contains spaces (macOS screenshots)', () => {
                const path = '/Users/allozaur/Desktop/Screenshot 2026-07-28 at 17.21.50.png';
                const source = `[Screenshot 2026-07-28 at 17.21.50.png](file://${path}) `;
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.21.50.png', path },
                        { kind: 'text', text: ' ' }
@@ -46,6 +47,7 @@ describe('tokenizeContent', () => {
                const path =
                        '/var/folders/78/j28m7pn57wb34bfjwlskh62h0000gn/T/TemporaryItems/NSIRD_screencaptureui_GD0A2R/Screenshot 2026-07-28 at 17.23.28.png';
                const source = `[Screenshot 2026-07-28 at 17.23.28.png](file://${path}) `;
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.23.28.png', path },
                        { kind: 'text', text: ' ' }
@@ -55,6 +57,7 @@ describe('tokenizeContent', () => {
        it('keeps text around a badge with spaces in the path', () => {
                const path = '/Users/allozaur/Desktop/Screenshot 2026-07-28 at 17.21.50.png';
                const source = `see [Screenshot 2026-07-28 at 17.21.50.png](file://${path}) done`;
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'text', text: 'see ' },
                        { kind: 'badge', name: 'Screenshot 2026-07-28 at 17.21.50.png', path },
@@ -65,6 +68,7 @@ describe('tokenizeContent', () => {
        it('recognizes badges whose path contains a close parenthesis (macOS duplicate files)', () => {
                const path = '/Users/foo/Screenshot (1).png';
                const source = `[Screenshot (1).png](file://${path}) `;
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'badge', name: 'Screenshot (1).png', path },
                        { kind: 'text', text: ' ' }
@@ -74,6 +78,7 @@ describe('tokenizeContent', () => {
        it('recognizes badges whose folder name is wrapped in parentheses', () => {
                const path = '/Users/foo/Project (Stuff)/main.rs';
                const source = `[main.rs](file://${path}) `;
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'badge', name: 'main.rs', path },
                        { kind: 'text', text: ' ' }
@@ -82,6 +87,7 @@ describe('tokenizeContent', () => {
 
        it('recognizes adjacent badges back-to-back with no separator', () => {
                const source = '[a](file:///p)[b](file:///q)';
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'badge', name: 'a', path: '/p' },
                        { kind: 'badge', name: 'b', path: '/q' }
@@ -98,6 +104,7 @@ describe('tokenizeContent', () => {
 
        it('tokenizes a fenced code block without a language', () => {
                const source = 'before\n```\nconst a = 1;\n```\nafter';
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'text', text: 'before\n' },
                        { kind: 'codeBlock', text: '```\nconst a = 1;\n```' },
@@ -107,6 +114,7 @@ describe('tokenizeContent', () => {
 
        it('tokenizes a fenced code block with a language', () => {
                const source = '```js\nconst a = 1;\n```';
+
                expect(tokenizeContent(source)).toEqual([
                        { kind: 'codeBlock', text: '```js\nconst a = 1;\n```' }
                ]);
@@ -185,6 +193,7 @@ describe('isOffsetInCodeBlock', () => {
 
        it('is true inside a still-open block while it is being typed', () => {
                const open = '```js\nconst a = 1;';
+
                expect(isOffsetInCodeBlock(open, open.length)).toBe(true);
        });
 
@@ -198,6 +207,7 @@ describe('isOffsetInCodeBlock', () => {
        it('toggles per fence across multiple blocks', () => {
                const two = BLOCK + '\ntext\n' + BLOCK;
                const secondBlock = two.lastIndexOf(BLOCK);
+
                expect(isOffsetInCodeBlock(two, secondBlock - 2)).toBe(false);
                expect(isOffsetInCodeBlock(two, secondBlock + 6)).toBe(true);
                expect(isOffsetInCodeBlock(two, two.length)).toBe(false);
index 8e68950d48914a898ccf8efe6ca8d0f06822a4ed..03eb974e5849722f993356f5c93dece3d1fb9582 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { badgeAwareWordJump, leadingBadgeEdgeOffset } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 // Layout of `hello [docs](file:///a/b) world foo`:
 //   "hello" 0-4, " " 5, badge 6-24 (length 19), " " 25, "world" 26-30, " " 31, "foo" 32-34
@@ -42,6 +42,7 @@ describe('badgeAwareWordJump', () => {
 
        it('treats a leading badge as one word in both directions', () => {
                const source = `${BADGE} rest`;
+
                expect(badgeAwareWordJump(source, 0, 'forward')).toBe(BADGE.length);
                expect(badgeAwareWordJump(source, BADGE.length, 'backward')).toBe(0);
        });
@@ -49,6 +50,7 @@ describe('badgeAwareWordJump', () => {
        it('treats adjacent badges as separate words', () => {
                // each badge is 14 chars: "[a](file:///x)" / "[b](file:///y)"
                const source = '[a](file:///x)[b](file:///y)';
+
                expect(badgeAwareWordJump(source, 0, 'forward')).toBe(14);
                expect(badgeAwareWordJump(source, 14, 'forward')).toBe(28);
                expect(badgeAwareWordJump(source, 28, 'backward')).toBe(14);
@@ -58,6 +60,7 @@ describe('badgeAwareWordJump', () => {
        it('jumps over a badge following punctuation', () => {
                // "foo," 0-3, " " 4, badge 5-23 (end 24), " bar" 24-27
                const source = `foo, ${BADGE} bar`;
+
                expect(badgeAwareWordJump(source, 0, 'forward')).toBeNull();
                expect(badgeAwareWordJump(source, 3, 'forward')).toBe(24);
        });
index 76539c76a9e8a25f333a0899693f32da5ebbcaf5..817449822d336e8c6839052d5c0aa5214f229d39 100644 (file)
@@ -1,7 +1,7 @@
-import { describe, it, expect } from 'vitest';
-import { classifyContinueIntent } from '$lib/utils/agentic';
 import { ContinueIntentKind, MessageRole, MessageType } from '$lib/enums';
 import type { DatabaseMessage } from '$lib/types/database';
+import { classifyContinueIntent } from '$lib/utils/agentic';
+import { describe, expect, it } from 'vitest';
 
 /**
  * Tests for the Continue button intent classifier.
@@ -21,23 +21,25 @@ import type { DatabaseMessage } from '$lib/types/database';
  */
 
 let nextId = 0;
+
 function makeMsg(role: MessageRole, opts: Partial<DatabaseMessage> = {}): DatabaseMessage {
        nextId++;
+
        return {
-               id: `msg-${nextId}`,
-               convId: 'conv-1',
-               type: MessageType.TEXT,
-               timestamp: nextId,
-               role,
+               children: [],
                content: '',
+               convId: 'conv-1',
+               id: `msg-${nextId}`,
                parent: null,
-               children: [],
+               role,
+               timestamp: nextId,
+               type: MessageType.TEXT,
                ...opts
        };
 }
 
 function toolCall(id: string, name: string, args: string = '{}'): string {
-       return JSON.stringify([{ id, type: 'function', function: { name, arguments: args } }]);
+       return JSON.stringify([{ function: { arguments: args, name }, id, type: 'function' }]);
 }
 
 describe('classifyContinueIntent', () => {
@@ -46,7 +48,6 @@ describe('classifyContinueIntent', () => {
                        makeMsg(MessageRole.USER, { content: 'hello' }),
                        makeMsg(MessageRole.ASSISTANT, { content: 'hi there' })
                ];
-
                const intent = classifyContinueIntent(messages, 1);
 
                expect(intent).toEqual({ kind: ContinueIntentKind.APPEND_TEXT });
@@ -71,7 +72,6 @@ describe('classifyContinueIntent', () => {
                                toolCalls: toolCall('call_1', 'bash_tool', '{"command":"ls"}')
                        })
                ];
-
                const intent = classifyContinueIntent(messages, 1);
 
                expect(intent).toEqual({ kind: ContinueIntentKind.RERUN_TURN, truncateAfter: 0 });
@@ -86,7 +86,6 @@ describe('classifyContinueIntent', () => {
                        }),
                        makeMsg(MessageRole.TOOL, { content: 'file1\nfile2', toolCallId: 'call_1' })
                ];
-
                const intent = classifyContinueIntent(messages, 1);
 
                expect(intent).toEqual({ kind: ContinueIntentKind.NEXT_TURN, truncateAfter: 2 });
@@ -98,14 +97,13 @@ describe('classifyContinueIntent', () => {
                        makeMsg(MessageRole.ASSISTANT, {
                                content: '',
                                toolCalls: JSON.stringify([
-                                       { id: 'call_1', type: 'function', function: { name: 'a', arguments: '{}' } },
-                                       { id: 'call_2', type: 'function', function: { name: 'b', arguments: '{}' } }
+                                       { function: { arguments: '{}', name: 'a' }, id: 'call_1', type: 'function' },
+                                       { function: { arguments: '{}', name: 'b' }, id: 'call_2', type: 'function' }
                                ])
                        }),
                        makeMsg(MessageRole.TOOL, { content: 'r1', toolCallId: 'call_1' }),
                        makeMsg(MessageRole.TOOL, { content: 'r2', toolCallId: 'call_2' })
                ];
-
                const intent = classifyContinueIntent(messages, 1);
 
                expect(intent).toEqual({ kind: ContinueIntentKind.NEXT_TURN, truncateAfter: 3 });
@@ -122,7 +120,6 @@ describe('classifyContinueIntent', () => {
                        makeMsg(MessageRole.USER, { content: 'wait' }),
                        makeMsg(MessageRole.TOOL, { content: 'late', toolCallId: 'call_1' })
                ];
-
                const intent = classifyContinueIntent(messages, 1);
 
                // truncateAfter must point at the contiguous tool block, not jump over
index 4565ed5b5d4471d3dda73f08b57f9ecb7a7eedd6..1760f8f1767616f49b4c5d62497e50c83e20f3d9 100644 (file)
@@ -1,8 +1,8 @@
-import { beforeAll, describe, expect, it } from 'vitest';
-import { zipSync, strToU8 } from 'fflate';
-import { MessageRole, MessageType } from '$lib/enums';
 import { NEWLINE } from '$lib/constants';
+import { MessageRole, MessageType } from '$lib/enums';
 import type { ExportedConversation } from '$lib/types/database';
+import { strToU8, zipSync } from 'fflate';
+import { beforeAll, describe, expect, it } from 'vitest';
 
 let conversationsStore: typeof import('$lib/stores/conversations.svelte').conversationsStore;
 
@@ -12,12 +12,12 @@ let conversationsStore: typeof import('$lib/stores/conversations.svelte').conver
 beforeAll(async () => {
        const store = new Map<string, string>();
        const polyfill: Storage = {
-               get length() {
-                       return store.size;
-               },
                clear: () => store.clear(),
                getItem: (k) => (store.has(k) ? store.get(k)! : null),
                key: (i) => Array.from(store.keys())[i] ?? null,
+               get length() {
+                       return store.size;
+               },
                removeItem: (k) => {
                        store.delete(k);
                },
@@ -25,6 +25,7 @@ beforeAll(async () => {
                        store.set(k, String(v));
                }
        };
+
        (globalThis as unknown as { localStorage: Storage }).localStorage = polyfill;
 
        ({ conversationsStore } = await import('$lib/stores/conversations.svelte'));
@@ -32,17 +33,17 @@ beforeAll(async () => {
 
 function makeSession(id: string): ExportedConversation {
        return {
-               conv: { id, currNode: `${id}-msg`, lastModified: 0, name: `Chat ${id}` },
+               conv: { currNode: `${id}-msg`, id, lastModified: 0, name: `Chat ${id}` },
                messages: [
                        {
-                               id: `${id}-msg`,
-                               convId: id,
-                               type: MessageType.TEXT,
-                               timestamp: 0,
-                               role: MessageRole.USER,
+                               children: [],
                                content: `hello from ${id}`,
+                               convId: id,
+                               id: `${id}-msg`,
                                parent: null,
-                               children: []
+                               role: MessageRole.USER,
+                               timestamp: 0,
+                               type: MessageType.TEXT
                        }
                ]
        } as unknown as ExportedConversation;
@@ -56,7 +57,6 @@ function makeSession(id: string): ExportedConversation {
 describe('conversationsStore.parseImportFile', () => {
        it('imports a JSONL export whose name has no meaningful extension', async () => {
                const jsonl = conversationsStore.serializeSessionToJsonl(makeSession('a'));
-
                const sessions = await conversationsStore.parseImportFile(new File([jsonl], 'export'));
 
                expect(sessions).toHaveLength(1);
@@ -68,7 +68,6 @@ describe('conversationsStore.parseImportFile', () => {
                const jsonl = [makeSession('a'), makeSession('b')]
                        .map((session) => conversationsStore.serializeSessionToJsonl(session))
                        .join(NEWLINE);
-
                const sessions = await conversationsStore.parseImportFile(new File([jsonl], 'export.txt'));
 
                expect(sessions.map((session) => session.conv.id)).toEqual(['a', 'b']);
@@ -80,7 +79,6 @@ describe('conversationsStore.parseImportFile', () => {
                        'b.jsonl': strToU8(conversationsStore.serializeSessionToJsonl(makeSession('b'))),
                        'notes.txt': strToU8('ignored')
                });
-
                const sessions = await conversationsStore.parseImportFile(new File([zipped], 'archive'));
 
                expect(sessions.map((session) => session.conv.id).sort()).toEqual(['a', 'b']);
@@ -88,7 +86,6 @@ describe('conversationsStore.parseImportFile', () => {
 
        it('imports the legacy JSON array format', async () => {
                const json = JSON.stringify([makeSession('a')], null, 2);
-
                const sessions = await conversationsStore.parseImportFile(new File([json], 'export.jsonl'));
 
                expect(sessions).toHaveLength(1);
@@ -97,7 +94,6 @@ describe('conversationsStore.parseImportFile', () => {
 
        it('imports the legacy JSON single object format', async () => {
                const json = JSON.stringify(makeSession('a'));
-
                const sessions = await conversationsStore.parseImportFile(new File([json], 'export'));
 
                expect(sessions).toHaveLength(1);
index 12b8a6f21cde6c3f2701606c799df2addffd026e..b286db37d5f29259e0628f502461a8c2d93e9fe9 100644 (file)
@@ -1,12 +1,12 @@
-import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { afterEach, beforeEach, describe, expect, it } from 'vitest';
 import {
        colorizeFaviconSvg,
        padFaviconSvg,
        writeThemeFavicons
 } from '../../scripts/favicon-colorize';
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
 
 const SOURCE_SVG = [
        '<svg xmlns="http://www.w3.org/2000/svg">',
@@ -19,6 +19,7 @@ const SOURCE_SVG = [
 describe('colorizeFaviconSvg', () => {
        it('substitutes every currentColor occurrence for the light variant', () => {
                const { light } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
+
                expect(light.match(/currentColor/g)).toBeNull();
                expect(light).toContain('fill="#111111"');
                expect(light).toContain('<circle fill="#111111"/>');
@@ -26,34 +27,39 @@ describe('colorizeFaviconSvg', () => {
 
        it('substitutes every currentColor occurrence for the dark variant', () => {
                const { dark } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
+
                expect(dark.match(/currentColor/g)).toBeNull();
                expect(dark).toContain('fill="#fafafa"');
                expect(dark).toContain('<circle fill="#fafafa"/>');
        });
 
        it('leaves non-currentColor colors untouched in both variants', () => {
-               const { light, dark } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
+               const { dark, light } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
+
                expect(light).toContain('fill="#ff00aa"');
                expect(dark).toContain('fill="#ff00aa"');
        });
 
        it('does not alter any other part of the SVG', () => {
-               const { light, dark } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
+               const { dark, light } = colorizeFaviconSvg(SOURCE_SVG, '#111111', '#fafafa');
                const stripColors = (s: string) =>
                        s.replaceAll('#111111', '').replaceAll('#fafafa', '').replaceAll('currentColor', '');
                const expected = stripColors(SOURCE_SVG);
+
                expect(stripColors(light)).toBe(expected);
                expect(stripColors(dark)).toBe(expected);
        });
 
        it('returns the same SVG for light and dark when called with the same color', () => {
                const result = colorizeFaviconSvg(SOURCE_SVG, '#abcdef', '#abcdef');
+
                expect(result.light).toBe(result.dark);
        });
 
        it('returns the source unchanged when given a color that does not appear (no currentColor in source)', () => {
                const plain = '<svg><path fill="#000"/></svg>';
-               const { light, dark } = colorizeFaviconSvg(plain, '#111111', '#fafafa');
+               const { dark, light } = colorizeFaviconSvg(plain, '#111111', '#fafafa');
+
                expect(light).toBe(plain);
                expect(dark).toBe(plain);
        });
@@ -67,6 +73,7 @@ describe('padFaviconSvg', () => {
 
        it('wraps inner content in a translate-then-scale group that matches padding', () => {
                const padded = padFaviconSvg(SIZED_SVG, 0.05);
+
                // scale = 1 - 0.05 = 0.95
                // translate = (0.05 * 512) / 2 = 12.8 on each axis
                expect(padded).toContain('transform="translate(12.8 12.8) scale(0.95)"');
@@ -77,6 +84,7 @@ describe('padFaviconSvg', () => {
 
        it('preserves the outer <svg> tag attributes', () => {
                const padded = padFaviconSvg(SIZED_SVG, 0.1);
+
                expect(padded.startsWith('<svg width="512" height="512" viewBox="0 0 512 512"')).toBe(true);
        });
 
@@ -92,17 +100,20 @@ describe('padFaviconSvg', () => {
 
        it('returns the input unchanged when no viewBox is present', () => {
                const noViewBox = '<svg width="32" height="32"><path d="M0 0Z"/></svg>';
+
                expect(padFaviconSvg(noViewBox, 0.1)).toBe(noViewBox);
        });
 
        it('returns the input unchanged when viewBox values are not finite numbers', () => {
                const bad = '<svg viewBox="auto auto 0 0"><path/></svg>';
+
                expect(padFaviconSvg(bad, 0.1)).toBe(bad);
        });
 
        it('tolerates a non-square viewBox', () => {
                const wide = '<svg viewBox="0 0 100 50"><rect/></svg>';
                const padded = padFaviconSvg(wide, 0.1);
+
                // scale 0.9, translate (5, 2.5)
                expect(padded).toContain('transform="translate(5 2.5) scale(0.9)"');
        });
@@ -121,27 +132,29 @@ describe('writeThemeFavicons', () => {
        });
 
        afterEach(() => {
-               rmSync(tmpDir, { recursive: true, force: true });
+               rmSync(tmpDir, { force: true, recursive: true });
        });
 
        function setupSource() {
                const sourcePath = join(tmpDir, 'logo.svg');
+
                writeFileSync(sourcePath, LOGO);
+
                return {
-                       sourcePath,
+                       darkPath: join(tmpDir, 'favicon-dark.svg'),
                        lightPath: join(tmpDir, 'favicon.svg'),
-                       darkPath: join(tmpDir, 'favicon-dark.svg')
+                       sourcePath
                };
        }
 
        it('writes colorized, un-padded favicons without modifying the source', () => {
-               const { sourcePath, lightPath, darkPath } = setupSource();
+               const { darkPath, lightPath, sourcePath } = setupSource();
                const before = readFileSync(sourcePath, 'utf-8');
 
                writeThemeFavicons('#abcdef', '#012345', {
-                       sourcePath,
+                       darkOutPath: darkPath,
                        lightOutPath: lightPath,
-                       darkOutPath: darkPath
+                       sourcePath
                });
 
                const lightOut = readFileSync(lightPath, 'utf-8');
@@ -162,17 +175,17 @@ describe('writeThemeFavicons', () => {
        });
 
        it('writes colorized favicons wrapped in a padding <g transform>...</g>', () => {
-               const { sourcePath, lightPath, darkPath } = setupSource();
+               const { darkPath, lightPath, sourcePath } = setupSource();
                // mirror the production wiring: PWA_ASSET_GENERATOR.FAVICON_PADDING
                const padding = 0.04;
                // scale = 1 - 0.04 = 0.96; translate = (0.04 * 512) / 2 = 10.24
                const expectedTransform = 'transform="translate(10.24 10.24) scale(0.96)"';
 
                writeThemeFavicons('#111111', '#fafafa', {
-                       sourcePath,
-                       lightOutPath: lightPath,
                        darkOutPath: darkPath,
-                       padding
+                       lightOutPath: lightPath,
+                       padding,
+                       sourcePath
                });
 
                const lightOut = readFileSync(lightPath, 'utf-8');
index 46b7caa4c6accdc4487fa3818792b58ef9ed9a28..e3e6fb5b87544c80ed6ea8db4817aaef18294411 100644 (file)
@@ -4,8 +4,8 @@ vi.mock('$lib/services/tools.service', () => ({
        ToolsService: { executeToolRaw: vi.fn() }
 }));
 
-import { ToolsService } from '$lib/services/tools.service';
 import { GlobSearchType } from '$lib/enums';
+import { ToolsService } from '$lib/services/tools.service';
 import { runGlobSearchWithChildren } from '$lib/utils';
 
 const mockExecute = vi.mocked(ToolsService.executeToolRaw);
@@ -32,6 +32,7 @@ describe('runGlobSearchWithChildren', () => {
                        50,
                        new AbortController().signal
                );
+
                expect(res.error).toBeUndefined();
                expect(res.entries.map((e) => e.path)).toEqual(['/Users/rootA/note.md', '/Users/rootA/src']);
                expect(res.exactDir).toBeUndefined();
@@ -54,8 +55,9 @@ describe('runGlobSearchWithChildren', () => {
                        3,
                        50,
                        new AbortController().signal,
-                       { type: GlobSearchType.ALL, descendOnTrailingSeparator: true }
+                       { descendOnTrailingSeparator: true, type: GlobSearchType.ALL }
                );
+
                expect(res.error).toBeUndefined();
                expect(res.exactDir).toBe('/Users/rootB/src');
                expect(res.entries.map((e) => e.path)).toEqual([
@@ -77,8 +79,9 @@ describe('runGlobSearchWithChildren', () => {
                        3,
                        50,
                        new AbortController().signal,
-                       { type: GlobSearchType.ALL, descendOnTrailingSeparator: true }
+                       { descendOnTrailingSeparator: true, type: GlobSearchType.ALL }
                );
+
                expect(res.exactDir).toBeUndefined();
                expect(mockExecute).toHaveBeenCalledTimes(1);
        });
@@ -98,6 +101,7 @@ describe('runGlobSearchWithChildren', () => {
                        new AbortController().signal,
                        { type: GlobSearchType.DIR }
                );
+
                expect(res.exactDir).toBe('/Users/rootD/src');
                expect(res.entries.map((e) => e.path)).toEqual(['/Users/rootD/src', '/Users/rootD/src/a.txt']);
                expect(mockExecute).toHaveBeenCalledTimes(2);
@@ -112,6 +116,7 @@ describe('runGlobSearchWithChildren', () => {
                        50,
                        new AbortController().signal
                );
+
                expect(res.error).toBe('boom');
                expect(res.entries).toEqual([]);
                expect(mockExecute).toHaveBeenCalledTimes(1);
index 33619547c62025c76d1df05109fc3d33b44c96e0..5aaa22d0ec748a7171ddaac90d51d23df98b17f5 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { parseHeadersToArray, serializeHeaders } from '$lib/utils/headers';
+import { describe, expect, it } from 'vitest';
 
 /**
  * Tests for the header serialization helpers used by the MCP server form
@@ -72,7 +72,7 @@ describe('serializeHeaders', () => {
 
                // Object key order follows insertion order in modern JS engines, so
                // the serialized JSON writes keys in our input order.
-               expect(JSON.parse(serialized)).toEqual({ 'X-C': '3', 'X-A': '1', 'X-B': '2' });
+               expect(JSON.parse(serialized)).toEqual({ 'X-A': '1', 'X-B': '2', 'X-C': '3' });
        });
 });
 
@@ -82,7 +82,6 @@ describe('parseHeadersToArray / serializeHeaders roundtrip', () => {
                        'Content-Type': 'application/json',
                        'X-Trace-Id': 'abc-123'
                });
-
                const roundtrip = serializeHeaders(parseHeadersToArray(original));
 
                expect(JSON.parse(roundtrip)).toEqual(JSON.parse(original));
@@ -102,7 +101,6 @@ describe('parseHeadersToArray / serializeHeaders roundtrip', () => {
 
        it('preserves upstream keys untouched (does not lowercase them)', () => {
                const upperCased = '{"Authorization":"Bearer xyz"}';
-
                const parsed = parseHeadersToArray(upperCased);
 
                expect(parsed).toEqual([{ key: 'Authorization', value: 'Bearer xyz' }]);
@@ -117,7 +115,6 @@ describe('parseHeadersToArray / serializeHeaders roundtrip', () => {
                        { key: 'X-Trace-Id', value: 'abc-123' },
                        { key: 'Authorization', value: 'Bearer super-secret' }
                ];
-
                const serialized = serializeHeaders(pairs);
 
                expect(serialized).toBe('{"X-Trace-Id":"abc-123","Authorization":"Bearer super-secret"}');
index b1ad4777d92b7846e9911852ab0c3cfa0c964d5d..5e9f9e84b836f0f31110553e4c77b033baa43e13 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { getJpegOrientationFromDataURL, isJpegMimeType } from '$lib/utils/jpeg-orientation';
+import { describe, expect, it } from 'vitest';
 
 // Builds the TIFF payload of an APP1 segment holding a single IFD0 entry
 function buildTiff(littleEndian: boolean, tag: number, value: number): number[] {
@@ -36,6 +36,7 @@ function buildJpegDataURL(tiff: number[] | null, prependApp0 = false): string {
        if (tiff) {
                const payload = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00, ...tiff];
                const length = payload.length + 2;
+
                bytes.push(0xff, 0xe1, length >> 8, length & 0xff, ...payload);
        }
 
@@ -74,11 +75,13 @@ describe('getJpegOrientationFromDataURL', () => {
 
        it('returns 1 for a payload that is not a JPEG', () => {
                const png = btoa(String.fromCharCode(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a));
+
                expect(getJpegOrientationFromDataURL(`data:image/png;base64,${png}`)).toBe(1);
        });
 
        it('returns 1 for a truncated payload', () => {
                const truncated = btoa(String.fromCharCode(0xff, 0xd8, 0xff));
+
                expect(getJpegOrientationFromDataURL(`data:image/jpeg;base64,${truncated}`)).toBe(1);
        });
 
index 011b9c0af7da4ac5353a35ccd0676bd773cf893d..75261c4dd8588262d41a4982b1c410e455dbcebc 100644 (file)
@@ -1,6 +1,6 @@
 /* eslint-disable no-irregular-whitespace */
-import { describe, it, expect, test } from 'vitest';
 import { maskInlineLaTeX, preprocessLaTeX } from '$lib/utils/latex-protection';
+import { describe, expect, it, test } from 'vitest';
 
 describe('maskInlineLaTeX', () => {
        it('should protect LaTeX $x + y$ but not money $3.99', () => {
@@ -126,6 +126,7 @@ describe('preprocessLaTeX', () => {
                const input =
                        '\\( \\mathrm{GL}_2(\\mathbb{F}_7) \\): Group of invertible matrices with entries in \\(\\mathbb{F}_7\\).';
                const output = preprocessLaTeX(input);
+
                expect(output).toBe(
                        '$ \\mathrm{GL}_2(\\mathbb{F}_7) $: Group of invertible matrices with entries in $\\mathbb{F}_7$.'
                );
@@ -135,6 +136,7 @@ describe('preprocessLaTeX', () => {
                const input =
                        'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula \\((x_1,\\ldots,x_n)\\).';
                const output = preprocessLaTeX(input);
+
                expect(output).toBe(
                        'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula $(x_1,\\ldots,x_n)$.'
                );
@@ -229,6 +231,7 @@ h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr]
 \\end{aligned}}
 \\]`;
                const output = preprocessLaTeX(input);
+
                expect(output).toBe(
                        `$$
 \\boxed{
@@ -280,6 +283,7 @@ $$\pi_n(\mathbb{S}^3) = \begin{cases}
 \mathbb{Z}_2 & n = 4 \\
 \end{cases}$$`;
                const output = preprocessLaTeX(input);
+
                // If the formula contains '\\' the $$-delimiters should be in their own line.
                expect(output).toBe(`- Algebraic topology, Homotopy Groups of $\\mathbb{S}^3$:
 $$\n\\pi_n(\\mathbb{S}^3) = \\begin{cases}
index ce721ca6cd943ac0152347a4337a48af60260829..619442dcd4fe357c48177b8820dffa4a64e91733 100644 (file)
@@ -1,16 +1,16 @@
+import { CONFIG_LOCALSTORAGE_KEY, STORAGE_APP_NAME } from '$lib/constants';
 import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
-import { STORAGE_APP_NAME, CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
 
 // node env unit project has no DOM, install a minimal localStorage backed by a Map
 beforeAll(() => {
        const store = new Map<string, string>();
        const polyfill: Storage = {
-               get length() {
-                       return store.size;
-               },
                clear: () => store.clear(),
                getItem: (k) => (store.has(k) ? store.get(k)! : null),
                key: (i) => Array.from(store.keys())[i] ?? null,
+               get length() {
+                       return store.size;
+               },
                removeItem: (k) => {
                        store.delete(k);
                },
@@ -18,6 +18,7 @@ beforeAll(() => {
                        store.set(k, String(v));
                }
        };
+
        (globalThis as unknown as { localStorage: Storage }).localStorage = polyfill;
 });
 
@@ -45,6 +46,7 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
 
        async function runMigrations() {
                const { MigrationService } = await import('$lib/services/migration.service');
+
                await MigrationService.runAllMigrations();
        }
 
@@ -60,13 +62,13 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
 
        it('applies matching overrides onto mcpServers[i].enabled and preserves the legacy key', async () => {
                writeConfig({
-                       mcpServers: JSON.stringify([
-                               { id: 'exa', enabled: false, url: 'https://mcp.exa.ai/mcp' },
-                               { id: 'hf', enabled: false, url: 'https://huggingface.co/mcp' }
-                       ]),
                        [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([
-                               { serverId: 'exa', enabled: true },
-                               { serverId: 'hf', enabled: false }
+                               { enabled: true, serverId: 'exa' },
+                               { enabled: false, serverId: 'hf' }
+                       ]),
+                       mcpServers: JSON.stringify([
+                               { enabled: false, id: 'exa', url: 'https://mcp.exa.ai/mcp' },
+                               { enabled: false, id: 'hf', url: 'https://huggingface.co/mcp' }
                        ])
                });
 
@@ -85,11 +87,11 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
 
        it('skips override ids that do not match any configured server', async () => {
                writeConfig({
-                       mcpServers: JSON.stringify([{ id: 'exa', enabled: false, url: 'https://mcp.exa.ai/mcp' }]),
                        [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([
-                               { serverId: 'orphan', enabled: true },
-                               { serverId: 'exa', enabled: true }
-                       ])
+                               { enabled: true, serverId: 'orphan' },
+                               { enabled: true, serverId: 'exa' }
+                       ]),
+                       mcpServers: JSON.stringify([{ enabled: false, id: 'exa', url: 'https://mcp.exa.ai/mcp' }])
                });
 
                await runMigrations();
@@ -107,7 +109,7 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
 
        it('is a no-op when there are no legacy overrides', async () => {
                writeConfig({
-                       mcpServers: JSON.stringify([{ id: 'exa', enabled: true, url: 'https://mcp.exa.ai/mcp' }])
+                       mcpServers: JSON.stringify([{ enabled: true, id: 'exa', url: 'https://mcp.exa.ai/mcp' }])
                });
 
                await runMigrations();
@@ -124,25 +126,26 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
 
        it('does not rewrite mcpServers when override.enabled already matches', async () => {
                const originalServers = JSON.stringify([
-                       { id: 'exa', enabled: true, url: 'https://mcp.exa.ai/mcp' }
+                       { enabled: true, id: 'exa', url: 'https://mcp.exa.ai/mcp' }
                ]);
 
                writeConfig({
-                       mcpServers: originalServers,
-                       [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([{ serverId: 'exa', enabled: true }])
+                       [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([{ enabled: true, serverId: 'exa' }]),
+                       mcpServers: originalServers
                });
 
                await runMigrations();
 
                const after = readConfig();
+
                expect(after.mcpServers).toBe(originalServers);
                expect(MCP_DEFAULT_OVERRIDES_KEY in after).toBe(true);
        });
 
        it('records itself as completed so subsequent loads do not re-run', async () => {
                writeConfig({
-                       mcpServers: JSON.stringify([{ id: 'exa', enabled: false, url: 'https://mcp.exa.ai/mcp' }]),
-                       [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([{ serverId: 'exa', enabled: true }])
+                       [MCP_DEFAULT_OVERRIDES_KEY]: JSON.stringify([{ enabled: true, serverId: 'exa' }]),
+                       mcpServers: JSON.stringify([{ enabled: false, id: 'exa', url: 'https://mcp.exa.ai/mcp' }])
                });
 
                const { MigrationService } = await import('$lib/services/migration.service');
@@ -150,8 +153,10 @@ describe('mcp-default-overrides-merge-v1 migration', () => {
                await MigrationService.runAllMigrations();
 
                const stateRaw = localStorage.getItem(MIGRATION_STATE_KEY);
+
                expect(stateRaw).not.toBeNull();
                const state = JSON.parse(stateRaw!) as { completed: string[]; failed: string[] };
+
                expect(state.completed).toContain('mcp-default-overrides-merge-v1');
                expect(state.failed).not.toContain('mcp-default-overrides-merge-v1');
        });
index 344ef8452871684c2788791c52243fe05e800002..e7400eeb121f2d35a2efd47881f79c9e1650e197 100644 (file)
@@ -1,18 +1,18 @@
-import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
 import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
 import type { DatabaseConversation } from '$lib/types/database';
+import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
 
 // node env unit project has no DOM, install a minimal localStorage backed by a Map
 beforeAll(() => {
        const store = new Map<string, string>();
        const polyfill: Storage = {
-               get length() {
-                       return store.size;
-               },
                clear: () => store.clear(),
                getItem: (k) => (store.has(k) ? store.get(k)! : null),
                key: (i) => Array.from(store.keys())[i] ?? null,
+               get length() {
+                       return store.size;
+               },
                removeItem: (k) => {
                        store.delete(k);
                },
@@ -20,6 +20,7 @@ beforeAll(() => {
                        store.set(k, String(v));
                }
        };
+
        (globalThis as unknown as { localStorage: Storage }).localStorage = polyfill;
 });
 
@@ -37,8 +38,8 @@ describe('conversationsStore MCP override resolution', () => {
                        CONFIG_LOCALSTORAGE_KEY,
                        JSON.stringify({
                                [SETTINGS_KEYS.MCP_SERVERS]: JSON.stringify([
-                                       { id: 'alpha', enabled: false, url: 'https://alpha.example.com/mcp' },
-                                       { id: 'bravo', enabled: true, url: 'https://bravo.example.com/mcp' }
+                                       { enabled: false, id: 'alpha', url: 'https://alpha.example.com/mcp' },
+                                       { enabled: true, id: 'bravo', url: 'https://bravo.example.com/mcp' }
                                ])
                        })
                );
@@ -49,6 +50,7 @@ describe('conversationsStore MCP override resolution', () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
                const raw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}';
                const saved = JSON.parse(raw) as Record<string, unknown>;
+
                settingsStore.config = {
                        ...settingsStore.config,
                        [SETTINGS_KEYS.MCP_SERVERS]: saved[SETTINGS_KEYS.MCP_SERVERS]
@@ -63,16 +65,17 @@ describe('conversationsStore MCP override resolution', () => {
                overrides?: { serverId: string; enabled: boolean }[]
        ): DatabaseConversation {
                return {
-                       id: 'conv-1',
                        currNode: null,
+                       id: 'conv-1',
                        lastModified: 0,
-                       name: 'Test chat',
-                       mcpServerOverrides: overrides
+                       mcpServerOverrides: overrides,
+                       name: 'Test chat'
                };
        }
 
        it('inherits server.enabled when no conversation is active', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = null;
 
                expect(conversationsStore.isMcpServerEnabledForChat('alpha')).toBe(false);
@@ -81,6 +84,7 @@ describe('conversationsStore MCP override resolution', () => {
 
        it('inherits server.enabled on a newly created chat with no overrides', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = makeConversation();
 
                // Empty override list: must fall back to global server.enabled, not all-off.
@@ -90,6 +94,7 @@ describe('conversationsStore MCP override resolution', () => {
 
        it('inherits server.enabled on a newly created chat when overrides is undefined', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = makeConversation(undefined);
 
                expect(conversationsStore.isMcpServerEnabledForChat('alpha')).toBe(false);
@@ -98,9 +103,10 @@ describe('conversationsStore MCP override resolution', () => {
 
        it('uses explicit per-chat overrides, with defaults for non-overridden servers', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                // Override flips bravo off for this chat, alpha keeps its global default.
                conversationsStore.activeConversation = makeConversation([
-                       { serverId: 'bravo', enabled: false }
+                       { enabled: false, serverId: 'bravo' }
                ]);
 
                expect(conversationsStore.isMcpServerEnabledForChat('alpha')).toBe(false);
@@ -109,35 +115,38 @@ describe('conversationsStore MCP override resolution', () => {
 
        it('getAllMcpServerOverrides returns a complete list merged from defaults', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = makeConversation([
-                       { serverId: 'alpha', enabled: true }
+                       { enabled: true, serverId: 'alpha' }
                ]);
 
                expect(conversationsStore.getAllMcpServerOverrides()).toEqual([
-                       { serverId: 'alpha', enabled: true },
-                       { serverId: 'bravo', enabled: true }
+                       { enabled: true, serverId: 'alpha' },
+                       { enabled: true, serverId: 'bravo' }
                ]);
        });
 
        it('getAllMcpServerOverrides falls back to defaults when there are no explicit overrides', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = makeConversation();
 
                expect(conversationsStore.getAllMcpServerOverrides()).toEqual([
-                       { serverId: 'alpha', enabled: false },
-                       { serverId: 'bravo', enabled: true }
+                       { enabled: false, serverId: 'alpha' },
+                       { enabled: true, serverId: 'bravo' }
                ]);
        });
 
        it('getMcpServerOverride returns the global default when the server has no explicit override', async () => {
                const { conversationsStore } = await import('$lib/stores/conversations.svelte');
+
                conversationsStore.activeConversation = makeConversation([
-                       { serverId: 'alpha', enabled: true }
+                       { enabled: true, serverId: 'alpha' }
                ]);
 
                expect(conversationsStore.getMcpServerOverride('bravo')).toEqual({
-                       serverId: 'bravo',
-                       enabled: true
+                       enabled: true,
+                       serverId: 'bravo'
                });
        });
 });
index 35eec6fb7967cbb06f06f69512f22537a218cd36..52bc0dad222c63e003fffed20ba5c7fe3df93ab0 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
+import { describe, expect, it } from 'vitest';
 
 /**
  * Default-value policy for the `MCP_SERVERS` setting.
index d3af906813aaed68e38785223c39968331dcb333..d79a3f025e88ab1053587c2ea8ce35bd9ad45c3d 100644 (file)
@@ -1,9 +1,9 @@
-import { afterEach, describe, expect, it, vi } from 'vitest';
 import { Client } from '@modelcontextprotocol/sdk/client';
-import { MCPService } from '$lib/services/mcp.service';
+import { CORS_PROXY_HEADER_PREFIX } from '$lib/constants';
 import { MCPConnectionPhase, MCPTransportType } from '$lib/enums';
+import { MCPService } from '$lib/services/mcp.service';
 import type { MCPConnectionLog, MCPServerConfig } from '$lib/types';
-import { CORS_PROXY_HEADER_PREFIX } from '$lib/constants';
+import { afterEach, describe, expect, it, vi } from 'vitest';
 
 type DiagnosticFetchFactory = (
        serverName: string,
@@ -33,25 +33,24 @@ describe('MCPService', () => {
        it('stops transport phase logging after handshake diagnostics are disabled', async () => {
                const logs: MCPConnectionLog[] = [];
                const response = new Response('{}', {
-                       status: 200,
-                       headers: { 'content-type': 'application/json' }
+                       headers: { 'content-type': 'application/json' },
+                       status: 200
                });
 
                vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response));
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
-                       transport: MCPTransportType.STREAMABLE_HTTP
+                       transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp'
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log));
 
-               await controller.fetch(config.url, { method: 'POST', body: '{}' });
+               await controller.fetch(config.url, { body: '{}', method: 'POST' });
                expect(logs).toHaveLength(2);
                expect(logs.every((log) => log.message.includes('https://example.com/mcp'))).toBe(true);
 
                controller.disable();
-               await controller.fetch(config.url, { method: 'POST', body: '{}' });
+               await controller.fetch(config.url, { body: '{}', method: 'POST' });
 
                expect(logs).toHaveLength(2);
        });
@@ -59,38 +58,37 @@ describe('MCPService', () => {
        it('redacts all configured custom headers in diagnostic request logs', async () => {
                const logs: MCPConnectionLog[] = [];
                const response = new Response('{}', {
-                       status: 200,
-                       headers: { 'content-type': 'application/json' }
+                       headers: { 'content-type': 'application/json' },
+                       status: 200
                });
 
                vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response));
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
-                       transport: MCPTransportType.STREAMABLE_HTTP,
                        headers: {
                                'x-auth-token': 'secret-token',
                                'x-vendor-api-key': 'secret-key'
-                       }
+                       },
+                       transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp'
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log), {
                        headers: config.headers
                });
 
                await controller.fetch(config.url, {
-                       method: 'POST',
+                       body: '{}',
                        headers: { 'content-type': 'application/json' },
-                       body: '{}'
+                       method: 'POST'
                });
 
                expect(logs).toHaveLength(2);
                expect(logs[0].details).toMatchObject({
                        request: {
                                headers: {
+                                       'content-type': 'application/json',
                                        'x-auth-token': '[redacted]',
-                                       'x-vendor-api-key': '[redacted]',
-                                       'content-type': 'application/json'
+                                       'x-vendor-api-key': '[redacted]'
                                }
                        }
                });
@@ -102,19 +100,18 @@ describe('MCPService', () => {
                const proxiedContentType = `${CORS_PROXY_HEADER_PREFIX}content-type`;
                const proxiedSessionId = `${CORS_PROXY_HEADER_PREFIX}mcp-session-id`;
                const response = new Response('{}', {
-                       status: 200,
-                       headers: { 'content-type': 'application/json' }
+                       headers: { 'content-type': 'application/json' },
+                       status: 200
                });
                const fetchMock = vi.fn().mockResolvedValue(response);
 
                vi.stubGlobal('fetch', fetchMock);
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
                        transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp',
                        useProxy: true
                };
-
                const controller = createDiagnosticFetch(
                        config,
                        (log) => logs.push(log),
@@ -128,15 +125,16 @@ describe('MCPService', () => {
                );
 
                await controller.fetch('http://localhost:8080/cors-proxy?url=https%3A%2F%2Fexample.com%2Fmcp', {
-                       method: 'POST',
+                       body: '{}',
                        headers: {
                                'content-type': 'application/json',
                                'mcp-session-id': 'session-request-12345'
                        },
-                       body: '{}'
+                       method: 'POST'
                });
 
                const sentHeaders = fetchMock.mock.calls[0]?.[1]?.headers as Headers;
+
                expect(sentHeaders.get('authorization')).toBe('Bearer llama-server-key');
                expect(sentHeaders.get(proxiedAuthToken)).toBe('target-token');
                expect(sentHeaders.get(proxiedContentType)).toBe('application/json');
@@ -161,13 +159,11 @@ describe('MCPService', () => {
                vi.stubGlobal('fetch', fetchMock);
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
                        transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp',
                        useProxy: true
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log), {}, true);
-
                const response = await controller.fetch(
                        'http://localhost:8080/cors-proxy?url=https%3A%2F%2Fexample.com%2Fmcp',
                        { method: 'DELETE' }
@@ -176,36 +172,35 @@ describe('MCPService', () => {
                expect(fetchMock).not.toHaveBeenCalled();
                expect(response.status).toBe(200);
                expect(logs.at(-1)?.details).toMatchObject({
-                       response: { status: 200, isFake: true }
+                       response: { isFake: true, status: 200 }
                });
        });
 
        it('partially redacts mcp-session-id in diagnostic request and response logs', async () => {
                const logs: MCPConnectionLog[] = [];
                const response = new Response('{}', {
-                       status: 200,
                        headers: {
                                'content-type': 'application/json',
                                'mcp-session-id': 'session-response-67890'
-                       }
+                       },
+                       status: 200
                });
 
                vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response));
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
-                       transport: MCPTransportType.STREAMABLE_HTTP
+                       transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp'
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log));
 
                await controller.fetch(config.url, {
-                       method: 'POST',
+                       body: '{}',
                        headers: {
                                'content-type': 'application/json',
                                'mcp-session-id': 'session-request-12345'
                        },
-                       body: '{}'
+                       method: 'POST'
                });
 
                expect(logs).toHaveLength(2);
@@ -230,35 +225,34 @@ describe('MCPService', () => {
        it('extracts JSON-RPC methods without logging the raw request body', async () => {
                const logs: MCPConnectionLog[] = [];
                const response = new Response('{}', {
-                       status: 200,
-                       headers: { 'content-type': 'application/json' }
+                       headers: { 'content-type': 'application/json' },
+                       status: 200
                });
 
                vi.stubGlobal('fetch', vi.fn().mockResolvedValue(response));
 
                const config: MCPServerConfig = {
-                       url: 'https://example.com/mcp',
-                       transport: MCPTransportType.STREAMABLE_HTTP
+                       transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'https://example.com/mcp'
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log));
 
                await controller.fetch(config.url, {
-                       method: 'POST',
                        body: JSON.stringify([
-                               { jsonrpc: '2.0', id: 1, method: 'initialize' },
+                               { id: 1, jsonrpc: '2.0', method: 'initialize' },
                                { jsonrpc: '2.0', method: 'notifications/initialized' }
-                       ])
+                       ]),
+                       method: 'POST'
                });
 
                expect(logs[0].details).toMatchObject({
                        request: {
-                               method: 'POST',
                                body: {
                                        kind: 'string',
                                        size: expect.any(Number)
                                },
-                               jsonRpcMethods: ['initialize', 'notifications/initialized']
+                               jsonRpcMethods: ['initialize', 'notifications/initialized'],
+                               method: 'POST'
                        }
                });
        });
@@ -270,13 +264,12 @@ describe('MCPService', () => {
                vi.stubGlobal('fetch', vi.fn().mockRejectedValue(fetchError));
 
                const config: MCPServerConfig = {
-                       url: 'http://localhost:8000/mcp',
-                       transport: MCPTransportType.STREAMABLE_HTTP
+                       transport: MCPTransportType.STREAMABLE_HTTP,
+                       url: 'http://localhost:8000/mcp'
                };
-
                const controller = createDiagnosticFetch(config, (log) => logs.push(log));
 
-               await expect(controller.fetch(config.url, { method: 'POST', body: '{}' })).rejects.toThrow(
+               await expect(controller.fetch(config.url, { body: '{}', method: 'POST' })).rejects.toThrow(
                        'Failed to fetch'
                );
 
@@ -289,12 +282,13 @@ describe('MCPService', () => {
        it('detaches phase error logging after the initialize handshake completes', async () => {
                const phaseLogs: Array<{ phase: MCPConnectionPhase; log: MCPConnectionLog }> = [];
                const stopPhaseLogging = vi.fn();
+
                let emitClientError: ((error: Error) => void) | undefined;
 
                vi.spyOn(MCPService, 'createTransport').mockReturnValue({
+                       stopPhaseLogging,
                        transport: {} as never,
-                       type: MCPTransportType.WEBSOCKET,
-                       stopPhaseLogging
+                       type: MCPTransportType.WEBSOCKET
                });
                vi.spyOn(MCPService, 'listTools').mockResolvedValue([]);
                vi.spyOn(Client.prototype, 'getServerVersion').mockReturnValue(undefined);
@@ -308,18 +302,18 @@ describe('MCPService', () => {
                await MCPService.connect(
                        'test-server',
                        {
-                               url: 'ws://example.com/mcp',
-                               transport: MCPTransportType.WEBSOCKET
+                               transport: MCPTransportType.WEBSOCKET,
+                               url: 'ws://example.com/mcp'
                        },
                        undefined,
                        undefined,
-                       (phase, log) => phaseLogs.push({ phase, log })
+                       (phase, log) => phaseLogs.push({ log, phase })
                );
 
                expect(stopPhaseLogging).toHaveBeenCalledTimes(1);
                expect(
                        phaseLogs.filter(
-                               ({ phase, log }) =>
+                               ({ log, phase }) =>
                                        phase === MCPConnectionPhase.ERROR &&
                                        log.message === 'Protocol error: handshake protocol error'
                        )
@@ -329,7 +323,7 @@ describe('MCPService', () => {
 
                expect(
                        phaseLogs.filter(
-                               ({ phase, log }) =>
+                               ({ log, phase }) =>
                                        phase === MCPConnectionPhase.ERROR &&
                                        log.message === 'Protocol error: runtime protocol error'
                        )
index f2c00418290bff6ae0ebcba14425e5e28edeb13a..af27bbdefac1c00d8201684058d6221a94a87ca4 100644 (file)
@@ -1,16 +1,16 @@
-import { describe, expect, it } from 'vitest';
+import { FileMentionEntryType } from '$lib/enums';
 import {
-       MENTION_BADGE_FILE_ICON_PATHS,
-       MENTION_BADGE_FOLDER_ICON_PATHS,
        buildMentionInsertion,
        containsFileMentionLink,
        decodeFileLinkPath,
        encodeFileLinkPath,
        fileMentionLinkRe,
        getMentionBadgeIconPaths,
-       getMentionBadgeLabel
+       getMentionBadgeLabel,
+       MENTION_BADGE_FILE_ICON_PATHS,
+       MENTION_BADGE_FOLDER_ICON_PATHS
 } from '$lib/utils';
-import { FileMentionEntryType } from '$lib/enums';
+import { describe, expect, it } from 'vitest';
 
 describe('encodeFileLinkPath', () => {
        it('leaves a clean path unchanged', () => {
@@ -47,6 +47,7 @@ describe('fileMentionLinkRe', () => {
                const match = fileMentionLinkRe().exec(
                        '[Screenshot (1).png](file:///Users/foo/Screenshot (1).png)'
                );
+
                expect(match).not.toBeNull();
                expect(match?.[1]).toBe('Screenshot (1).png');
                expect(match?.[2]).toBe('/Users/foo/Screenshot (1).png');
@@ -121,24 +122,26 @@ describe('decodeFileLinkPath', () => {
 
 describe('buildMentionInsertion', () => {
        const file = (path: string, name: string) => ({
-               path,
                name,
+               path,
                type: FileMentionEntryType.FILE
        });
        const dir = (path: string, name: string) => ({
-               path,
                name,
+               path,
                type: FileMentionEntryType.DIRECTORY
        });
 
        it('splices a root-anchored file link in place of the token', () => {
                const value = 'hello @repo';
                const result = buildMentionInsertion(file('/Users/foo/myRepo', 'myRepo'), value, {
-                       start: 6,
-                       end: 11
+                       end: 11,
+                       start: 6
                });
+
                expect(result).not.toBeNull();
-               const { newValue, caretOffset } = result!;
+               const { caretOffset, newValue } = result!;
+
                expect(newValue).toBe('hello [myRepo](file:///Users/foo/myRepo) ');
                expect(caretOffset).toBe(6 + '[myRepo](file:///Users/foo/myRepo) '.length);
        });
@@ -146,9 +149,10 @@ describe('buildMentionInsertion', () => {
        it('keeps the trailing slash on the directory marker', () => {
                const value = 'see @src';
                const { newValue } = buildMentionInsertion(dir('/Users/foo/myRepo/src/', 'src'), value, {
-                       start: 4,
-                       end: 8
+                       end: 8,
+                       start: 4
                })!;
+
                expect(newValue).toBe('see [src](file:///Users/foo/myRepo/src/) ');
        });
 
@@ -157,18 +161,20 @@ describe('buildMentionInsertion', () => {
                const { newValue } = buildMentionInsertion(
                        file('/Users/foo/Desktop/Pic (1).png', 'Pic (1).png'),
                        value,
-                       { start: 0, end: 4 }
+                       { end: 4, start: 0 }
                )!;
+
                expect(newValue).toBe('[Pic (1).png](file:///Users/foo/Desktop/Pic%20(1).png) ');
        });
 
        it('re-adds the directory marker when the cleaned path empties', () => {
-               const { newValue } = buildMentionInsertion(dir('/', 'root'), '/', { start: 0, end: 1 })!;
+               const { newValue } = buildMentionInsertion(dir('/', 'root'), '/', { end: 1, start: 0 })!;
+
                expect(newValue).toBe('[root](file:///) ');
        });
 
        it('returns null for an out-of-range token', () => {
-               expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { start: 0, end: 5 })).toBeNull();
-               expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { start: 2, end: 1 })).toBeNull();
+               expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { end: 5, start: 0 })).toBeNull();
+               expect(buildMentionInsertion(file('/a/b.txt', 'b.txt'), 'x', { end: 1, start: 2 })).toBeNull();
        });
 });
index fd21a94db1f5258d641dbd638bf60d68039805fb..d01c747f53ec300d9664a9da233b0eee5f4df24e 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { findMentionToken, takeMentionDismissSnapshot } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('findMentionToken', () => {
        it('returns null for an empty/bare cursor', () => {
@@ -8,11 +8,11 @@ describe('findMentionToken', () => {
        });
 
        it('recognizes a mention at the start of the value', () => {
-               expect(findMentionToken('@pr', 3)).toEqual({ start: 0, end: 3, query: 'pr' });
+               expect(findMentionToken('@pr', 3)).toEqual({ end: 3, query: 'pr', start: 0 });
        });
 
        it('recognizes a mention after a word boundary', () => {
-               expect(findMentionToken('hello @pr', 9)).toEqual({ start: 6, end: 9, query: 'pr' });
+               expect(findMentionToken('hello @pr', 9)).toEqual({ end: 9, query: 'pr', start: 6 });
        });
 
        it('returns null when the @ is mid-identifier', () => {
@@ -25,9 +25,9 @@ describe('findMentionToken', () => {
        });
 
        it('treats boundary characters (parens, brackets, comma) as token starts', () => {
-               expect(findMentionToken('(@pr', 4)).toEqual({ start: 1, end: 4, query: 'pr' });
-               expect(findMentionToken('[@pr', 4)).toEqual({ start: 1, end: 4, query: 'pr' });
-               expect(findMentionToken('a,@pr', 5)).toEqual({ start: 2, end: 5, query: 'pr' });
+               expect(findMentionToken('(@pr', 4)).toEqual({ end: 4, query: 'pr', start: 1 });
+               expect(findMentionToken('[@pr', 4)).toEqual({ end: 4, query: 'pr', start: 1 });
+               expect(findMentionToken('a,@pr', 5)).toEqual({ end: 5, query: 'pr', start: 2 });
        });
 
        it('does not treat an identifier character as a boundary', () => {
@@ -35,17 +35,17 @@ describe('findMentionToken', () => {
        });
 
        it('extracts the whole token up to the trailing boundary as the query', () => {
-               expect(findMentionToken('@', 1)).toEqual({ start: 0, end: 1, query: '' });
-               expect(findMentionToken('@hello', 6)).toEqual({ start: 0, end: 6, query: 'hello' });
+               expect(findMentionToken('@', 1)).toEqual({ end: 1, query: '', start: 0 });
+               expect(findMentionToken('@hello', 6)).toEqual({ end: 6, query: 'hello', start: 0 });
        });
 
        it('keeps the whole token as the query when the caret is mid-token', () => {
-               expect(findMentionToken('@hello', 4)).toEqual({ start: 0, end: 6, query: 'hello' });
-               expect(findMentionToken('@hello world', 4)).toEqual({ start: 0, end: 6, query: 'hello' });
+               expect(findMentionToken('@hello', 4)).toEqual({ end: 6, query: 'hello', start: 0 });
+               expect(findMentionToken('@hello world', 4)).toEqual({ end: 6, query: 'hello', start: 0 });
        });
 
        it('ignores a boundary @ and keeps the most recent token', () => {
-               expect(findMentionToken('a @foo @bar', 11)).toEqual({ start: 7, end: 11, query: 'bar' });
+               expect(findMentionToken('a @foo @bar', 11)).toEqual({ end: 11, query: 'bar', start: 7 });
        });
 });
 
@@ -57,8 +57,8 @@ describe('takeMentionDismissSnapshot', () => {
 
        it('captures start and query of the current mention', () => {
                expect(takeMentionDismissSnapshot('hello @proj', 11)).toEqual({
-                       start: 6,
-                       query: 'proj'
+                       query: 'proj',
+                       start: 6
                });
        });
 });
index 3439d956eb90c8920837ca2cc70b15f13102b8b7..3dac1dfaba71e27d2bcc3705b5977c2659323ad5 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { ModelsService } from '$lib/services/models.service';
+import { describe, expect, it } from 'vitest';
 
 const { parseModelId } = ModelsService;
 
@@ -244,16 +244,16 @@ describe('parseModelId', () => {
        it('handles ambiguous model names', () => {
                // Qwen3.5 Instruct vs Thinking — tags should distinguish them
                expect(parseModelId('Qwen/Qwen3.5-30B-A3B-Instruct')).toMatchObject({
+                       activatedParams: 'A3B',
                        modelName: 'Qwen3.5',
                        params: '30B',
-                       activatedParams: 'A3B',
                        tags: ['Instruct']
                });
 
                expect(parseModelId('Qwen/Qwen3.5-30B-A3B-Thinking')).toMatchObject({
+                       activatedParams: 'A3B',
                        modelName: 'Qwen3.5',
                        params: '30B',
-                       activatedParams: 'A3B',
                        tags: ['Thinking']
                });
 
index 40c5a0e3aa2a20b5e25aa108f98f516e4b117803..3f1e9e9ba79fc7132ec1db632fa0a9691d2154f5 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { isValidModelName, normalizeModelName } from '$lib/utils/model-names';
+import { describe, expect, it } from 'vitest';
 
 describe('normalizeModelName', () => {
        it('preserves Hugging Face org/model format (single slash)', () => {
index f4143d9c94d8f7bf842d1b7deaad799a46b8945c..ed499d078e8e07dcd8f3e182066989ddc8c95563 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { isExitCodeSummaryLine, parseExecShellCommandExitStatus } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('parseExecShellCommandExitStatus', () => {
        it('returns undefined when result is empty', () => {
@@ -9,15 +9,17 @@ describe('parseExecShellCommandExitStatus', () => {
 
        it('parses a zero-exit summary at end of clean stdout', () => {
                const status = parseExecShellCommandExitStatus('hello world\n[exit code: 0]');
+
                expect(status).toEqual({
                        code: 0,
-                       timedOut: false,
-                       rawText: '[exit code: 0]'
+                       rawText: '[exit code: 0]',
+                       timedOut: false
                });
        });
 
        it('parses a non-zero exit summary', () => {
                const status = parseExecShellCommandExitStatus('cargo: error[E0425]\n[exit code: 101]');
+
                expect(status?.code).toBe(101);
                expect(status?.timedOut).toBe(false);
        });
@@ -26,12 +28,14 @@ describe('parseExecShellCommandExitStatus', () => {
                const status = parseExecShellCommandExitStatus(
                        'still building...\n[exit code: -1] [exit due to timed out]'
                );
+
                expect(status?.code).toBe(-1);
                expect(status?.timedOut).toBe(true);
        });
 
        it('tolerates trailing whitespace after the tail line', () => {
                const status = parseExecShellCommandExitStatus('[exit code: 0]   \n\n');
+
                expect(status?.code).toBe(0);
        });
 
@@ -41,11 +45,13 @@ describe('parseExecShellCommandExitStatus', () => {
                const status = parseExecShellCommandExitStatus(
                        'the shell prints [exit code: 0]\nwhen done\nreally done\n'
                );
+
                expect(status).toBeUndefined();
        });
 
        it('does not match mid-stream exit lines followed by more output', () => {
                const status = parseExecShellCommandExitStatus('[exit code: 0]\nmore output keeps streaming');
+
                expect(status).toBeUndefined();
        });
 });
index 8dc7dffe3af02a04df4bf772f3dc84684efbbf08..85b3420710bd7b6603ce5c33edb8c2246f0620a6 100644 (file)
@@ -1,6 +1,6 @@
-import { describe, expect, it, vi } from 'vitest';
-import { parseMcpServerSettings } from '$lib/utils/mcp';
 import { MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
+import { parseMcpServerSettings } from '$lib/utils/mcp';
+import { describe, expect, it, vi } from 'vitest';
 
 /**
  * Tests for the mcpServers settings parser.
@@ -36,7 +36,7 @@ describe('parseMcpServerSettings', () => {
 
        it('drops entries with no parseable id and substitutes a stable fallback', () => {
                const parsed = parseMcpServerSettings(
-                       JSON.stringify([{ url: 'https://a.test', enabled: true }, { url: 'https://b.test' }])
+                       JSON.stringify([{ enabled: true, url: 'https://a.test' }, { url: 'https://b.test' }])
                );
 
                expect(parsed).toHaveLength(2);
@@ -64,7 +64,7 @@ describe('parseMcpServerSettings', () => {
                // making the Settings value a no-op for existing servers. The
                // parser drops the field so the global applies live everywhere.
                const parsed = parseMcpServerSettings(
-                       JSON.stringify([{ id: 'a', url: 'https://a.test', requestTimeoutSeconds: 45 }])
+                       JSON.stringify([{ id: 'a', requestTimeoutSeconds: 45, url: 'https://a.test' }])
                );
 
                expect(parsed[0]).not.toHaveProperty('requestTimeoutSeconds');
@@ -73,8 +73,8 @@ describe('parseMcpServerSettings', () => {
        it('treats whitespace-only headers strings as undefined', () => {
                const parsed = parseMcpServerSettings(
                        JSON.stringify([
-                               { id: 'a', url: 'https://a.test', headers: '   ' },
-                               { id: 'b', url: 'https://b.test', headers: '{"X-Foo":"bar"}' }
+                               { headers: '   ', id: 'a', url: 'https://a.test' },
+                               { headers: '{"X-Foo":"bar"}', id: 'b', url: 'https://b.test' }
                        ])
                );
 
@@ -87,8 +87,8 @@ describe('parseMcpServerSettings', () => {
                const parsed = parseMcpServerSettings(
                        JSON.stringify([
                                { id: 'a', url: 'https://a.test' },
-                               { id: 'b', url: 'https://b.test', enabled: true },
-                               { id: 'c', url: 'https://c.test', enabled: false },
+                               { enabled: true, id: 'b', url: 'https://b.test' },
+                               { enabled: false, id: 'c', url: 'https://c.test' },
                                { id: 'd', url: 'https://d.test', useProxy: true }
                        ])
                );
@@ -107,8 +107,8 @@ describe('parseMcpServerSettings', () => {
                // contain disabled entries.
                const parsed = parseMcpServerSettings(
                        JSON.stringify([
-                               { id: 'on', url: 'https://on.test', enabled: true },
-                               { id: 'off', url: 'https://off.test', enabled: false }
+                               { enabled: true, id: 'on', url: 'https://on.test' },
+                               { enabled: false, id: 'off', url: 'https://off.test' }
                        ])
                );
 
@@ -122,7 +122,6 @@ describe('parseMcpServerSettings', () => {
                        { id: 'alpha', url: 'https://a.test' },
                        { id: 'beta', url: 'https://b.test' }
                ];
-
                const parsed = parseMcpServerSettings(JSON.stringify(source));
 
                expect(parsed.map((entry) => entry.id)).toEqual(['gamma', 'alpha', 'beta']);
@@ -131,7 +130,7 @@ describe('parseMcpServerSettings', () => {
        it('passes non-string raw input through the JSON-equality path', () => {
                const parsed = parseMcpServerSettings([
                        { id: 'a', url: 'https://a.test' },
-                       { id: 'b', url: 'https://b.test', enabled: true }
+                       { enabled: true, id: 'b', url: 'https://b.test' }
                ]);
 
                expect(parsed).toHaveLength(2);
index 85187febcba42ce1cefe5552e186c2ac06ac6388..4cc94d55c509b5f7aca4ff79686cd562506c6feb 100644 (file)
@@ -3,22 +3,22 @@
 // streaming text tokens trigger redundant JSON.parse calls on unchanged
 // tool call data.
 
-import { describe, it, expect, vi } from 'vitest';
-import { deriveAgenticSections } from '$lib/utils/agentic';
+import { AgenticSectionType, MessageRole } from '$lib/enums';
 import type { ApiChatCompletionToolCall } from '$lib/types/api';
 import type { DatabaseMessage } from '$lib/types/database';
-import { MessageRole, AgenticSectionType } from '$lib/enums';
+import { deriveAgenticSections } from '$lib/utils/agentic';
+import { describe, expect, it, vi } from 'vitest';
 
 function makeMessage(overrides: Partial<DatabaseMessage>): DatabaseMessage {
        return {
-               id: 'm1',
-               convId: 'c1',
-               type: 'text',
-               timestamp: 0,
-               role: MessageRole.ASSISTANT,
+               children: [],
                content: '',
+               convId: 'c1',
+               id: 'm1',
                parent: null,
-               children: [],
+               role: MessageRole.ASSISTANT,
+               timestamp: 0,
+               type: 'text',
                ...overrides
        } as DatabaseMessage;
 }
@@ -31,9 +31,8 @@ describe('parseToolCalls memoization', () => {
                // re-parse (which we verify by checking the returned sections
                // are equivalent).
                const toolCallsJson = JSON.stringify([
-                       { id: 'call_1', type: 'function', function: { name: 'test', arguments: '{}' } }
+                       { function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
                ]);
-
                const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
                const sections1 = deriveAgenticSections(msg, [], [], false);
                const sections2 = deriveAgenticSections(msg, [], [], false);
@@ -44,9 +43,8 @@ describe('parseToolCalls memoization', () => {
 
        it('does not re-parse JSON on cache hit', () => {
                const toolCallsJson = JSON.stringify([
-                       { id: 'call_1', type: 'function', function: { name: 'test', arguments: '{}' } }
+                       { function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
                ]);
-
                const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
                const spy = vi.spyOn(JSON, 'parse');
 
@@ -80,20 +78,18 @@ describe('parseToolCalls memoization', () => {
 describe('deriveAgenticSections O(1) tool message lookup', () => {
        it('matches tool messages to tool calls by toolCallId', () => {
                const toolCallsJson = JSON.stringify([
-                       { id: 'call_1', type: 'function', function: { name: 'test_1', arguments: '{}' } },
-                       { id: 'call_2', type: 'function', function: { name: 'test_2', arguments: '{}' } }
+                       { function: { arguments: '{}', name: 'test_1' }, id: 'call_1', type: 'function' },
+                       { function: { arguments: '{}', name: 'test_2' }, id: 'call_2', type: 'function' }
                ]);
-
                const toolMessages = [
-                       makeMessage({ role: MessageRole.TOOL, toolCallId: 'call_1', content: 'result_1' }),
-                       makeMessage({ role: MessageRole.TOOL, toolCallId: 'call_2', content: 'result_2' })
+                       makeMessage({ content: 'result_1', role: MessageRole.TOOL, toolCallId: 'call_1' }),
+                       makeMessage({ content: 'result_2', role: MessageRole.TOOL, toolCallId: 'call_2' })
                ];
-
                const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
                const sections = deriveAgenticSections(msg, toolMessages, [], false);
-
                // Expect: TEXT + 2 TOOL_CALL sections
                const toolCallSections = sections.filter((s) => s.type === AgenticSectionType.TOOL_CALL);
+
                expect(toolCallSections).toHaveLength(2);
                expect(toolCallSections[0].toolResult).toBe('result_1');
                expect(toolCallSections[1].toolResult).toBe('result_2');
@@ -101,13 +97,12 @@ describe('deriveAgenticSections O(1) tool message lookup', () => {
 
        it('handles missing tool messages (pending calls during streaming)', () => {
                const toolCallsJson = JSON.stringify([
-                       { id: 'call_1', type: 'function', function: { name: 'test', arguments: '{}' } }
+                       { function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }
                ]);
-
                const msg = makeMessage({ content: '', toolCalls: toolCallsJson });
                const sections = deriveAgenticSections(msg, [], [], true);
-
                const toolCallSection = sections.find((s) => s.type === AgenticSectionType.TOOL_CALL_PENDING);
+
                expect(toolCallSection).toBeDefined();
                expect(toolCallSection?.content).toBe('');
        });
@@ -117,29 +112,26 @@ describe('deriveAgenticSections O(1) tool message lookup', () => {
                const toolCalls = Array.from(
                        { length: N },
                        (_, i): ApiChatCompletionToolCall => ({
+                               function: { arguments: '{}', name: `tool_${i}` },
                                id: `call_${i}`,
-                               type: 'function',
-                               function: { name: `tool_${i}`, arguments: '{}' }
+                               type: 'function'
                        })
                );
                const toolCallsJson = JSON.stringify(toolCalls);
-
                const toolMessages = Array.from({ length: N }, (_, i) =>
                        makeMessage({
+                               content: `result_${i}`,
                                role: MessageRole.TOOL,
-                               toolCallId: `call_${i}`,
-                               content: `result_${i}`
+                               toolCallId: `call_${i}`
                        })
                );
-
                const msg = makeMessage({ content: 'hello', toolCalls: toolCallsJson });
-
                // If the lookup were still O(n^2), this would be noticeably slow
                const start = Date.now();
                const sections = deriveAgenticSections(msg, toolMessages, [], false);
                const elapsed = Date.now() - start;
-
                const toolCallSections = sections.filter((s) => s.type === AgenticSectionType.TOOL_CALL);
+
                expect(toolCallSections).toHaveLength(N);
                expect(elapsed).toBeLessThan(100); // Should be fast with O(1) lookup
        });
index efceb4562b459f172b6595296c6863324686eab3..daaacf8a5df31e441c27ce85ac40f401f4b38c25 100644 (file)
@@ -1,18 +1,18 @@
-import { describe, it, expect } from 'vitest';
 import { MessageRole } from '$lib/enums';
-import { deriveAgenticSections } from '$lib/utils/agentic';
 import type { DatabaseMessage } from '$lib/types/database';
+import { deriveAgenticSections } from '$lib/utils/agentic';
+import { describe, expect, it } from 'vitest';
 
 function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {
        return {
-               id: overrides.id ?? 'ast-1',
-               convId: 'conv-1',
-               type: 'text',
-               timestamp: Date.now(),
-               role: MessageRole.ASSISTANT,
+               children: [],
                content: overrides.content ?? '',
+               convId: 'conv-1',
+               id: overrides.id ?? 'ast-1',
                parent: null,
-               children: [],
+               role: MessageRole.ASSISTANT,
+               timestamp: Date.now(),
+               type: 'text',
                ...overrides
        } as DatabaseMessage;
 }
@@ -24,8 +24,10 @@ function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessag
 // onAssistantTurnComplete(...undefined).
 function buildApiToolCalls(message: DatabaseMessage): unknown[] | undefined {
        if (!message.toolCalls) return undefined;
+
        try {
                const parsed = JSON.parse(message.toolCalls);
+
                return Array.isArray(parsed) && parsed.length > 0 ? parsed : undefined;
        } catch {
                return undefined;
@@ -42,16 +44,15 @@ describe('partial tool call cleanup', () => {
                        content: 'partial reasoning',
                        toolCalls: JSON.stringify([
                                {
-                                       id: 'call_1',
-                                       type: 'function',
                                        function: {
-                                               name: 'exec_shell_command',
-                                               arguments: '{"command":`grep -n \\"read_to\\" ` /Users'
-                                       }
+                                               arguments: '{"command":`grep -n \\"read_to\\" ` /Users',
+                                               name: 'exec_shell_command'
+                                       },
+                                       id: 'call_1',
+                                       type: 'function'
                                }
                        ])
                });
-
                const apiToolCalls = buildApiToolCalls(message);
 
                // The bug: even though arguments are invalid, the outer array parses and
@@ -59,6 +60,7 @@ describe('partial tool call cleanup', () => {
                // own for the server to execute the tool.
                expect(apiToolCalls).toBeDefined();
                const args = (apiToolCalls![0] as { function: { arguments: string } }).function.arguments;
+
                expect(() => JSON.parse(args)).toThrow();
        });
 
@@ -71,8 +73,8 @@ describe('partial tool call cleanup', () => {
                        content: 'partial reasoning',
                        toolCalls: ''
                });
-
                const apiToolCalls = buildApiToolCalls(clearedMessage);
+
                expect(apiToolCalls).toBeUndefined();
        });
 
@@ -86,8 +88,8 @@ describe('partial tool call cleanup', () => {
                        reasoningContent: 'thinking about read_to',
                        toolCalls: ''
                });
-
                const sections = deriveAgenticSections(cleared);
+
                expect(sections).toHaveLength(1);
                expect(sections[0].type).toBe('reasoning');
                expect(sections.some((s) => s.type.includes('tool_call'))).toBe(false);
index 64b9630d2293188c829fabea5f0d16d282698023..fc96e4ed9d3569524dddd40bd4cd07d652f4ef71 100644 (file)
@@ -1,4 +1,4 @@
-import { existsSync, readFileSync, readdirSync } from 'node:fs';
+import { existsSync, readdirSync, readFileSync } from 'node:fs';
 import { resolve } from 'node:path';
 import { describe, expect, it } from 'vitest';
 
@@ -11,6 +11,7 @@ describe('PWA Build Output', () => {
        if (!distExists) {
                console.warn(`⚠ Skipping PWA Build Output tests - dist/ not found (run 'npm run build' first)`);
                it('skipped - dist/ not found', () => {});
+
                return;
        }
 
@@ -25,6 +26,7 @@ describe('PWA Build Output', () => {
                it('workbox library exists (hashed filename)', () => {
                        // SvelteKit generates workbox-{hash}.js files
                        const files = readdirSync(DIST_DIR).filter((f) => f.match(/^workbox-[^.]+\.js$/));
+
                        expect(files.length).toBeGreaterThan(0);
                });
 
@@ -38,18 +40,22 @@ describe('PWA Build Output', () => {
                it('SvelteKit bundle.js exists in _app/immutable/', () => {
                        // SvelteKit generates hashed bundle names in _app/immutable/
                        const appDir = resolve(DIST_DIR, '_app', 'immutable');
+
                        expect(existsSync(appDir), '_app/immutable/ not found').toBeTruthy();
                        const files = readdirSync(appDir).filter((f) => f.startsWith('bundle.') && f.endsWith('.js'));
+
                        expect(files.length).toBeGreaterThan(0);
                });
 
                it('SvelteKit bundle.css exists in _app/immutable/assets/', () => {
                        // SvelteKit generates hashed CSS bundles in _app/immutable/assets/
                        const cssDir = resolve(DIST_DIR, '_app', 'immutable', 'assets');
+
                        expect(existsSync(cssDir), '_app/immutable/assets/ not found').toBeTruthy();
                        const files = readdirSync(cssDir).filter(
                                (f) => f.startsWith('bundle.') && f.endsWith('.css')
                        );
+
                        expect(files.length).toBeGreaterThan(0);
                });
 
@@ -66,6 +72,7 @@ describe('PWA Build Output', () => {
                it('has valid JSON with version field', () => {
                        const content = readFileSync(resolve(DIST_DIR, '_app', 'version.json'), 'utf-8');
                        const parsed = JSON.parse(content);
+
                        expect(parsed).toHaveProperty('version');
                        expect(typeof parsed.version).toBe('string');
                        expect(parsed.version.length).toBeGreaterThan(0);
@@ -175,6 +182,7 @@ describe('PWA Build Output', () => {
        describe('Hashed workbox files', () => {
                it('workbox-*.js files exist in dist root (SvelteKit build output)', () => {
                        const files = readdirSync(DIST_DIR).filter((f) => f.match(/^workbox-[^.]+\.js$/));
+
                        expect(files.length).toBeGreaterThan(0);
                });
        });
index b448974a38da76b2831052f02e3b34d5b476d037..df6574d063985c8f1ee096083e9c5d2f95dbab9b 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
 import { MessageRole } from '$lib/enums';
+import { describe, expect, it } from 'vitest';
 
 /**
  * Tests for the new reasoning content handling.
@@ -27,15 +27,15 @@ describe('reasoning content in new structured format', () => {
        it('convertDbMessageToApiChatMessageData includes reasoning_content', () => {
                // Simulate the conversion logic
                const dbMessage = {
-                       role: MessageRole.ASSISTANT,
                        content: 'The answer is 4.',
-                       reasoningContent: 'Let me think: 2+2=4, basic arithmetic.'
+                       reasoningContent: 'Let me think: 2+2=4, basic arithmetic.',
+                       role: MessageRole.ASSISTANT
                };
-
                const apiMessage: Record<string, unknown> = {
-                       role: dbMessage.role,
-                       content: dbMessage.content
+                       content: dbMessage.content,
+                       role: dbMessage.role
                };
+
                if (dbMessage.reasoningContent) {
                        apiMessage.reasoning_content = dbMessage.reasoningContent;
                }
@@ -49,17 +49,16 @@ describe('reasoning content in new structured format', () => {
 
        it('API message excludes reasoning when excludeReasoningFromContext is true', () => {
                const dbMessage = {
-                       role: MessageRole.ASSISTANT,
                        content: 'The answer is 4.',
-                       reasoningContent: 'internal thinking'
+                       reasoningContent: 'internal thinking',
+                       role: MessageRole.ASSISTANT
                };
-
                const excludeReasoningFromContext = true;
-
                const apiMessage: Record<string, unknown> = {
-                       role: dbMessage.role,
-                       content: dbMessage.content
+                       content: dbMessage.content,
+                       role: dbMessage.role
                };
+
                if (!excludeReasoningFromContext && dbMessage.reasoningContent) {
                        apiMessage.reasoning_content = dbMessage.reasoningContent;
                }
@@ -70,15 +69,15 @@ describe('reasoning content in new structured format', () => {
 
        it('handles messages with no reasoning', () => {
                const dbMessage = {
-                       role: MessageRole.ASSISTANT,
                        content: 'No reasoning here.',
-                       reasoningContent: undefined
+                       reasoningContent: undefined,
+                       role: MessageRole.ASSISTANT
                };
-
                const apiMessage: Record<string, unknown> = {
-                       role: dbMessage.role,
-                       content: dbMessage.content
+                       content: dbMessage.content,
+                       role: dbMessage.role
                };
+
                if (dbMessage.reasoningContent) {
                        apiMessage.reasoning_content = dbMessage.reasoningContent;
                }
index 750296c53a772273c75f199e47bf27aca78a106a..05111ce9adcfabff5f482fee07e5023b6ac2b9de 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { redactValue } from '$lib/utils/redact';
+import { describe, expect, it } from 'vitest';
 
 describe('redactValue', () => {
        it('returns [redacted] by default', () => {
index c4325287608afb2b3723f59738e42ea6f6133c30..c895c7c1810d30544970812fe9c036758def9556 100644 (file)
@@ -1,12 +1,12 @@
-import { describe, expect, it } from 'vitest';
 import {
-       getRequestUrl,
-       getRequestMethod,
-       getRequestBody,
-       summarizeRequestBody,
+       extractJsonRpcMethods,
        formatDiagnosticErrorMessage,
-       extractJsonRpcMethods
+       getRequestBody,
+       getRequestMethod,
+       getRequestUrl,
+       summarizeRequestBody
 } from '$lib/utils/request-helpers';
+import { describe, expect, it } from 'vitest';
 
 describe('getRequestUrl', () => {
        it('returns a plain string input as-is', () => {
@@ -19,6 +19,7 @@ describe('getRequestUrl', () => {
 
        it('returns url from a Request object', () => {
                const req = new Request('https://example.com/mcp');
+
                expect(getRequestUrl(req)).toBe('https://example.com/mcp');
        });
 });
@@ -30,6 +31,7 @@ describe('getRequestMethod', () => {
 
        it('falls back to Request.method', () => {
                const req = new Request('https://example.com', { method: 'PUT' });
+
                expect(getRequestMethod(req)).toBe('PUT');
        });
 
@@ -67,6 +69,7 @@ describe('summarizeRequestBody', () => {
 
        it('returns blob kind with size', () => {
                const blob = new Blob(['abc']);
+
                expect(summarizeRequestBody(blob)).toEqual({ kind: 'blob', size: 3 });
        });
 
@@ -98,14 +101,16 @@ describe('formatDiagnosticErrorMessage', () => {
 describe('extractJsonRpcMethods', () => {
        it('extracts methods from a JSON-RPC array', () => {
                const body = JSON.stringify([
-                       { jsonrpc: '2.0', id: 1, method: 'initialize' },
+                       { id: 1, jsonrpc: '2.0', method: 'initialize' },
                        { jsonrpc: '2.0', method: 'notifications/initialized' }
                ]);
+
                expect(extractJsonRpcMethods(body)).toEqual(['initialize', 'notifications/initialized']);
        });
 
        it('extracts method from a single JSON-RPC message', () => {
-               const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' });
+               const body = JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tools/list' });
+
                expect(extractJsonRpcMethods(body)).toEqual(['tools/list']);
        });
 
index 8cc1fcdfc8437f34b9762d8d142e75e3da6f2875..099de8bdf13ab71dbe6085f76989760e63468585 100644 (file)
@@ -1,6 +1,6 @@
-import { describe, expect, it } from 'vitest';
-import { sanitizeHeaders } from '$lib/utils/api-headers';
 import { CORS_PROXY_HEADER_PREFIX } from '$lib/constants';
+import { sanitizeHeaders } from '$lib/utils/api-headers';
+import { describe, expect, it } from 'vitest';
 
 describe('sanitizeHeaders', () => {
        it('returns empty object for undefined input', () => {
@@ -8,20 +8,22 @@ describe('sanitizeHeaders', () => {
        });
 
        it('passes through non-sensitive headers', () => {
-               const headers = new Headers({ 'content-type': 'application/json', accept: 'text/html' });
+               const headers = new Headers({ accept: 'text/html', 'content-type': 'application/json' });
+
                expect(sanitizeHeaders(headers)).toEqual({
-                       'content-type': 'application/json',
-                       accept: 'text/html'
+                       accept: 'text/html',
+                       'content-type': 'application/json'
                });
        });
 
        it('redacts known sensitive headers', () => {
                const headers = new Headers({
                        authorization: 'Bearer secret',
-                       'x-api-key': 'key-123',
-                       'content-type': 'application/json'
+                       'content-type': 'application/json',
+                       'x-api-key': 'key-123'
                });
                const result = sanitizeHeaders(headers);
+
                expect(result.authorization).toBe('[redacted]');
                expect(result['x-api-key']).toBe('[redacted]');
                expect(result['content-type']).toBe('application/json');
@@ -30,20 +32,23 @@ describe('sanitizeHeaders', () => {
        it('partially redacts headers specified in partialRedactHeaders', () => {
                const headers = new Headers({ 'mcp-session-id': 'session-12345' });
                const partial = new Map([['mcp-session-id', 5]]);
+
                expect(sanitizeHeaders(headers, undefined, partial)['mcp-session-id']).toBe('....12345');
        });
 
        it('fully redacts mcp-session-id when no partialRedactHeaders is given', () => {
                const headers = new Headers({ 'mcp-session-id': 'session-12345' });
+
                expect(sanitizeHeaders(headers)['mcp-session-id']).toBe('[redacted]');
        });
 
        it('redacts extra headers provided by the caller', () => {
                const headers = new Headers({
-                       'x-vendor-key': 'vendor-secret',
-                       'content-type': 'application/json'
+                       'content-type': 'application/json',
+                       'x-vendor-key': 'vendor-secret'
                });
                const result = sanitizeHeaders(headers, ['x-vendor-key']);
+
                expect(result['x-vendor-key']).toBe('[redacted]');
                expect(result['content-type']).toBe('application/json');
        });
@@ -51,6 +56,7 @@ describe('sanitizeHeaders', () => {
        it('handles case-insensitive extra header names', () => {
                const headers = new Headers({ 'X-Custom-Token': 'token-value' });
                const result = sanitizeHeaders(headers, ['X-CUSTOM-TOKEN']);
+
                expect(result['x-custom-token']).toBe('[redacted]');
        });
 
index 6a84adf5accf48dfbc526aa703fea39a71f143ac..5733a379745d102cbf1402e0652e9a02a0cdc2a0 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest';
-import { extractSearchResults, extractSearchQuery } from '$lib/utils/search-results';
+import { extractSearchQuery, extractSearchResults } from '$lib/utils/search-results';
+import { describe, expect, it } from 'vitest';
 
 const SAMPLE = `Title: World Cup 2026 | Match schedule, fixtures, results & stadiums
 URL: https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/match-schedule-fixtures-results-teams-stadiums
@@ -23,12 +23,12 @@ Highlights:
 Something
 # World Cup
 ...`;
-
 const QUERY_ARGS = '{"query":"FIFA World Cup 2026 schedule"}';
 
 describe('real-world Exa fixture', () => {
        it('extracts every search result and preserves rich highlights', () => {
                const results = extractSearchResults(SAMPLE);
+
                expect(results.length).toBe(3);
                expect(results[0].title).toContain('World Cup 2026 | Match schedule');
                expect(results[0].url).toContain('fifa.com');
index e2972eaf3db2bb65d24bf69fb9a278339a94fe62..c168dec2582954a227564b16d6b4f566dd186b34 100644 (file)
@@ -1,10 +1,10 @@
-import { describe, expect, it } from 'vitest';
 import {
-       extractSearchResults,
        extractSearchQuery,
+       extractSearchResults,
        faviconForUrl,
        isWebSearchToolName
 } from '$lib/utils/search-results';
+import { describe, expect, it } from 'vitest';
 
 describe('extractSearchResults', () => {
        it('parses the Exa fixture with multiple results', () => {
@@ -30,6 +30,7 @@ Highlights:
 # FIFA World Cup Schedule
 ...`;
                const results = extractSearchResults(fixture);
+
                expect(results.length).toBe(3);
                expect(results[0].title).toContain('World Cup 2026');
                expect(results[0].url).toBe('https://www.fifa.com/articles/match-schedule');
@@ -60,6 +61,7 @@ just a paragraph
 Title: b
 URL: not a url`;
                const results = extractSearchResults(txt);
+
                // Only middle one should pass (has title + url).
                expect(results.length).toBe(1);
                expect(results[0].url).toBe('https://x.com');
@@ -73,6 +75,7 @@ Author: alice
 Highlights:
 a highlight`;
                const results = extractSearchResults(txt);
+
                expect(results.length).toBe(1);
                expect(results[0].title).toBe('only one');
                expect(results[0].author).toBe('alice');
index b83030941b4e883c956d4c9f8b8214930ab33910..33869a6e896fcc7b440586562b5eb61b02e16a78 100644 (file)
@@ -1,65 +1,72 @@
-import { describe, expect, it } from 'vitest';
 import { SourceHistory } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('SourceHistory', () => {
        it('coalesces pushes inside the group window into one undo step', () => {
                const h = new SourceHistory(100, 800);
-               h.push({ value: '', caret: 0 }, 1000);
-               h.push({ value: 'a', caret: 1 }, 1200);
-               h.push({ value: 'ab', caret: 2 }, 1500);
 
-               expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 });
-               expect(h.undo({ value: '', caret: 0 })).toBeNull();
+               h.push({ caret: 0, value: '' }, 1000);
+               h.push({ caret: 1, value: 'a' }, 1200);
+               h.push({ caret: 2, value: 'ab' }, 1500);
+
+               expect(h.undo({ caret: 3, value: 'abc' })).toEqual({ caret: 0, value: '' });
+               expect(h.undo({ caret: 0, value: '' })).toBeNull();
        });
 
        it('starts a new group once the window has passed', () => {
                const h = new SourceHistory(100, 800);
-               h.push({ value: '', caret: 0 }, 1000);
-               h.push({ value: 'abc', caret: 3 }, 2000);
 
-               expect(h.undo({ value: 'abcdef', caret: 6 })).toEqual({ value: 'abc', caret: 3 });
-               expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 });
+               h.push({ caret: 0, value: '' }, 1000);
+               h.push({ caret: 3, value: 'abc' }, 2000);
+
+               expect(h.undo({ caret: 6, value: 'abcdef' })).toEqual({ caret: 3, value: 'abc' });
+               expect(h.undo({ caret: 3, value: 'abc' })).toEqual({ caret: 0, value: '' });
        });
 
        it('newGroup forces a separate entry even inside the window', () => {
                const h = new SourceHistory(100, 800);
-               h.push({ value: '', caret: 0 }, 1000);
-               h.push({ value: 'abc', caret: 3 }, 1100, true);
 
-               expect(h.undo({ value: 'abc\n', caret: 4 })).toEqual({ value: 'abc', caret: 3 });
-               expect(h.undo({ value: 'abc', caret: 3 })).toEqual({ value: '', caret: 0 });
+               h.push({ caret: 0, value: '' }, 1000);
+               h.push({ caret: 3, value: 'abc' }, 1100, true);
+
+               expect(h.undo({ caret: 4, value: 'abc\n' })).toEqual({ caret: 3, value: 'abc' });
+               expect(h.undo({ caret: 3, value: 'abc' })).toEqual({ caret: 0, value: '' });
        });
 
        it('redo round-trips and a fresh push clears the redo stack', () => {
                const h = new SourceHistory(100, 800);
-               h.push({ value: '', caret: 0 }, 1000);
 
-               const undone = h.undo({ value: 'abc', caret: 3 });
-               expect(undone).toEqual({ value: '', caret: 0 });
-               expect(h.redo({ value: '', caret: 0 })).toEqual({ value: 'abc', caret: 3 });
+               h.push({ caret: 0, value: '' }, 1000);
 
-               h.undo({ value: 'abc', caret: 3 });
-               h.push({ value: '', caret: 0 }, 5000);
-               expect(h.redo({ value: 'x', caret: 1 })).toBeNull();
+               const undone = h.undo({ caret: 3, value: 'abc' });
+
+               expect(undone).toEqual({ caret: 0, value: '' });
+               expect(h.redo({ caret: 0, value: '' })).toEqual({ caret: 3, value: 'abc' });
+
+               h.undo({ caret: 3, value: 'abc' });
+               h.push({ caret: 0, value: '' }, 5000);
+               expect(h.redo({ caret: 1, value: 'x' })).toBeNull();
        });
 
        it('starts a new group on the first edit after an undo', () => {
                const h = new SourceHistory(100, 800);
-               h.push({ value: '', caret: 0 }, 1000);
-               h.undo({ value: 'abc', caret: 3 });
 
-               h.push({ value: '', caret: 0 }, 1200);
-               expect(h.undo({ value: 'x', caret: 1 })).toEqual({ value: '', caret: 0 });
+               h.push({ caret: 0, value: '' }, 1000);
+               h.undo({ caret: 3, value: 'abc' });
+
+               h.push({ caret: 0, value: '' }, 1200);
+               expect(h.undo({ caret: 1, value: 'x' })).toEqual({ caret: 0, value: '' });
        });
 
        it('evicts the oldest entry past the limit', () => {
                const h = new SourceHistory(2, 800);
-               h.push({ value: 'one', caret: 0 }, 1000);
-               h.push({ value: 'two', caret: 0 }, 2000);
-               h.push({ value: 'three', caret: 0 }, 3000);
 
-               expect(h.undo({ value: 'cur', caret: 0 })).toEqual({ value: 'three', caret: 0 });
-               expect(h.undo({ value: 'three', caret: 0 })).toEqual({ value: 'two', caret: 0 });
-               expect(h.undo({ value: 'two', caret: 0 })).toBeNull();
+               h.push({ caret: 0, value: 'one' }, 1000);
+               h.push({ caret: 0, value: 'two' }, 2000);
+               h.push({ caret: 0, value: 'three' }, 3000);
+
+               expect(h.undo({ caret: 0, value: 'cur' })).toEqual({ caret: 0, value: 'three' });
+               expect(h.undo({ caret: 0, value: 'three' })).toEqual({ caret: 0, value: 'two' });
+               expect(h.undo({ caret: 0, value: 'two' })).toBeNull();
        });
 });
index 741d107419cc1b97991149d374683fff07e18e70..2d9f0e963c8a2d6d2ab4c41d9254ea3cdcaceda1 100644 (file)
@@ -1,11 +1,12 @@
-import { describe, expect, it } from 'vitest';
 import { parseSseJsonStream } from '$lib/utils/sse';
+import { describe, expect, it } from 'vitest';
 
 function makeSseResponse(events: string[]): Response {
        const body = events.join('\n\n') + '\n\n';
+
        return new Response(body, {
-               status: 200,
-               headers: { 'content-type': 'text/event-stream' }
+               headers: { 'content-type': 'text/event-stream' },
+               status: 200
        });
 }
 
@@ -13,6 +14,7 @@ describe('parseSseJsonStream', () => {
        it('yields parsed data for each record', async () => {
                const response = makeSseResponse(['data: {"chunk": "a"}', 'data: {"chunk": "b"}']);
                const collected: unknown[] = [];
+
                for await (const ev of parseSseJsonStream(response)) {
                        collected.push(ev.data);
                }
@@ -26,6 +28,7 @@ describe('parseSseJsonStream', () => {
                        'data: {"chunk": "after-done"}'
                ]);
                const collected: unknown[] = [];
+
                for await (const ev of parseSseJsonStream(response)) {
                        collected.push(ev.data);
                }
@@ -39,6 +42,7 @@ describe('parseSseJsonStream', () => {
                        'data: {"chunk": "also-ok"}'
                ]);
                const collected: unknown[] = [];
+
                for await (const ev of parseSseJsonStream(response)) {
                        collected.push(ev.data);
                }
@@ -50,16 +54,18 @@ describe('parseSseJsonStream', () => {
                const stream = new ReadableStream<Uint8Array>({
                        start(controller) {
                                const enc = new TextEncoder();
+
                                controller.enqueue(enc.encode(full.slice(0, full.length / 2)));
                                controller.enqueue(enc.encode(full.slice(full.length / 2)));
                                controller.close();
                        }
                });
                const response = new Response(stream, {
-                       status: 200,
-                       headers: { 'content-type': 'text/event-stream' }
+                       headers: { 'content-type': 'text/event-stream' },
+                       status: 200
                });
                const collected: unknown[] = [];
+
                for await (const ev of parseSseJsonStream(response)) {
                        collected.push(ev.data);
                }
@@ -69,6 +75,7 @@ describe('parseSseJsonStream', () => {
        it('returns immediately if response has no body', async () => {
                const response = new Response(null, { status: 200 });
                const collected: unknown[] = [];
+
                for await (const ev of parseSseJsonStream(response)) {
                        collected.push(ev.data);
                }
index a428e5df8a3992f032e3b97df9b6821823553034..a7899d95515f2848c28db10a8f7aad02d28f9d15 100644 (file)
@@ -1,14 +1,14 @@
-import { describe, expect, it } from 'vitest';
 import { ChatService } from '$lib/services/chat.service';
 import type { ApiStreamSession } from '$lib/types';
+import { describe, expect, it } from 'vitest';
 
 function makeSession(overrides: Partial<ApiStreamSession>): ApiStreamSession {
        return {
+               completed_at: 0,
                conversation_id: 'conv',
                is_done: true,
-               total_bytes: 0,
                started_at: 0,
-               completed_at: 0,
+               total_bytes: 0,
                ...overrides
        };
 }
@@ -25,17 +25,20 @@ describe('selectActiveStream', () => {
 
        it('returns the single session when it is running', () => {
                const s = makeSession({ conversation_id: 'only', is_done: false, started_at: 42 });
+
                expect(ChatService.selectActiveStream([s])).toBe(s);
        });
 
        it('returns null when the single session is finalized', () => {
                const s = makeSession({ conversation_id: 'only', is_done: true, started_at: 42 });
+
                expect(ChatService.selectActiveStream([s])).toBeNull();
        });
 
        it('prefers a still running session over a finalized one regardless of started_at', () => {
                const finalized = makeSession({ conversation_id: 'old', is_done: true, started_at: 1000 });
                const running = makeSession({ conversation_id: 'new', is_done: false, started_at: 10 });
+
                expect(ChatService.selectActiveStream([finalized, running])?.conversation_id).toBe('new');
                expect(ChatService.selectActiveStream([running, finalized])?.conversation_id).toBe('new');
        });
@@ -44,6 +47,7 @@ describe('selectActiveStream', () => {
                const a = makeSession({ conversation_id: 'a', is_done: false, started_at: 100 });
                const b = makeSession({ conversation_id: 'b', is_done: false, started_at: 200 });
                const c = makeSession({ conversation_id: 'c', is_done: false, started_at: 150 });
+
                expect(ChatService.selectActiveStream([a, b, c])?.conversation_id).toBe('b');
                expect(ChatService.selectActiveStream([c, a, b])?.conversation_id).toBe('b');
        });
@@ -52,6 +56,7 @@ describe('selectActiveStream', () => {
                const a = makeSession({ conversation_id: 'a', is_done: true, started_at: 10 });
                const b = makeSession({ conversation_id: 'b', is_done: true, started_at: 30 });
                const c = makeSession({ conversation_id: 'c', is_done: true, started_at: 20 });
+
                expect(ChatService.selectActiveStream([a, b, c])).toBeNull();
        });
 
@@ -59,6 +64,7 @@ describe('selectActiveStream', () => {
                // reduce visits left to right, the initial accumulator stays unless a strictly greater value appears
                const a = makeSession({ conversation_id: 'first', is_done: false, started_at: 50 });
                const b = makeSession({ conversation_id: 'second', is_done: false, started_at: 50 });
+
                expect(ChatService.selectActiveStream([a, b])?.conversation_id).toBe('first');
        });
 
@@ -67,6 +73,7 @@ describe('selectActiveStream', () => {
                const old2 = makeSession({ conversation_id: 'old2', is_done: true, started_at: 200 });
                const freshFin = makeSession({ conversation_id: 'freshFin', is_done: true, started_at: 500 });
                const running = makeSession({ conversation_id: 'running', is_done: false, started_at: 400 });
+
                expect(ChatService.selectActiveStream([old1, old2, freshFin, running])?.conversation_id).toBe(
                        'running'
                );
index f52f2cabd53dcf958f7b4107c8dc9ba4ce45647e..43d89272efd6e961f91ca3cb676124aaafe50018 100644 (file)
@@ -4,12 +4,12 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
 beforeAll(() => {
        const store = new Map<string, string>();
        const polyfill: Storage = {
-               get length() {
-                       return store.size;
-               },
                clear: () => store.clear(),
                getItem: (k) => (store.has(k) ? store.get(k)! : null),
                key: (i) => Array.from(store.keys())[i] ?? null,
+               get length() {
+                       return store.size;
+               },
                removeItem: (k) => {
                        store.delete(k);
                },
@@ -17,11 +17,12 @@ beforeAll(() => {
                        store.set(k, String(v));
                }
        };
+
        (globalThis as unknown as { localStorage: Storage }).localStorage = polyfill;
 });
 
-import { ChatService } from '$lib/services/chat.service';
 import { STREAM_RESUME_LOCALSTORAGE_KEY_PREFIX } from '$lib/constants';
+import { ChatService } from '$lib/services/chat.service';
 
 describe('ChatService stream resume', () => {
        beforeEach(() => {
@@ -38,6 +39,7 @@ describe('ChatService stream resume', () => {
        it('saves and reads back the byte count', () => {
                ChatService.saveStreamState('conv-a', 4242);
                const got = ChatService.getStreamState('conv-a');
+
                expect(got).not.toBeNull();
                expect(got!.bytesReceived).toBe(4242);
                expect(typeof got!.updatedAt).toBe('number');
@@ -47,6 +49,7 @@ describe('ChatService stream resume', () => {
                ChatService.saveStreamState('conv-a', 100);
                ChatService.saveStreamState('conv-a', 200);
                const got = ChatService.getStreamState('conv-a');
+
                expect(got!.bytesReceived).toBe(200);
        });
 
index d3184e4c44fdfd7636303e5cf9400c447a90a222..bb28e3830cdf877a8c84a6f1ad036d91353d2657 100644 (file)
@@ -1,5 +1,5 @@
-import { describe, expect, it } from 'vitest';
 import { tryParseToolResultObject } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 describe('tryParseToolResultObject', () => {
        it('returns null when no result is provided', () => {
@@ -9,8 +9,8 @@ describe('tryParseToolResultObject', () => {
 
        it('returns the parsed object when the result is JSON', () => {
                expect(tryParseToolResultObject('{"result":"ok","bytes":42}')).toEqual({
-                       result: 'ok',
-                       bytes: 42
+                       bytes: 42,
+                       result: 'ok'
                });
        });
 
index 1e15451de058ccb5c4a4449dfcab53e9ba56fc23..3349b7ab6e1cddb8d94b175eafc0cfa742a27923 100644 (file)
@@ -1,29 +1,29 @@
-import { describe, expect, it } from 'vitest';
-import { AgenticSectionType, BuiltInTool } from '$lib/enums';
-import type { AgenticSection } from '$lib/utils';
 import { parseToolArgs } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared';
-import { lastPathSegment, abbreviateHome, formatCwdMessage, parseCwdMessage } from '$lib/utils';
+import { parseEditFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
+import { parseExecShellCommandMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command';
+import { parseFileGlobSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search';
+import { parseGrepSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search';
+import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file';
+import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';
 import {
        parseWriteFileMeta,
        type WriteFileMeta
 } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file';
-import { parseEditFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file';
-import { parseReadFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file';
-import { parseGrepSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search';
-import { parseFileGlobSearchMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search';
-import { parseRunJavascriptMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript';
-import { parseExecShellCommandMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command';
+import { AgenticSectionType, BuiltInTool } from '$lib/enums';
+import type { AgenticSection } from '$lib/utils';
+import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils';
+import { describe, expect, it } from 'vitest';
 
 function makeSection(
        overrides: Partial<AgenticSection> = {},
        toolName = BuiltInTool.READ_FILE
 ): AgenticSection {
        return {
-               type: AgenticSectionType.TOOL_CALL,
                content: '',
-               toolName,
                toolArgs: JSON.stringify({ path: '/foo.txt' }),
+               toolName,
                toolResult: undefined,
+               type: AgenticSectionType.TOOL_CALL,
                ...overrides
        };
 }
@@ -91,6 +91,7 @@ describe('formatCwdMessage / parseCwdMessage', () => {
 
        it('round-trips through the parser', () => {
                const info = parseCwdMessage(formatCwdMessage('/Users/al/Documents', '/Users/al'));
+
                expect(info?.path).toBe('/Users/al/Documents');
                expect(info?.display).toBe('~/Documents');
        });
@@ -100,11 +101,11 @@ describe('formatCwdMessage / parseCwdMessage', () => {
                        parseCwdMessage(
                                'Set working directory to [file:///a/b](~/b). Tool calls run with this as their working directory.'
                        )
-               ).toEqual({ path: '/a/b', display: '~/b' });
+               ).toEqual({ display: '~/b', path: '/a/b' });
        });
 
        it('parses the cleared marker', () => {
-               expect(parseCwdMessage('Working directory cleared')).toEqual({ path: null, display: '' });
+               expect(parseCwdMessage('Working directory cleared')).toEqual({ display: '', path: null });
        });
 
        it('returns null for non-cwd content', () => {
@@ -115,6 +116,7 @@ describe('formatCwdMessage / parseCwdMessage', () => {
 describe('parseToolArgs (shared)', () => {
        it('returns null when the section has no toolArgs', () => {
                const result = parseToolArgs(BuiltInTool.READ_FILE, makeSection({ toolArgs: undefined }));
+
                expect(result).toBeNull();
        });
 
@@ -123,6 +125,7 @@ describe('parseToolArgs (shared)', () => {
                        BuiltInTool.READ_FILE,
                        makeSection({ toolArgs: '{"path":"/x"}' }, BuiltInTool.WRITE_FILE)
                );
+
                expect(result).toBeNull();
        });
 
@@ -131,6 +134,7 @@ describe('parseToolArgs (shared)', () => {
                        BuiltInTool.READ_FILE,
                        makeSection({ toolArgs: '{"path": "/foo.tx' })
                );
+
                expect(result).toBeNull();
        });
 
@@ -139,6 +143,7 @@ describe('parseToolArgs (shared)', () => {
                        BuiltInTool.READ_FILE,
                        makeSection({ toolArgs: '{"path":"/foo.txt"}' })
                );
+
                expect(result).toEqual({ path: '/foo.txt' });
        });
 
@@ -148,6 +153,7 @@ describe('parseToolArgs (shared)', () => {
                        makeSection({ toolArgs: '{"path": "/foo.tx' }),
                        { partial: true }
                );
+
                expect(result).toEqual({ path: '/foo.tx' });
        });
 });
@@ -156,7 +162,7 @@ describe('parseWriteFileMeta', () => {
        it('returns null for sections with a different tool name', () => {
                expect(
                        parseWriteFileMeta(
-                               makeSection({ toolName: BuiltInTool.READ_FILE, toolArgs: '{"path":"/x","content":"y"}' })
+                               makeSection({ toolArgs: '{"path":"/x","content":"y"}', toolName: BuiltInTool.READ_FILE })
                        )
                ).toBeNull();
        });
@@ -164,15 +170,16 @@ describe('parseWriteFileMeta', () => {
        it('returns null when args have no path-like field', () => {
                expect(
                        parseWriteFileMeta(
-                               makeSection({ toolName: BuiltInTool.WRITE_FILE, toolArgs: '{"content":"x"}' })
+                               makeSection({ toolArgs: '{"content":"x"}', toolName: BuiltInTool.WRITE_FILE })
                        )
                ).toBeNull();
        });
 
        it('accepts partial args (renders incrementally as content streams in)', () => {
                const meta = parseWriteFileMeta(
-                       makeSection({ toolName: BuiltInTool.WRITE_FILE, toolArgs: '{"path":"/foo.t' })
+                       makeSection({ toolArgs: '{"path":"/foo.t', toolName: BuiltInTool.WRITE_FILE })
                );
+
                expect(meta?.filePath).toBe('/foo.t');
        });
 
@@ -180,18 +187,19 @@ describe('parseWriteFileMeta', () => {
                const meta = parseWriteFileMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.WRITE_FILE,
                                        toolArgs: '{"path":"/foo.ts","content":"x"}',
+                                       toolName: BuiltInTool.WRITE_FILE,
                                        toolResult: '{"result":"wrote","bytes":42}'
                                },
                                BuiltInTool.WRITE_FILE
                        )
                );
+
                expect(meta).toMatchObject<Partial<WriteFileMeta>>({
+                       bytesWritten: 42,
+                       content: 'x',
                        filePath: '/foo.ts',
                        language: expect.any(String),
-                       content: 'x',
-                       bytesWritten: 42,
                        resultMessage: 'wrote'
                });
        });
@@ -199,11 +207,12 @@ describe('parseWriteFileMeta', () => {
        it('surfaces errorMessage from the result blob', () => {
                const meta = parseWriteFileMeta(
                        makeSection({
-                               toolName: BuiltInTool.WRITE_FILE,
                                toolArgs: '{"path":"/foo","content":"x"}',
+                               toolName: BuiltInTool.WRITE_FILE,
                                toolResult: '{"error":"permission denied"}'
                        })
                );
+
                expect(meta?.errorMessage).toBe('permission denied');
        });
 });
@@ -212,17 +221,18 @@ describe('parseEditFileMeta', () => {
        it('parses edits array and applies editsApplied from the result', () => {
                const section = makeSection(
                        {
-                               toolName: BuiltInTool.EDIT_FILE,
                                toolArgs:
                                        '{"path":"/foo.ts","edits":[{"old_text":"a","new_text":"b"},{"old_text":"c","new_text":"d"}]}',
+                               toolName: BuiltInTool.EDIT_FILE,
                                toolResult: '{"result":"ok","edits_applied":2}'
                        },
                        BuiltInTool.EDIT_FILE
                );
                const meta = parseEditFileMeta(section);
+
                expect(meta?.edits).toEqual([
-                       { oldText: 'a', newText: 'b' },
-                       { oldText: 'c', newText: 'd' }
+                       { newText: 'b', oldText: 'a' },
+                       { newText: 'd', oldText: 'c' }
                ]);
                expect(meta?.editsApplied).toBe(2);
                expect(meta?.resultMessage).toBe('ok');
@@ -231,27 +241,29 @@ describe('parseEditFileMeta', () => {
        it('drops edits with empty old_text', () => {
                const section = makeSection(
                        {
-                               toolName: BuiltInTool.EDIT_FILE,
-                               toolArgs: '{"path":"/foo","edits":[{"old_text":""},{"old_text":"a","new_text":""}]}'
+                               toolArgs: '{"path":"/foo","edits":[{"old_text":""},{"old_text":"a","new_text":""}]}',
+                               toolName: BuiltInTool.EDIT_FILE
                        },
                        BuiltInTool.EDIT_FILE
                );
                const meta = parseEditFileMeta(section);
+
                // First entry is dropped (empty old_text). Second is kept
                // (empty new_text is fine - it's the "delete" case).
-               expect(meta?.edits).toEqual([{ oldText: 'a', newText: '' }]);
+               expect(meta?.edits).toEqual([{ newText: '', oldText: 'a' }]);
        });
 
        it('errorMessage wins over result message', () => {
                const section = makeSection(
                        {
-                               toolName: BuiltInTool.EDIT_FILE,
                                toolArgs: '{"path":"/foo"}',
+                               toolName: BuiltInTool.EDIT_FILE,
                                toolResult: '{"error":"bad path","result":"ok"}'
                        },
                        BuiltInTool.EDIT_FILE
                );
                const meta = parseEditFileMeta(section);
+
                expect(meta?.errorMessage).toBe('bad path');
                expect(meta?.resultMessage).toBeUndefined();
        });
@@ -262,6 +274,7 @@ describe('parseReadFileMeta', () => {
                const meta = parseReadFileMeta(
                        makeSection({ toolArgs: '{"path":"/foo.txt"}' }, BuiltInTool.READ_FILE)
                );
+
                expect(meta?.fileName).toBe('foo.txt');
                expect(meta?.lineRange).toBeNull();
        });
@@ -273,7 +286,8 @@ describe('parseReadFileMeta', () => {
                                BuiltInTool.READ_FILE
                        )
                );
-               expect(meta?.lineRange).toEqual({ start: 10, end: 20 });
+
+               expect(meta?.lineRange).toEqual({ end: 20, start: 10 });
        });
 
        it('parses start_line + line_count into a range', () => {
@@ -283,7 +297,8 @@ describe('parseReadFileMeta', () => {
                                BuiltInTool.READ_FILE
                        )
                );
-               expect(meta?.lineRange).toEqual({ start: 10, end: 14 });
+
+               expect(meta?.lineRange).toEqual({ end: 14, start: 10 });
        });
 
        it('returns null when args cannot be parsed', () => {
@@ -295,12 +310,12 @@ describe('parseGrepSearchMeta', () => {
        it('returns null when path or pattern is missing', () => {
                expect(
                        parseGrepSearchMeta(
-                               makeSection({ toolName: BuiltInTool.GREP_SEARCH, toolArgs: '{"pattern":"foo"}' })
+                               makeSection({ toolArgs: '{"pattern":"foo"}', toolName: BuiltInTool.GREP_SEARCH })
                        )
                ).toBeNull();
                expect(
                        parseGrepSearchMeta(
-                               makeSection({ toolName: BuiltInTool.GREP_SEARCH, toolArgs: '{"path":"/x"}' })
+                               makeSection({ toolArgs: '{"path":"/x"}', toolName: BuiltInTool.GREP_SEARCH })
                        )
                ).toBeNull();
        });
@@ -309,28 +324,30 @@ describe('parseGrepSearchMeta', () => {
                const meta = parseGrepSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolArgs: '{"path":"/x","pattern":"foo"}',
+                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolResult: JSON.stringify({ plain_text_response: 'a.ts:hello\nb.ts:world' })
                                },
                                BuiltInTool.GREP_SEARCH
                        )
                );
+
                expect(meta?.matches).toHaveLength(2);
-               expect(meta?.matches[0]).toEqual({ file: 'a.ts', content: 'hello' });
+               expect(meta?.matches[0]).toEqual({ content: 'hello', file: 'a.ts' });
        });
 
        it('falls back to raw-text parsing when result is not JSON', () => {
                const meta = parseGrepSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolArgs: '{"path":"/x","pattern":"foo"}',
+                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolResult: 'a.ts:hello\nb.ts:world'
                                },
                                BuiltInTool.GREP_SEARCH
                        )
                );
+
                expect(meta?.matches).toHaveLength(2);
        });
 
@@ -338,14 +355,15 @@ describe('parseGrepSearchMeta', () => {
                const meta = parseGrepSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolArgs: '{"path":"/x","pattern":"foo","return_line_numbers":true}',
+                                       toolName: BuiltInTool.GREP_SEARCH,
                                        toolResult: 'a.ts:12:hello'
                                },
                                BuiltInTool.GREP_SEARCH
                        )
                );
-               expect(meta?.matches[0]).toEqual({ file: 'a.ts', line: 12, content: 'hello' });
+
+               expect(meta?.matches[0]).toEqual({ content: 'hello', file: 'a.ts', line: 12 });
                expect(meta?.showLineNumbers).toBe(true);
        });
 });
@@ -355,13 +373,14 @@ describe('parseFileGlobSearchMeta', () => {
                const meta = parseFileGlobSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolArgs: '{"path":"/x"}',
+                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolResult: 'a.ts\nb.ts'
                                },
                                BuiltInTool.FILE_GLOB_SEARCH
                        )
                );
+
                expect(meta?.matches).toEqual(['a.ts', 'b.ts']);
        });
 
@@ -369,13 +388,14 @@ describe('parseFileGlobSearchMeta', () => {
                const meta = parseFileGlobSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolArgs: '{"path":"/x"}',
+                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolResult: JSON.stringify({ plain_text_response: 'a.ts\nb.ts' })
                                },
                                BuiltInTool.FILE_GLOB_SEARCH
                        )
                );
+
                expect(meta?.matches).toEqual(['a.ts', 'b.ts']);
        });
 
@@ -383,13 +403,14 @@ describe('parseFileGlobSearchMeta', () => {
                const meta = parseFileGlobSearchMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolArgs: '{"path":"/x"}',
+                                       toolName: BuiltInTool.FILE_GLOB_SEARCH,
                                        toolResult: JSON.stringify({ error: 'permission denied' })
                                },
                                BuiltInTool.FILE_GLOB_SEARCH
                        )
                );
+
                expect(meta?.errorMessage).toBe('permission denied');
        });
 });
@@ -397,17 +418,18 @@ describe('parseFileGlobSearchMeta', () => {
 describe('parseRunJavascriptMeta', () => {
        it('returns null when code is missing', () => {
                expect(
-                       parseRunJavascriptMeta(makeSection({ toolName: BuiltInTool.RUN_JAVASCRIPT, toolArgs: '{}' }))
+                       parseRunJavascriptMeta(makeSection({ toolArgs: '{}', toolName: BuiltInTool.RUN_JAVASCRIPT }))
                ).toBeNull();
        });
 
        it('reads code and timeout', () => {
                const meta = parseRunJavascriptMeta(
                        makeSection(
-                               { toolName: BuiltInTool.RUN_JAVASCRIPT, toolArgs: '{"code":"Math.PI","timeout_ms":5000}' },
+                               { toolArgs: '{"code":"Math.PI","timeout_ms":5000}', toolName: BuiltInTool.RUN_JAVASCRIPT },
                                BuiltInTool.RUN_JAVASCRIPT
                        )
                );
+
                expect(meta?.code).toBe('Math.PI');
                expect(meta?.timeoutMs).toBe(5000);
        });
@@ -416,13 +438,14 @@ describe('parseRunJavascriptMeta', () => {
                const meta = parseRunJavascriptMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolArgs: '{"code":"throw new Error()"}',
+                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolResult: JSON.stringify({ error: 'undefined is not a function' })
                                },
                                BuiltInTool.RUN_JAVASCRIPT
                        )
                );
+
                expect(meta?.errorMessage).toBe('undefined is not a function');
        });
 
@@ -433,13 +456,14 @@ describe('parseRunJavascriptMeta', () => {
                const meta = parseRunJavascriptMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolArgs: '{"code":"[1,2,3]"}',
+                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolResult: '[1,2,3]'
                                },
                                BuiltInTool.RUN_JAVASCRIPT
                        )
                );
+
                expect(meta?.errorMessage).toBeUndefined();
        });
 
@@ -447,13 +471,14 @@ describe('parseRunJavascriptMeta', () => {
                const meta = parseRunJavascriptMeta(
                        makeSection(
                                {
-                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolArgs: '{"code":"foo"}',
+                                       toolName: BuiltInTool.RUN_JAVASCRIPT,
                                        toolResult: 'Error: undefined is not a function\n  at <anonymous>:1:1'
                                },
                                BuiltInTool.RUN_JAVASCRIPT
                        )
                );
+
                expect(meta?.errorMessage).toBe('undefined is not a function');
        });
 });
@@ -462,10 +487,11 @@ describe('parseExecShellCommandMeta', () => {
        it('reads command from the args', () => {
                const meta = parseExecShellCommandMeta(
                        makeSection(
-                               { toolName: BuiltInTool.EXEC_SHELL_COMMAND, toolArgs: '{"command":"ls -la"}' },
+                               { toolArgs: '{"command":"ls -la"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
                                BuiltInTool.EXEC_SHELL_COMMAND
                        )
                );
+
                expect(meta?.command).toBe('ls -la');
        });
 
@@ -473,7 +499,7 @@ describe('parseExecShellCommandMeta', () => {
                expect(
                        parseExecShellCommandMeta(
                                makeSection(
-                                       { toolName: BuiltInTool.EXEC_SHELL_COMMAND, toolArgs: '{"cmd":"ls"}' },
+                                       { toolArgs: '{"cmd":"ls"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
                                        BuiltInTool.EXEC_SHELL_COMMAND
                                )
                        )?.command
@@ -481,7 +507,7 @@ describe('parseExecShellCommandMeta', () => {
                expect(
                        parseExecShellCommandMeta(
                                makeSection(
-                                       { toolName: BuiltInTool.EXEC_SHELL_COMMAND, toolArgs: '{"shell_command":"ls"}' },
+                                       { toolArgs: '{"shell_command":"ls"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
                                        BuiltInTool.EXEC_SHELL_COMMAND
                                )
                        )?.command
@@ -492,7 +518,7 @@ describe('parseExecShellCommandMeta', () => {
                expect(
                        parseExecShellCommandMeta(
                                makeSection(
-                                       { toolName: BuiltInTool.EXEC_SHELL_COMMAND, toolArgs: '{"cwd":"/x"}' },
+                                       { toolArgs: '{"cwd":"/x"}', toolName: BuiltInTool.EXEC_SHELL_COMMAND },
                                        BuiltInTool.EXEC_SHELL_COMMAND
                                )
                        )
index 23645af3f5fc50ab1a25bc931c366d315d13e3c8..4b52c1a9c33c34f799fe072be32e8b141853d53b 100644 (file)
@@ -1,20 +1,22 @@
-import { describe, it, expect } from 'vitest';
+import { URI_TEMPLATE_OPERATORS } from '../../src/lib/constants/uri-template';
 import {
-       extractTemplateVariables,
        expandTemplate,
+       extractTemplateVariables,
        isTemplateComplete,
        normalizeResourceUri
 } from '../../src/lib/utils/uri-template';
-import { URI_TEMPLATE_OPERATORS } from '../../src/lib/constants/uri-template';
+import { describe, expect, it } from 'vitest';
 
 describe('extractTemplateVariables', () => {
        it('extracts simple variables', () => {
                const vars = extractTemplateVariables('file:///{path}');
+
                expect(vars).toEqual([{ name: 'path', operator: '' }]);
        });
 
        it('extracts multiple variables', () => {
                const vars = extractTemplateVariables('db://{schema}/{table}');
+
                expect(vars).toEqual([
                        { name: 'schema', operator: '' },
                        { name: 'table', operator: '' }
@@ -23,11 +25,13 @@ describe('extractTemplateVariables', () => {
 
        it('extracts variables with operators', () => {
                const vars = extractTemplateVariables('http://example.com{+path}');
+
                expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_OPERATORS.RESERVED }]);
        });
 
        it('extracts comma-separated variable lists', () => {
                const vars = extractTemplateVariables('{x,y,z}');
+
                expect(vars).toEqual([
                        { name: 'x', operator: '' },
                        { name: 'y', operator: '' },
@@ -37,31 +41,37 @@ describe('extractTemplateVariables', () => {
 
        it('deduplicates variable names', () => {
                const vars = extractTemplateVariables('{name}/{name}');
+
                expect(vars).toEqual([{ name: 'name', operator: '' }]);
        });
 
        it('handles fragment expansion', () => {
                const vars = extractTemplateVariables('http://example.com/page{#section}');
+
                expect(vars).toEqual([{ name: 'section', operator: URI_TEMPLATE_OPERATORS.FRAGMENT }]);
        });
 
        it('handles path segment expansion', () => {
                const vars = extractTemplateVariables('http://example.com{/path}');
+
                expect(vars).toEqual([{ name: 'path', operator: URI_TEMPLATE_OPERATORS.PATH_SEGMENT }]);
        });
 
        it('returns empty array for template without variables', () => {
                const vars = extractTemplateVariables('http://example.com/static');
+
                expect(vars).toEqual([]);
        });
 
        it('strips explode modifier', () => {
                const vars = extractTemplateVariables('{list*}');
+
                expect(vars).toEqual([{ name: 'list', operator: '' }]);
        });
 
        it('strips prefix modifier', () => {
                const vars = extractTemplateVariables('{value:5}');
+
                expect(vars).toEqual([{ name: 'value', operator: '' }]);
        });
 });
@@ -69,11 +79,13 @@ describe('extractTemplateVariables', () => {
 describe('expandTemplate', () => {
        it('expands simple variable', () => {
                const result = expandTemplate('file:///{path}', { path: 'src/main.rs' });
+
                expect(result).toBe('file:///src%2Fmain.rs');
        });
 
        it('expands reserved variable (no encoding)', () => {
                const result = expandTemplate('file:///{+path}', { path: 'src/main.rs' });
+
                expect(result).toBe('file:///src/main.rs');
        });
 
@@ -82,11 +94,13 @@ describe('expandTemplate', () => {
                        schema: 'public',
                        table: 'users'
                });
+
                expect(result).toBe('db://public/users');
        });
 
        it('leaves empty for missing variables', () => {
                const result = expandTemplate('{missing}', {});
+
                expect(result).toBe('');
        });
 
@@ -94,16 +108,19 @@ describe('expandTemplate', () => {
                const result = expandTemplate('http://example.com/page{#section}', {
                        section: 'intro'
                });
+
                expect(result).toBe('http://example.com/page#intro');
        });
 
        it('expands path segments', () => {
                const result = expandTemplate('http://example.com{/path}', { path: 'docs' });
+
                expect(result).toBe('http://example.com/docs');
        });
 
        it('expands query parameters', () => {
                const result = expandTemplate('http://example.com{?q}', { q: 'search term' });
+
                expect(result).toBe('http://example.com?q=search%20term');
        });
 
@@ -112,11 +129,13 @@ describe('expandTemplate', () => {
                        q: 'search term',
                        sort: 'descending'
                });
+
                expect(result).toBe('http://example.com?q=search%20term&sort=descending');
        });
 
        it('keeps static parts unchanged', () => {
                const result = expandTemplate('http://example.com/static', {});
+
                expect(result).toBe('http://example.com/static');
        });
 });
index 7481c154d06e62e80b79cc0c1ef487380ea7b8d4..35b15c5c5f554d5253b186e964cce21750988634 100644 (file)
@@ -1,13 +1,13 @@
-import { describe, expect, it } from 'vitest';
+import { GLOB_WILDCARD, PATH_NAV_MAX_DEPTH } from '$lib/constants';
 import {
-       splitPathQuery,
        buildCaseInsensitiveGlob,
        buildGlobSearchArgs,
-       rankEntries,
+       highlightMatch,
        joinPath,
-       highlightMatch
+       rankEntries,
+       splitPathQuery
 } from '$lib/utils';
-import { GLOB_WILDCARD, PATH_NAV_MAX_DEPTH } from '$lib/constants';
+import { describe, expect, it } from 'vitest';
 
 describe('splitPathQuery', () => {
        it('treats a plain query as a home-relative glob (not navigation)', () => {
@@ -15,54 +15,54 @@ describe('splitPathQuery', () => {
        });
 
        it('navigates the root for `/`', () => {
-               expect(splitPathQuery('/')).toEqual({ parent: '/', last: '' });
+               expect(splitPathQuery('/')).toEqual({ last: '', parent: '/' });
        });
 
        it('navigates home for `~`', () => {
-               expect(splitPathQuery('~')).toEqual({ parent: '~', last: '' });
+               expect(splitPathQuery('~')).toEqual({ last: '', parent: '~' });
        });
 
        it('splits an absolute path into parent and last segment', () => {
-               expect(splitPathQuery('/Users/al/proj')).toEqual({ parent: '/Users/al', last: 'proj' });
+               expect(splitPathQuery('/Users/al/proj')).toEqual({ last: 'proj', parent: '/Users/al' });
        });
 
        it('navigates a Windows drive path written with backslashes', () => {
                expect(splitPathQuery('C:\\repos\\llama.cpp')).toEqual({
-                       parent: 'C:/repos',
-                       last: 'llama.cpp'
+                       last: 'llama.cpp',
+                       parent: 'C:/repos'
                });
        });
 
        it('navigates a Windows drive path written with forward slashes', () => {
-               expect(splitPathQuery('D:/repos')).toEqual({ parent: 'D:/', last: 'repos' });
+               expect(splitPathQuery('D:/repos')).toEqual({ last: 'repos', parent: 'D:/' });
        });
 
        it('treats a bare drive as its root', () => {
-               expect(splitPathQuery('D:')).toEqual({ parent: 'D:/', last: '' });
-               expect(splitPathQuery('D:\\')).toEqual({ parent: 'D:/', last: '' });
+               expect(splitPathQuery('D:')).toEqual({ last: '', parent: 'D:/' });
+               expect(splitPathQuery('D:\\')).toEqual({ last: '', parent: 'D:/' });
        });
 
        it('navigates a UNC share', () => {
                expect(splitPathQuery('\\\\host\\share\\proj')).toEqual({
-                       parent: '//host/share/',
-                       last: 'proj'
+                       last: 'proj',
+                       parent: '//host/share/'
                });
        });
 
        it('keeps a backslash as a POSIX filename character', () => {
-               expect(splitPathQuery('/tmp/a\\b')).toEqual({ parent: '/tmp', last: 'a\\b' });
+               expect(splitPathQuery('/tmp/a\\b')).toEqual({ last: 'a\\b', parent: '/tmp' });
        });
 
        it('splits a home-relative path into parent and last segment', () => {
-               expect(splitPathQuery('~/Documents')).toEqual({ parent: '~', last: 'Documents' });
+               expect(splitPathQuery('~/Documents')).toEqual({ last: 'Documents', parent: '~' });
        });
 
        it('strips trailing slashes before splitting', () => {
-               expect(splitPathQuery('/Users/al/')).toEqual({ parent: '/Users', last: 'al' });
+               expect(splitPathQuery('/Users/al/')).toEqual({ last: 'al', parent: '/Users' });
        });
 
        it('handles a single-segment absolute path', () => {
-               expect(splitPathQuery('/opt')).toEqual({ parent: '/', last: 'opt' });
+               expect(splitPathQuery('/opt')).toEqual({ last: 'opt', parent: '/' });
        });
 });
 
@@ -85,16 +85,19 @@ describe('rankEntries', () => {
 
        it('ranks exact basename match first', () => {
                const ranked = rankEntries(entries, 'read');
+
                expect(ranked[0].path).toBe('/h/read');
        });
 
        it('breaks ties by shorter path, then alphabetically', () => {
                const ranked = rankEntries(entries, 'read');
+
                expect(ranked[ranked.length - 1].path).toBe('/h/readme.txt');
        });
 
        it('does not mutate the input', () => {
                const snapshot = [...entries];
+
                rankEntries(entries, 'read');
                expect(entries).toEqual(snapshot);
        });
@@ -112,18 +115,18 @@ describe('joinPath', () => {
 
 describe('highlightMatch', () => {
        it('returns a single non-matching segment when query is empty', () => {
-               expect(highlightMatch('abc', '')).toEqual([{ text: 'abc', match: false }]);
+               expect(highlightMatch('abc', '')).toEqual([{ match: false, text: 'abc' }]);
        });
 
        it('marks every case-insensitive occurrence of the query', () => {
                expect(highlightMatch('aXa', 'ax')).toEqual([
-                       { text: 'aX', match: true },
-                       { text: 'a', match: false }
+                       { match: true, text: 'aX' },
+                       { match: false, text: 'a' }
                ]);
        });
 
        it('returns non-matching text when the query is absent', () => {
-               expect(highlightMatch('abc', 'z')).toEqual([{ text: 'abc', match: false }]);
+               expect(highlightMatch('abc', 'z')).toEqual([{ match: false, text: 'abc' }]);
        });
 });
 
@@ -132,6 +135,7 @@ describe('buildGlobSearchArgs', () => {
 
        it('glob-matches home-relative within the scope path', () => {
                const args = buildGlobSearchArgs('docs', '/home', DEPTH);
+
                expect(args.path).toBe('/home');
                expect(args.include).toBe(buildCaseInsensitiveGlob('docs'));
                expect(args.maxDepth).toBe(DEPTH);
@@ -141,6 +145,7 @@ describe('buildGlobSearchArgs', () => {
 
        it('navigates home for a `~` path query', () => {
                const args = buildGlobSearchArgs('~/proj', '/home', DEPTH);
+
                expect(args.path).toBe('~');
                expect(args.include).toBe(buildCaseInsensitiveGlob('proj'));
                expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
@@ -150,6 +155,7 @@ describe('buildGlobSearchArgs', () => {
 
        it('lists the scope root when a path query has no last segment', () => {
                const args = buildGlobSearchArgs('~/', '/home', DEPTH);
+
                expect(args.path).toBe('~');
                expect(args.include).toBe(GLOB_WILDCARD);
                expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
@@ -157,6 +163,7 @@ describe('buildGlobSearchArgs', () => {
 
        it('navigates an absolute path under its root', () => {
                const args = buildGlobSearchArgs('/usr/local/bin', '/home', DEPTH);
+
                expect(args.path).toBe('/usr/local');
                expect(args.include).toBe(buildCaseInsensitiveGlob('bin'));
                expect(args.maxDepth).toBe(PATH_NAV_MAX_DEPTH);
index 91afeacf87d96e4465e1ebe3d377443cbce404cc..8f80d8ec08f0f7b2c54a2912af2f766ada5e6f32 100644 (file)
@@ -1,40 +1,31 @@
-import tailwindcss from '@tailwindcss/vite';
+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 { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
 import { sveltekit } from '@sveltejs/kit/vite';
+import tailwindcss from '@tailwindcss/vite';
 import { SvelteKitPWA } from '@vite-pwa/sveltekit';
+import { playwright } from '@vitest/browser-playwright';
 import { dirname, resolve } from 'path';
 import { fileURLToPath } from 'url';
-
 import { defineConfig, searchForWorkspaceRoot } from 'vite';
-import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
-import { splashScreenPlugin } from './scripts/vite-plugin-splash-screen';
-import { buildInfoPlugin } from './scripts/vite-plugin-build-info';
-import { relativizeBasePlugin } from './scripts/vite-plugin-relativize-base';
-import { nerdamerPlugin } from './scripts/vite-plugin-nerdamer';
-import { playwright } from '@vitest/browser-playwright';
-import { SVELTEKIT_PWA_OPTIONS } from './src/lib/constants/pwa';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
-
 const SERVER_ORIGIN = import.meta.env?.VITE_PUBLIC_SERVER_ORIGIN || 'http://localhost:8080';
-
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const browserBaseConfig: any = {
        enabled: true,
+       instances: [{ browser: 'chromium' }],
        provider: playwright({
                launchOptions: {
                        args: ['--no-sandbox']
                }
-       }),
-       instances: [{ browser: 'chromium' }]
+       })
 };
 
 export default defineConfig({
-       resolve: {
-               alias: {
-                       'katex-fonts': resolve('node_modules/katex/dist/fonts')
-               }
-       },
-
        build: {
                assetsInlineLimit: 32000,
                chunkSizeWarningLimit: 3072,
@@ -51,14 +42,38 @@ export default defineConfig({
                relativizeBasePlugin()
        ],
 
+       resolve: {
+               alias: {
+                       'katex-fonts': resolve('node_modules/katex/dist/fonts')
+               }
+       },
+
+       server: {
+               fs: {
+                       allow: [searchForWorkspaceRoot(process.cwd()), resolve(__dirname, 'tests')]
+               },
+               headers: {
+                       'Cross-Origin-Embedder-Policy': 'require-corp',
+                       'Cross-Origin-Opener-Policy': 'same-origin'
+               },
+               proxy: {
+                       '/cors-proxy': SERVER_ORIGIN,
+                       '/models': SERVER_ORIGIN,
+                       '/props': SERVER_ORIGIN,
+                       '/slots': SERVER_ORIGIN,
+                       '/tools': SERVER_ORIGIN,
+                       '/v1': SERVER_ORIGIN
+               }
+       },
+
        test: {
                projects: [
                        {
                                extends: './vite.config.ts',
                                test: {
-                                       name: 'client',
                                        browser: browserBaseConfig,
                                        include: ['tests/client/**/*.svelte.{test,spec}.{js,ts}'],
+                                       name: 'client',
                                        setupFiles: ['./vitest-setup-client.ts']
                                }
                        },
@@ -66,42 +81,24 @@ export default defineConfig({
                        {
                                extends: './vite.config.ts',
                                test: {
-                                       name: 'unit',
                                        environment: 'node',
-                                       include: ['tests/unit/**/*.{test,spec}.{js,ts}']
+                                       include: ['tests/unit/**/*.{test,spec}.{js,ts}'],
+                                       name: 'unit'
                                }
                        },
 
                        {
                                extends: './vite.config.ts',
-                               test: {
-                                       name: 'ui',
-                                       browser: { ...browserBaseConfig, instances: [{ browser: 'chromium', headless: true }] }
-                               },
                                plugins: [
                                        storybookTest({
                                                storybookScript: 'pnpm run storybook --no-open'
                                        })
-                               ]
+                               ],
+                               test: {
+                                       browser: { ...browserBaseConfig, instances: [{ browser: 'chromium', headless: true }] },
+                                       name: 'ui'
+                               }
                        }
                ]
-       },
-
-       server: {
-               proxy: {
-                       '/v1': SERVER_ORIGIN,
-                       '/props': SERVER_ORIGIN,
-                       '/models': SERVER_ORIGIN,
-                       '/tools': SERVER_ORIGIN,
-                       '/slots': SERVER_ORIGIN,
-                       '/cors-proxy': SERVER_ORIGIN
-               },
-               headers: {
-                       'Cross-Origin-Embedder-Policy': 'require-corp',
-                       'Cross-Origin-Opener-Policy': 'same-origin'
-               },
-               fs: {
-                       allow: [searchForWorkspaceRoot(process.cwd()), resolve(__dirname, 'tests')]
-               }
        }
 });
index e76b8df8be8a19db42817e9f4d66c35154ab9bb2..06d275c25fa9451db1f1a2b4956f521b6bb9f4e6 100644 (file)
@@ -21,12 +21,12 @@ beforeEach(() => {
                        if (url.includes('/server')) {
                                return new Response(
                                        JSON.stringify({
-                                               mode: 'router',
-                                               version: 'test',
+                                               git_branch: 'test',
                                                git_commit: 'test',
-                                               git_branch: 'test'
+                                               mode: 'router',
+                                               version: 'test'
                                        }),
-                                       { status: 200, headers: { 'Content-Type': 'application/json' } }
+                                       { headers: { 'Content-Type': 'application/json' }, status: 200 }
                                );
                        }
 
@@ -34,28 +34,28 @@ beforeEach(() => {
                        if (/\/v1\/models|\/models\b/.test(url)) {
                                return new Response(
                                        JSON.stringify({
-                                               object: 'list',
                                                data: [
                                                        {
+                                                               created: 0,
                                                                id: 'test-model.gguf',
+                                                               in_cache: false,
+                                                               meta: {},
                                                                object: 'model',
                                                                owned_by: 'llamacpp',
-                                                               created: 0,
-                                                               in_cache: false,
                                                                path: 'models/test-model.gguf',
-                                                               status: { value: 'unloaded' },
-                                                               meta: {}
+                                                               status: { value: 'unloaded' }
                                                        }
                                                ],
                                                models: [
                                                        {
+                                                               details: {},
                                                                model: 'test-model.gguf',
-                                                               name: 'Test Model',
-                                                               details: {}
+                                                               name: 'Test Model'
                                                        }
-                                               ]
+                                               ],
+                                               object: 'list'
                                        }),
-                                       { status: 200, headers: { 'Content-Type': 'application/json' } }
+                                       { headers: { 'Content-Type': 'application/json' }, status: 200 }
                                );
                        }
 
@@ -65,15 +65,15 @@ beforeEach(() => {
                                        JSON.stringify({
                                                default_generation_settings: { n_ctx: 2048 }
                                        }),
-                                       { status: 200, headers: { 'Content-Type': 'application/json' } }
+                                       { headers: { 'Content-Type': 'application/json' }, status: 200 }
                                );
                        }
 
                        // Mock /tools endpoint (used for built-in tools list)
                        if (url.includes('/tools')) {
                                return new Response(JSON.stringify([]), {
-                                       status: 200,
-                                       headers: { 'Content-Type': 'application/json' }
+                                       headers: { 'Content-Type': 'application/json' },
+                                       status: 200
                                });
                        }