]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: Stores architecture improvements (#26910)
authorAleksander Grygier <redacted>
Thu, 13 Aug 2026 06:11:30 +0000 (08:11 +0200)
committerGitHub <redacted>
Thu, 13 Aug 2026 06:11:30 +0000 (08:11 +0200)
* refactor: Stores barrel imports + SSR gates

* refactor: Drop agenticStore wrapper exports

* refactor: Drop chatStore wrapper exports

* refactor: Drop modelsStore wrapper exports

* refactor: Drop serverStore wrapper exports

* refactor: Drop unused mcpStore wrapper exports

* refactor: Drop mcpResourceStore wrapper exports

* refactor: Drop conversationsStore wrapper exports + move buildConversationTree to utils

* refactor: Drop settingsStore wrapper exports

* refactor: Drop unused toolsStore wrapper exports

* refactor: Fix lint errors from store wrapper removal

* fix: Missing change

* refactor: Cleanup

* refactor: Context Stats store

116 files changed:
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpResource.svelte
tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpServersSubmenu.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/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/ContextGaugePopup.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormMcpResourcesList.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/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.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/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/ChatMessageAssistantStatistics.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageMcpPrompt/ChatMessageMcpPromptContent.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.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/ChatMessageToolCallBlockGetInfo.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.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/ChatMessageUser/ChatMessageUser.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserBubble.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/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/ChatMessages.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte
tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenGreeting.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/MarkdownContent/MarkdownContent.svelte
tools/ui/src/lib/components/app/content/MarkdownContent/plugins/rehype/file-badge.ts
tools/ui/src/lib/components/app/content/MentionBadge.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/DialogModelInformation.svelte
tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte
tools/ui/src/lib/components/app/mcp/McpResourcePreview.svelte
tools/ui/src/lib/components/app/mcp/McpResourceTemplateForm.svelte
tools/ui/src/lib/components/app/mcp/McpResourcesBrowser/McpResourcesBrowser.svelte
tools/ui/src/lib/components/app/mcp/McpResourcesBrowser/McpResourcesBrowserServerItem.svelte
tools/ui/src/lib/components/app/mcp/McpServerCard/McpServerCard.svelte
tools/ui/src/lib/components/app/mcp/McpServerForm.svelte
tools/ui/src/lib/components/app/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/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/SidebarNavigationSearchResults.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/SettingsChatImportExportTab.svelte
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChatToolsTab.svelte
tools/ui/src/lib/components/app/settings/SettingsFooter.svelte
tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte
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-context-gauge.svelte.ts
tools/ui/src/lib/hooks/use-draft-messages.svelte.ts
tools/ui/src/lib/hooks/use-models-selector.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-settings-navigation.svelte.ts
tools/ui/src/lib/hooks/use-tools-panel.svelte.ts
tools/ui/src/lib/services/sandbox.service.ts
tools/ui/src/lib/stores/agentic.svelte.ts
tools/ui/src/lib/stores/chat.svelte.ts
tools/ui/src/lib/stores/context-stats.svelte.ts [new file with mode: 0644]
tools/ui/src/lib/stores/conversations.svelte.ts
tools/ui/src/lib/stores/index.ts [new file with mode: 0644]
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/server.svelte.ts
tools/ui/src/lib/stores/settings.svelte.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/types/agentic.d.ts
tools/ui/src/lib/utils/api-headers.ts
tools/ui/src/lib/utils/api-key-validation.ts
tools/ui/src/lib/utils/conversation-utils.ts
tools/ui/src/lib/utils/convert-files-to-extra.ts
tools/ui/src/lib/utils/index.ts
tools/ui/src/routes/(chat)/+page.svelte
tools/ui/src/routes/(chat)/chat/[id]/+page.svelte
tools/ui/src/routes/+layout.svelte
tools/ui/src/routes/search/+page.svelte
tools/ui/tests/client/settings-registry-invariants.svelte.test.ts
tools/ui/tests/client/settings-render-keys-migration.svelte.test.ts
tools/ui/tests/client/ui-settings-sync.svelte.test.ts

index 486b15b3b124d8db3e075a0af562f59202bd7515..80ef25bbcbe949eaa01fd756b4582cc775df324f 100644 (file)
@@ -3,7 +3,7 @@
        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 { mcpStore } from '$lib/stores';
        import type { MCPResourceAttachment } from '$lib/types';
        import { getResourceDisplayName, getResourceIcon } from '$lib/utils';
 
index c161e5524796cdd42180846b8d900b763949de3e..0efe052b1f19bf0b40f616e36d6ebeee4607f5a6 100644 (file)
@@ -5,7 +5,7 @@
                ChatAttachmentsPreviewNavButtons,
                ChatAttachmentsPreviewThumbnailStrip
        } from '$lib/components/app';
-       import { modelsStore } from '$lib/stores/models.svelte';
+       import { modelsStore } from '$lib/stores';
        import {
                createBase64DataUrl,
                formatFileSize,
index 9921d8d88d428c3bb48fe658eb94601ea5b2ef80..bf56e91083dbada2b8eae1bcf4d65fd08f4d4cd5 100644 (file)
                SpecialFileType
        } from '$lib/enums';
        import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
-       import { chatStore } from '$lib/stores/chat.svelte';
        import {
-               activeConversation,
-               activeMessages,
+               chatStore,
                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';
+               mcpResourceStore,
+               mcpStore,
+               modelsStore,
+               serverStore,
+               settingsStore,
+               toolsStore
+       } from '$lib/stores';
        import type {
                FileMentionEntry,
                GetPromptResult,
        // float above the box.
        let mentionAnchor: HTMLDivElement | null = $state(null);
 
-       let cwd = $derived(activeConversation()?.cwd ?? pendingCwd());
+       let cwd = $derived(conversationsStore.activeConversation?.cwd ?? conversationsStore.pendingCwd);
 
        const pickers = useChatFormPickers({
                focusInput: refocusInput,
        let isResourceDialogOpen = $state(false);
        let preSelectedResourceUri = $state<string | undefined>(undefined);
 
-       let currentConfig = $derived(config());
+       let currentConfig = $derived(settingsStore.config);
 
        let pasteLongTextToFileLength = $derived.by(() => {
                const n = Number(currentConfig.pasteLongTextToFileLen);
                return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
        });
 
-       let isRouter = $derived(isRouterMode());
+       let isRouter = $derived(serverStore.isRouterMode);
        let conversationModel = $derived(
-               chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
+               chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
        );
        let activeModelId = $derived.by(() => {
-               const options = modelOptions();
+               const options = modelsStore.models;
 
                if (!isRouter) {
                        return options.length > 0 ? options[0].model : null;
                }
 
-               const selectedId = selectedModelId();
+               const selectedId = modelsStore.selectedModelId;
 
                if (selectedId) {
                        const model = options.find((m) => m.id === selectedId);
                return null;
        });
 
-       let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
+       let hasModelSelected = $derived(
+               !isRouter || !!conversationModel || !!modelsStore.selectedModelId
+       );
        let hasLoadingAttachments = $derived(uploadedFiles.some((f) => f.isLoading));
        let hasAttachments = $derived(
                (attachments && attachments.length > 0) || (uploadedFiles && uploadedFiles.length > 0)
                                />
                        {/if}
 
-                       {#if mcpHasResourceAttachments()}
+                       {#if mcpResourceStore.hasAttachments}
                                <ChatFormMcpResourcesList
                                        class="mb-3"
                                        onResourceClick={(uri) => {
index 178345e8349da561eac8da1434e95f79fd1096cb..3d04d14cb1c77c9378b07c7485c6bfaa37f12331 100644 (file)
@@ -6,8 +6,7 @@
        import { Switch } from '$lib/components/ui/switch';
        import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
        import { HealthCheckStatus } from '$lib/enums';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { conversationsStore, mcpStore } from '$lib/stores';
        import type { MCPServerSettingsEntry } from '$lib/types';
 
        interface Props {
index 299b22209f904a0b2764a53d0b7eedec9ce392c2..aa5397ca0b301d9b79fa9877b0d303ac8b24d4c2 100644 (file)
@@ -24,8 +24,7 @@
        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 { conversationsStore, mcpStore } from '$lib/stores';
        import type { Snippet } from 'svelte';
 
        interface Props {
index ac3b5712fcc9baae60d4b874b2e50da94e3f3c1b..1204390fd81ad28393eecd4e737498a1da9ff48f 100644 (file)
@@ -6,8 +6,7 @@
        import * as Tooltip from '$lib/components/ui/tooltip';
        import { CLI_FLAGS, ICON_CLASS_DEFAULT } from '$lib/constants';
        import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { mcpStore, toolsStore } from '$lib/stores';
 
        const toolsPanel = useToolsPanel();
        const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0);
index c54471a5aea87f5a2e121b3a0ea84cd9d9207ec3..a31ffb394dddd65d48526f77780e54c6906d1b51 100644 (file)
@@ -2,7 +2,7 @@
        import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
        import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
        import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { isMobile } from '$lib/stores';
 
        interface Props {
                disabled?: boolean;
index 62befb5ddabc5edf9d6f31e2295b0c3fdc29eada..fad223a98b0c9a3522e77a9cae07962a7592d976 100644 (file)
@@ -1,15 +1,6 @@
 <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 {
-               modelOptions,
-               modelsStore,
-               selectedModelId,
-               selectedModelName
-       } from '$lib/stores/models.svelte';
-       import { isRouterMode, serverError } from '$lib/stores/server.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { chatStore, conversationsStore, isMobile, modelsStore, serverStore } from '$lib/stores';
 
        interface Props {
                disabled?: boolean;
                useGlobalSelection = false
        }: Props = $props();
 
-       let isRouter = $derived(isRouterMode());
-       let isOffline = $derived(!!serverError());
+       let isRouter = $derived(serverStore.isRouterMode);
+       let isOffline = $derived(!!serverStore.error);
 
        let conversationModel = $derived(
-               chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
+               chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
        );
 
        let lastSyncedConversationModel: string | null = null;
 
        let selectorModel = $derived.by(() => {
-               const storeModel = selectedModelName();
+               const storeModel = modelsStore.selectedModelName;
 
                if (storeModel && storeModel !== conversationModel) {
                        return storeModel;
@@ -60,7 +51,7 @@
 
        $effect(() => {
                if (conversationModel && conversationModel !== lastSyncedConversationModel) {
-                       if (modelOptions().some((m) => m.model === conversationModel)) {
+                       if (modelsStore.models.some((m) => m.model === conversationModel)) {
                                modelsStore.selectedModelName = conversationModel;
                                modelsStore.selectModelByName(conversationModel);
                        } else {
                        isRouter &&
                        !modelsStore.selectedModelId &&
                        modelsStore.loadedModelIds.length > 0 &&
-                       activeMessages().length > 0 &&
+                       conversationsStore.activeMessages.length > 0 &&
                        !conversationModel
                ) {
                        lastSyncedConversationModel = null;
-                       const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
+                       const first = modelsStore.models.find((m) => modelsStore.loadedModelIds.includes(m.model));
 
                        if (first) modelsStore.selectModelById(first.id);
                }
        });
 
        let activeModelId = $derived.by(() => {
-               const options = modelOptions();
+               const options = modelsStore.models;
 
                if (!isRouter) {
                        return options.length > 0 ? options[0].model : null;
                }
 
-               const selectedId = selectedModelId();
+               const selectedId = modelsStore.selectedModelId;
 
                if (selectedId) {
                        const model = options.find((m) => m.id === selectedId);
        });
 
        $effect(() => {
-               hasModelSelected = !isRouter || !!conversationModel || !!selectedModelId();
+               hasModelSelected = !isRouter || !!conversationModel || !!modelsStore.selectedModelId;
        });
 
        $effect(() => {
                if (!isRouter) {
                        isSelectedModelInCache = true;
                } else if (conversationModel) {
-                       isSelectedModelInCache = modelOptions().some((option) => option.model === conversationModel);
+                       isSelectedModelInCache = modelsStore.models.some(
+                               (option) => option.model === conversationModel
+                       );
                } else {
-                       const currentModelId = selectedModelId();
+                       const currentModelId = modelsStore.selectedModelId;
 
                        if (!currentModelId) {
                                isSelectedModelInCache = false;
                        } else {
-                               isSelectedModelInCache = modelOptions().some((option) => option.id === currentModelId);
+                               isSelectedModelInCache = modelsStore.models.some((option) => option.id === currentModelId);
                        }
                }
        });
index 5dcf3531ae9f8f8cb990ac94a4d02af5416f053b..13e97cc73c6fd8a5a79e03c0f20c748c796a8954 100644 (file)
        import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
        import { FileTypeCategory, MessageRole } from '$lib/enums';
        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 { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
        import { getFileTypeCategory } from '$lib/utils';
 
        interface Props {
@@ -61,7 +54,7 @@
                uploadedFiles = []
        }: Props = $props();
 
-       let currentConfig = $derived(config());
+       let currentConfig = $derived(settingsStore.config);
 
        let hasMcpPromptsSupport = $derived.by(() => {
                const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
        let hasProcessedTokens = $derived.by(() => {
                if (!page.params.id) return false;
 
-               const messages = activeMessages() as DatabaseMessage[];
+               const messages = conversationsStore.activeMessages as DatabaseMessage[];
 
                let totalHistoricalTokens = 0;
 
 
                if (totalHistoricalTokens > 0) return true;
 
-               if (!chatIsLoading() && !isChatStreaming()) return false;
+               if (!chatStore.isLoading && !chatStore.isStreaming()) return false;
 
-               const processingState = activeProcessingState();
+               const processingState = chatStore.activeProcessingState;
 
                if (!processingState) return false;
 
index 2062076cf1dc6c9ae41baa1284fceb533a6d7713..ba44304011d973bca90bb8507be136de5363950e 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { CODE_BLOCK } from '$lib/constants';
        import { ColorMode } from '$lib/enums';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { isMobile } from '$lib/stores';
        import type { ContentEditableToken } from '$lib/types';
        import type { SourceHistoryEntry } from '$lib/utils';
        import {
index 0e7fadc4a06905b720ac790fd42a18906fa282d2..606fa6cd3af2156f1e6ffb0b7c21d85e921a8936 100644 (file)
@@ -1,32 +1,32 @@
 <script lang="ts">
        import ContextGaugeDial from './ContextGaugeDial.svelte';
        import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
-       import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
        import {
+               chatStore,
+               conversationsStore,
                gaugeTriggerClick,
                gaugeTriggerEnter,
                gaugeTriggerKeydown,
                gaugeTriggerLeave,
                gaugeTriggerPointerDown
-       } from '$lib/stores/context-gauge-popup.svelte';
-       import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
+       } from '$lib/stores';
        import { untrack } from 'svelte';
 
        const gauge = useContextGauge();
 
        $effect(() => {
-               const conv = activeConversation();
+               const conv = conversationsStore.activeConversation;
 
                untrack(() => chatStore.setActiveProcessingConversation(conv?.id ?? null));
        });
 
        $effect(() => {
-               const conv = activeConversation();
-               const messages = activeMessages() as DatabaseMessage[];
+               const conv = conversationsStore.activeConversation;
+               const messages = conversationsStore.activeMessages as DatabaseMessage[];
 
                if (!conv) return;
 
-               if (isLoading() || isChatStreaming()) return;
+               if (chatStore.isLoading || chatStore.isStreaming()) return;
 
                if (messages.length === 0) {
                        untrack(() => chatStore.clearProcessingState(conv.id));
index db4479b65b08c218ebfc236b561839db260118e7..e6abb4a3e9d4b00a4f4571c377df22a93afd5718 100644 (file)
@@ -3,12 +3,7 @@
        import ContextGaugeDetails from './ContextGaugeDetails.svelte';
        import ContextGaugeLoadModel from './ContextGaugeLoadModel.svelte';
        import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
-       import {
-               gaugeCardEnter,
-               gaugeCardLeave,
-               gaugePopup,
-               gaugePopupClose
-       } from '$lib/stores/context-gauge-popup.svelte';
+       import { gaugeCardEnter, gaugeCardLeave, gaugePopup, gaugePopupClose } from '$lib/stores';
        import { formatParameters } from '$lib/utils/formatters';
 
        const gauge = useContextGauge();
@@ -92,7 +87,7 @@
                                                <span class={colorLevelTextClass(gauge.colorLevel)}>{gauge.contextPercent}%</span> used
                                        </span>
                                        <span>
-                                               {formatParameters((gauge.contextTotal ?? 0) - gauge.contextUsed)} remaining
+                                               {formatParameters(gauge.contextAvailable ?? 0)} remaining
                                        </span>
                                </div>
                        {:else}
index 874dca5a5f6cd81013b56aea1a2dc3eefcd6500a..3f178da188b160dfdf51d9a116a8e1c153ead256 100644 (file)
@@ -3,11 +3,7 @@
                ChatAttachmentsListItemMcpResource,
                HorizontalScrollCarousel
        } from '$lib/components/app';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import {
-               mcpHasResourceAttachments,
-               mcpResourceAttachments
-       } from '$lib/stores/mcp-resources.svelte';
+       import { mcpResourceStore, mcpStore } from '$lib/stores';
 
        interface Props {
                class?: string;
@@ -16,8 +12,8 @@
 
        let { class: className, onResourceClick }: Props = $props();
 
-       const attachments = $derived(mcpResourceAttachments());
-       const hasAttachments = $derived(mcpHasResourceAttachments());
+       const attachments = $derived(mcpResourceStore.attachments);
+       const hasAttachments = $derived(mcpResourceStore.hasAttachments);
 
        function handleRemove(attachmentId: string) {
                mcpStore.removeResourceAttachment(attachmentId);
index 6a2be8cffdf1751caec5c05866f049a9784e39b0..1c7c8f7d4c53e51f43d11bde607944e8dafc2501 100644 (file)
@@ -8,9 +8,7 @@
        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 { isMobile, settingsStore, toolsStore } from '$lib/stores';
        import type { FileMentionEntry, GlobEntryResult } from '$lib/types';
        import { abbreviateHome, runGlobSearchWithChildren } from '$lib/utils';
 
@@ -64,7 +62,7 @@
        // Coerce the depth setting to a positive integer; an invalid value
        // would otherwise reach the server as max_depth 0 = unlimited.
        const searchDepth = $derived.by(() => {
-               const n = Number(config().mentionSearchMaxDepth);
+               const n = Number(settingsStore.config.mentionSearchMaxDepth);
 
                return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS.DEFAULT_SEARCH_DEPTH;
        });
index 82ba581d275d3ee84d25a35bc8d869edf841fc40..d7c66f0d2d8afb2df1687a5d65c1596a2e3dd1e0 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { MCPServerSettingsEntry } from '$lib/types';
        import type { Snippet } from 'svelte';
 
index 126d08744a9305040e9d1e9042a86e24e043ad2c..9b5a57b9b9e12e1f1e8a4dbf8cc28f2898fd0e0d 100644 (file)
@@ -9,8 +9,7 @@
        } 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 { conversationsStore, mcpStore } from '$lib/stores';
        import type { GetPromptResult, MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
        import { debounce, uuid } from '$lib/utils';
        import { SvelteMap } from 'svelte/reactivity';
index 1ad5ba553295d1077b951522952ca951a7872a32..623744d8af4dadbd44206c9f4040839b660cc099 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { isMobile } from '$lib/stores';
        import { autoResizeTextarea } from '$lib/utils';
        import { onMount } from 'svelte';
 
index 639ddb52475daaed8ea94f007106247f90a28f67..b1652d026a78c97c7ef8de074bf1933be80dd532 100644 (file)
@@ -10,7 +10,7 @@
        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 { toolsStore } from '$lib/stores';
        import type { GlobEntry } from '$lib/types';
        import {
                abbreviateHome,
index 2cc3fb42f20bfe0c8f3b12b72e712679946e9941..b3e54ada02fd3b3a6d4072c765b28b934070824b 100644 (file)
@@ -11,9 +11,7 @@
        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 { chatStore, conversationsStore, isMobile } from '$lib/stores';
        import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
        import { deriveAgenticSections } from '$lib/utils';
        import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
        });
 
        $effect(() => {
-               const pendingId = pendingEditMessageId();
+               const pendingId = chatStore.pendingEditMessageId;
 
                if (pendingId && pendingId === message.id && !isEditing) {
                        handleEdit();
index 14ca160ed856fe83d7064b1cc90292eb7842dc4c..14ea68099c6fcc92705b4061b775655838de3e71 100644 (file)
        import { getMessageEditContext } from '$lib/contexts';
        import { MessageRole } from '$lib/enums';
        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 { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
        import { modelLoadProgressText } from '$lib/utils';
        import { hasAgenticContent } from '$lib/utils';
 
        const isAgentic = $derived(hasAgenticContent(message, toolMessages));
        const processingState = useProcessingState();
 
-       let currentConfig = $derived(config());
-       let isRouter = $derived(isRouterMode());
+       let currentConfig = $derived(settingsStore.config);
+       let isRouter = $derived(serverStore.isRouterMode);
 
        let showRawOutput = $state(false);
 
        let displayedModel = $derived(message.model ?? null);
 
-       let isCurrentlyLoading = $derived(isLoading());
-       let isStreaming = $derived(isChatStreaming());
+       let isCurrentlyLoading = $derived(chatStore.isLoading);
+       let isStreaming = $derived(chatStore.isStreaming());
        let hasNoContent = $derived(!message?.content?.trim());
        let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
 
                        <ChatMessageAgenticContent
                                {message}
                                {toolMessages}
-                               isStreaming={isChatStreaming()}
+                               isStreaming={chatStore.isStreaming()}
                                {isLastAssistantMessage}
                        />
                {/if}
                        <div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
                                <ChatMessageAssistantModel
                                        {displayedModel}
-                                       isLoading={isLoading()}
+                                       isLoading={chatStore.isLoading}
                                        {isRouter}
                                        {onRegenerate}
                                />
 
                                <ChatMessageAssistantStatistics
                                        {message}
-                                       isLoading={isLoading()}
+                                       isLoading={chatStore.isLoading}
                                        {processingState}
                                        showMessageStats={currentConfig.showMessageStats}
                                />
index d27e3e01d76930cf3637e9fcabed32f0ad3b4f35..d3fb33a0089702d93e7dddaecf43bfceeb80d185 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
        import { ServerModelStatus } from '$lib/enums';
-       import { modelsStore } from '$lib/stores/models.svelte';
+       import { modelsStore } from '$lib/stores';
        import { copyToClipboard } from '$lib/utils';
 
        interface Props {
index 657157956426e33466d18cbc58c3b30b520ec2ef..52fbd4eaa3ff10caa978808fd0029528e1aa2936 100644 (file)
@@ -10,7 +10,7 @@
 
        let { modelLoadingText, position, processingState }: Props = $props();
 
-       const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
+       const marginClass = $derived(position === 'top' ? 'mt-6' : 'mt-4');
 </script>
 
 <div class="{marginClass} w-full max-w-3xl" in:fade>
index 5c5ac8c25c8256460aecfb69c16b103f4d78d57b..e6e18ae0809dbbfccf989a12c2e034a25ba7f111 100644 (file)
@@ -2,6 +2,7 @@
        import { ChatMessageStatistics } from '$lib/components/app';
        import { ChatMessageStatisticsMode } from '$lib/enums';
        import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
+       import { agenticStore } from '$lib/stores';
 
        interface Props {
                message: DatabaseMessage;
        }
 
        let { isLoading, message, processingState, showMessageStats }: Props = $props();
+
+       // A running agentic flow stamps per-turn timings on its root message at each
+       // turn boundary and the cumulative agentic totals only on exit; while it runs,
+       // show the session's live totals on the root message instead.
+       const liveLlm = $derived(agenticStore.getLiveLlmTotals(message.convId));
+       const isLiveFlowRoot = $derived(
+               liveLlm !== null && agenticStore.getFlowRootMessageId(message.convId) === message.id
+       );
 </script>
 
-{#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
+{#if showMessageStats && isLiveFlowRoot && liveLlm}
+       <ChatMessageStatistics
+               mode={ChatMessageStatisticsMode.GENERATION}
+               isLive
+               promptTokens={liveLlm.prompt_n}
+               promptMs={liveLlm.prompt_ms}
+               predictedTokens={liveLlm.predicted_n}
+               predictedMs={liveLlm.predicted_ms}
+       />
+{:else if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
        {@const agentic = message.timings.agentic}
        <ChatMessageStatistics
                mode={ChatMessageStatisticsMode.GENERATION}
index 3d45ca4dac2ede8287a89520afb20659740ac09b..9190c7e62f5ccd0c533405af1d882be706f72542 100644 (file)
@@ -3,7 +3,7 @@
        import { Card } from '$lib/components/ui/card';
        import * as Tooltip from '$lib/components/ui/tooltip';
        import { McpPromptVariant } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
        import { SvelteMap } from 'svelte/reactivity';
 
index 30605ba9f1e44d141231a4deffaeb821dba5455d..2f919f9eb89cb8627fde1fae07ec83c414d87f3f 100644 (file)
@@ -6,7 +6,7 @@
        import { INPUT_CLASSES } from '$lib/constants';
        import { getMessageEditContext } from '$lib/contexts';
        import { KeyboardKey, MessageRole } from '$lib/enums';
-       import { config } from '$lib/stores/settings.svelte';
+       import { settingsStore } from '$lib/stores';
        import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
 
        interface Props {
@@ -64,7 +64,7 @@
        let contentHeight = $state(0);
 
        const MAX_HEIGHT = 200; // pixels
-       const currentConfig = config();
+       const currentConfig = settingsStore.config;
 
        let showExpandButton = $derived(contentHeight > MAX_HEIGHT);
 
index 6b95a92fb2e78b0fa5e045ad09483c78c924ec7d..6545cc39f7a9c002cdac66ab4755d5061bf7e2ad 100644 (file)
@@ -3,7 +3,7 @@
        import ToolCallBlock from './ToolCallBlock.svelte';
        import { XCircle } from '@lucide/svelte';
        import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { toolsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import { abbreviateHome, computeLineDiff, prefixFor } from '$lib/utils';
 
index caf6d832b1955dc5a71d83fcd741de8df32abae2..639bee89784a55f8503f48d996982144076d1520 100644 (file)
@@ -12,8 +12,7 @@
        import { CollapsibleTerminalBlock } from '$lib/components/app';
        import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
        import { AttachmentType } from '$lib/enums';
-       import { config } from '$lib/stores/settings.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { settingsStore, toolsStore } from '$lib/stores';
        import type { AgenticSection, ToolResultLine } from '$lib/types';
        import type { DatabaseMessageExtra } from '$lib/types';
        import {
@@ -93,7 +92,7 @@
        );
 
        const useFullHeightCodeBlocks = $derived(
-               Boolean(config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS])
+               Boolean(settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS])
        );
 
        const autoScroll = $derived(isLive && !useFullHeightCodeBlocks);
index 66eb3a2ad6226902cd4de17fed84de318917ded3..7a9fbe97b6edcfe383495b17b9b789273be9bc97 100644 (file)
@@ -2,7 +2,7 @@
        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 { toolsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import { abbreviateHome } from '$lib/utils';
 
index 9e6ef10919db6a72d06455efba81de2c0387d022..6b39e7d92f73c8cfaf01a37e222d3e2985e9840b 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { Info, Loader2 } from '@lucide/svelte';
        import { AgenticSectionType } from '$lib/enums';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { toolsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import { abbreviateHome } from '$lib/utils';
 
index c650318adb06188be01f479d7621a20396b8c73d..a56a8da9d6e401af465d4e0f8dc7ee0ba921badc 100644 (file)
@@ -2,7 +2,7 @@
        import { parseGrepSearchMeta } from './parsers/grep-search';
        import ToolCallBlock from './ToolCallBlock.svelte';
        import { XCircle } from '@lucide/svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { toolsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import { abbreviateHome } from '$lib/utils';
 
index 67a66d7da8a1a5d91cc6181fbffd716ec90631f9..e6bd9472203ec9c421d09b46b9e7df05286fa698 100644 (file)
@@ -4,7 +4,7 @@
        import * as HoverCard from '$lib/components/ui/hover-card';
        import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
        import { AgenticSectionType } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { AgenticSection, SearchResult } from '$lib/types';
        import {
                extractSearchQuery,
index 0f3917f6ab31ea28dfd604e53d9c378f527623c8..2551fb0b21c027982b9a24489b8ca3bdc07a2ce3 100644 (file)
@@ -4,7 +4,7 @@
        import { XCircle } from '@lucide/svelte';
        import { SyntaxHighlightedCode } from '$lib/components/app';
        import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { toolsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import { abbreviateHome } from '$lib/utils';
 
index ea08f01357975e1f2cd14b02356df2268607471b..4b8524fe69eeecb1f5abc8427ebf40bd80deab5d 100644 (file)
@@ -13,7 +13,7 @@
        import { CollapsibleContentBlock } from '$lib/components/app';
        import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
        import { AgenticSectionType } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { AgenticSection, BuiltinToolUiEntry } from '$lib/types';
        import { getBuiltinToolUi } from '$lib/utils';
        import type { Component, Snippet } from 'svelte';
index 7e79ab2309b0c1f618575666500b011685f7e902..f902ec88d594f9ae7ea1bb2f3476eb5c49023dbc 100644 (file)
@@ -8,8 +8,7 @@
        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 { config } from '$lib/stores/settings.svelte';
+       import { chatStore, settingsStore } from '$lib/stores';
 
        interface Props {
                class?: string;
@@ -54,8 +53,8 @@
        const editCtx = getMessageEditContext();
        const processingState = useProcessingState();
 
-       const currentConfig = $derived(config());
-       const isActivelyProcessing = $derived(isLastUserMessage && isLoading());
+       const currentConfig = $derived(settingsStore.config);
+       const isActivelyProcessing = $derived(isLastUserMessage && chatStore.isLoading);
 
        // For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
        let storedReadingStats = $derived.by(() => {
index f5a3ff6f98d4569e0dd7d1657b9f856cd2a742d8..99539c3051bb010848bb8bc46e9cc5d10a0d7f96 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { ChatAttachmentsList, MarkdownContent, MentionText } from '$lib/components/app';
        import { Card } from '$lib/components/ui/card';
-       import { config } from '$lib/stores/settings.svelte';
+       import { settingsStore } from '$lib/stores';
        import type { DatabaseMessageExtra } from '$lib/types/database';
 
        interface Props {
@@ -24,7 +24,7 @@
 
        let isMultiline = $state(false);
        let messageElement: HTMLElement | undefined = $state();
-       const currentConfig = config();
+       const currentConfig = settingsStore.config;
 
        $effect(() => {
                if (!messageElement || !content.trim()) return;
index ef79fabeda3481401e0152706a71a044000a5b42..a0cee94f468f4513b137e44ebf18e8e26fc18b0d 100644 (file)
@@ -7,7 +7,7 @@
        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';
+       import { toolsStore } from '$lib/stores';
 
        interface Props {
                toolName: string;
index d08dafcff54f399fa3fecf658dff3fd423d711a6..1b3715bb4f31b3193fbc078a23883ef76e59d514 100644 (file)
@@ -10,7 +10,7 @@
        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';
+       import { conversationsStore } from '$lib/stores';
 
        interface Props {
                role: MessageRole.USER | MessageRole.ASSISTANT;
@@ -69,7 +69,7 @@
        }
 
        function handleOpenForkDialog() {
-               const conv = activeConversation();
+               const conv = conversationsStore.activeConversation;
 
                forkName = `Fork of ${conv?.name ?? 'Conversation'}`;
                forkIncludeAttachments = true;
index 7fa29a899521db09d13a20217854a26e17c03ed7..8ce1ead01e9fc5fa22c54713ca32759007570d99 100644 (file)
@@ -8,15 +8,7 @@
                MarkdownContent
        } from '$lib/components/app';
        import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
-       import {
-               agenticExecutingToolCallId,
-               agenticLastError,
-               agenticPendingContinueRequest,
-               agenticPendingPermissionRequest,
-               agenticResolveContinue,
-               agenticResolvePermission
-       } from '$lib/stores/agentic.svelte';
-       import { config } from '$lib/stores/settings.svelte';
+       import { agenticStore, settingsStore } from '$lib/stores';
        import type { AgenticSection } from '$lib/types';
        import type {
                ChatMessageAgenticTimings,
 
        let expandedStates: Record<number, boolean> = $state({});
 
-       const showThoughtInProgress = $derived(Boolean(config().showThoughtInProgress));
-       const alwaysShowToolCallContent = $derived(Boolean(config().alwaysShowToolCallContent));
-       const showMessageStats = $derived(Boolean(config().showMessageStats));
-       const showAgenticTurnStats = $derived(showMessageStats && Boolean(config().showAgenticTurnStats));
+       const showThoughtInProgress = $derived(Boolean(settingsStore.config.showThoughtInProgress));
+       const alwaysShowToolCallContent = $derived(
+               Boolean(settingsStore.config.alwaysShowToolCallContent)
+       );
+       const showMessageStats = $derived(Boolean(settingsStore.config.showMessageStats));
+       const showAgenticTurnStats = $derived(
+               showMessageStats && Boolean(settingsStore.config.showAgenticTurnStats)
+       );
 
        const hasReasoningError = $derived(
-               isLastAssistantMessage ? !!agenticLastError(message.convId) : false
+               isLastAssistantMessage ? !!agenticStore.lastError(message.convId) : false
        );
 
        let permissionDismissed = $state(false);
 
        const pendingPermission = $derived(
-               isStreaming && isLastAssistantMessage ? agenticPendingPermissionRequest(message.convId) : null
+               isStreaming && isLastAssistantMessage
+                       ? agenticStore.pendingPermissionRequest(message.convId)
+                       : null
        );
 
        let prevPendingRef: typeof pendingPermission = null;
 
        function handlePermission(decision: ToolPermissionDecision) {
                permissionDismissed = true;
-               agenticResolvePermission(message.convId, decision);
+               agenticStore.resolvePermission(message.convId, decision);
        }
 
        let continueDismissed = $state(false);
 
        const pendingContinue = $derived(
-               isStreaming && isLastAssistantMessage ? agenticPendingContinueRequest(message.convId) : false
+               isStreaming && isLastAssistantMessage
+                       ? agenticStore.pendingContinueRequest(message.convId)
+                       : false
        );
 
        let prevContinueRef = false;
 
        function handleContinue(shouldContinue: boolean) {
                continueDismissed = true;
-               agenticResolveContinue(message.convId, shouldContinue);
+               agenticStore.resolveContinue(message.convId, shouldContinue);
        }
 
        const sections = $derived(deriveAgenticSections(message, toolMessages, [], isStreaming));
 
        const currentlyExecutingToolCallId = $derived(
-               isStreaming ? agenticExecutingToolCallId(message.convId) : null
+               isStreaming ? agenticStore.executingToolCallId(message.convId) : null
        );
 
        type TurnGroup = {
index f44cd432e64035ba078fd26119d6f537a2874a05..feacd0ddde340e7b485d22d64ba8d3c3983af614 100644 (file)
@@ -5,7 +5,7 @@
        import { Switch } from '$lib/components/ui/switch';
        import { getMessageEditContext } from '$lib/contexts';
        import { KeyboardKey, MessageRole } from '$lib/enums';
-       import { chatStore } from '$lib/stores/chat.svelte';
+       import { chatStore } from '$lib/stores';
        import { processFilesToChatUploaded } from '$lib/utils/browser-only';
 
        const editCtx = getMessageEditContext();
index fbd1567615b9cb7765186f6aef3eb614c5de9b1d..78c9dcb25a74eea6ddfc37dae3251ea30fc1e245 100644 (file)
@@ -3,7 +3,7 @@
        import { CollapsibleContentBlock, MarkdownContent } from '$lib/components/app';
        import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
        import { AgenticSectionType } from '$lib/enums';
-       import { config } from '$lib/stores/settings.svelte';
+       import { settingsStore } from '$lib/stores';
        import type { DatabaseMessageExtra } from '$lib/types';
        import type { AgenticSection } from '$lib/types';
 
@@ -25,7 +25,7 @@
                section
        }: Props = $props();
 
-       const currentConfig = config();
+       const currentConfig = settingsStore.config;
 
        const REASONING_HEADER = 'Reasoning';
        const REASONING_HEADER_PENDING = 'Reasoning...';
index 3514165a5b480d7046dff91da155ba1adbae1e0c..33d971f93188b3aa0dad9843d503a32e662b1648 100644 (file)
@@ -2,21 +2,7 @@
        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 {
-               chatClearPendingMessage,
-               chatInjectPendingMessage,
-               chatPendingMessageContent,
-               chatPendingMessageExtras
-       } from '$lib/stores/chat.svelte';
-       import { activeConversation, conversationsStore } from '$lib/stores/conversations.svelte';
-       import { config } from '$lib/stores/settings.svelte';
+       import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
        import {
                buildSiblingInfoMap,
                copyToClipboard,
@@ -34,7 +20,7 @@
 
        let allConversationMessages = $state<DatabaseMessage[]>([]);
 
-       const currentConfig = config();
+       const currentConfig = settingsStore.config;
 
        setChatActionsContext({
                continueAssistantMessage: async (message: DatabaseMessage) => {
        });
 
        function refreshAllMessages() {
-               const conversation = activeConversation();
+               const conversation = conversationsStore.activeConversation;
 
                if (conversation) {
                        conversationsStore.getConversationMessages(conversation.id).then((messages) => {
 
        // Refresh messages whenever the active conversation changes
        $effect(() => {
-               if (activeConversation()) {
+               if (conversationsStore.activeConversation) {
                        refreshAllMessages();
                }
        });
                />
        {/each}
 
-       {#if activeConversation() && agenticPendingSteeringMessageContent(activeConversation()!.id)}
-               {@const convId = activeConversation()!.id}
-               {@const pendingContent = agenticPendingSteeringMessageContent(convId)}
+       {#if conversationsStore.activeConversation && agenticStore.pendingSteeringMessageContent(conversationsStore.activeConversation!.id)}
+               {@const convId = conversationsStore.activeConversation!.id}
+               {@const pendingContent = agenticStore.pendingSteeringMessageContent(convId)}
 
                {#if pendingContent}
                        <ChatMessageUserPending
                                class="mx-auto mt-12 w-full max-w-[48rem]"
                                content={pendingContent}
-                               extras={agenticPendingSteeringMessageExtras(convId)}
+                               extras={agenticStore.pendingSteeringMessageExtras(convId)}
                                onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
-                               onEdit={(newContent, extras) => agenticInjectSteeringMessage(convId, newContent, extras)}
-                               onDelete={() => agenticClearSteeringMessage(convId)}
+                               onEdit={(newContent, extras) =>
+                                       agenticStore.injectSteeringMessage(convId, newContent, extras)}
+                               onDelete={() => agenticStore.clearSteeringMessage(convId)}
                        />
                {/if}
-       {:else if activeConversation() && chatPendingMessageContent(activeConversation()!.id)}
-               {@const convId = activeConversation()!.id}
-               {@const pendingContent = chatPendingMessageContent(convId)}
+       {:else if conversationsStore.activeConversation && chatStore.pendingMessageContent(conversationsStore.activeConversation!.id)}
+               {@const convId = conversationsStore.activeConversation!.id}
+               {@const pendingContent = chatStore.pendingMessageContent(convId)}
 
                {#if pendingContent}
                        <ChatMessageUserPending
                                class="mx-auto mt-12 w-full max-w-[48rem]"
                                content={pendingContent}
-                               extras={chatPendingMessageExtras(convId)}
+                               extras={chatStore.pendingMessageExtras(convId)}
                                onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
-                               onEdit={(newContent, extras) => chatInjectPendingMessage(convId, newContent, extras)}
-                               onDelete={() => chatClearPendingMessage(convId)}
+                               onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)}
+                               onDelete={() => chatStore.clearPendingMessage(convId)}
                        />
                {/if}
        {/if}
index 48ab40ec097c7f69e4c53655dd00637b693d5696..2b5ca68de3cc2e7aaca3be5ca77812e178c8a6c3 100644 (file)
        import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
        import {
                chatStore,
-               errorDialog,
-               isChatStreaming,
-               isEditing,
-               isLoading
-       } from '$lib/stores/chat.svelte';
-       import {
-               activeConversation,
-               activeMessages,
-               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 { isMobile } from '$lib/stores/viewport.svelte';
+               conversationsStore,
+               device,
+               isMobile,
+               serverStore,
+               settingsStore
+       } from '$lib/stores';
        import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
        import { onDestroy, onMount, tick } from 'svelte';
 
        let { showCenteredEmpty = false } = $props();
 
-       let disableAutoScroll = $derived(Boolean(config().disableAutoScroll) || isMobile.current);
+       let disableAutoScroll = $derived(
+               Boolean(settingsStore.config.disableAutoScroll) || isMobile.current
+       );
        let isMobileUserScrolledUp = $state(false);
        let mobileScrollDownHint = $state(false);
        let mobileScrollDownHintLockedUntil = $state(0);
        let showDeleteDialog = $state(false);
        let showEmptyFileDialog = $state(false);
        let isEmpty = $derived(
-               showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
+               showCenteredEmpty &&
+                       !conversationsStore.activeConversation &&
+                       conversationsStore.activeMessages.length === 0 &&
+                       !chatStore.isLoading
        );
-       let activeErrorDialog = $derived(errorDialog());
-       let isServerLoading = $derived(serverLoading());
-       let hasPropsError = $derived(!!serverError());
-       let isCurrentConversationLoading = $derived(isLoading() || isChatStreaming());
+       let activeErrorDialog = $derived(chatStore.errorDialogState);
+       let isServerLoading = $derived(serverStore.loading);
+       let hasPropsError = $derived(!!serverStore.error);
+       let isCurrentConversationLoading = $derived(chatStore.isLoading || chatStore.isStreaming());
        let chatFormBottomPosition = $derived.by(() => {
                if (!isMobile.current) return '1rem';
 
@@ -80,7 +77,7 @@
        });
        const { handleKeydown } = useKeyboardShortcuts({
                deleteActiveConversation: () => {
-                       if (activeConversation()) {
+                       if (conversationsStore.activeConversation) {
                                showDeleteDialog = true;
                        }
                }
        }
 
        async function handleDeleteConfirm() {
-               const conversation = activeConversation();
+               const conversation = conversationsStore.activeConversation;
 
                if (conversation) {
                        await conversationsStore.deleteConversation(conversation.id);
        async function handleMessagesReady(messageCount: number) {
                if (messageCount === 0) return;
 
-               const id = activeConversation()?.id ?? null;
+               const id = conversationsStore.activeConversation?.id ?? null;
 
                if (!id || id === lastScrolledConversationId) return;
 
                const settle = () => {
                        if (autoScroll.userScrolledUp) return;
 
-                       if (activeConversation()?.id !== id) return;
+                       if (conversationsStore.activeConversation?.id !== id) return;
 
                        autoScroll.scrollToBottom();
                        const height = container.scrollHeight;
 
        $effect(() => {
                const shouldDisableAutoScroll =
-                       config().disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
+                       settingsStore.config.disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
 
                autoScroll.setDisabled(shouldDisableAutoScroll);
 
        >
                {#if !isEmpty}
                        <ChatMessages
-                               messages={activeMessages()}
+                               messages={conversationsStore.activeMessages}
                                onMessagesReady={handleMessagesReady}
                                onUserAction={() => {
                                        handleSendLikeScroll();
 
                        <ChatScreenForm
                                class="pointer-events-auto conversation-chat-form"
-                               disabled={hasPropsError || isEditing()}
+                               disabled={hasPropsError || chatStore.isEditing()}
                                {initialMessage}
                                isLoading={isCurrentConversationLoading}
                                onFileRemove={fileUpload.handleFileRemove}
index 679a7cd5fc260e050d375217372a054225bd9353..4119b2816d5c3905dc124053d7e3544aae464ef6 100644 (file)
@@ -3,7 +3,7 @@
        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 { isMobile } from '$lib/stores';
        import { onMount } from 'svelte';
 
        interface Props {
index 5b44bcf858b1ceda6957feac5abe384bb79148fe..5af00ebb479b9423a33ff137ead60829a1b6d393 100644 (file)
@@ -1,5 +1,5 @@
 <script lang="ts">
-       import { serverStore } from '$lib/stores/server.svelte';
+       import { serverStore } from '$lib/stores';
 
        interface Props {
                isEmpty: boolean;
index cae37d6f8a8b6da6565aef3fcebde85dc01fa126..2c5d1af10d721c0fc7af924c3fdc59eb2c28cddb 100644 (file)
@@ -2,10 +2,10 @@
        import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
        import * as Alert from '$lib/components/ui/alert';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
-       import { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
+       import { serverStore } from '$lib/stores';
 
-       let hasError = $derived(!!serverError());
-       let isLoadingModel = $derived(serverStatus() === 503);
+       let hasError = $derived(!!serverStore.error);
+       let isLoadingModel = $derived(serverStore.status === 503);
 </script>
 
 {#if hasError}
                                {#if !isLoadingModel}
                                        <button
                                                onclick={() => serverStore.fetch()}
-                                               disabled={serverLoading()}
+                                               disabled={serverStore.loading}
                                                class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
                                        >
-                                               <RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
-                                               {serverLoading() ? 'Retrying...' : 'Retry'}
+                                               <RefreshCw class="h-3 w-3 {serverStore.loading ? 'animate-spin' : ''}" />
+                                               {serverStore.loading ? 'Retrying...' : 'Retry'}
                                        </button>
                                {/if}
                        </Alert.Title>
 
                        {#if !isLoadingModel}
-                               <Alert.Description>{serverError()}</Alert.Description>
+                               <Alert.Description>{serverStore.error}</Alert.Description>
                        {/if}
                </Alert.Root>
        </div>
index 90611861016c33f840c4b7a09a289d905a8392e3..a8e0dcc196d51b0ea4f28dde7dcbd2c6abdb10e8 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { Loader2 } from '@lucide/svelte';
        import { StreamConnectionState } from '$lib/enums';
-       import { chatStore } from '$lib/stores/chat.svelte';
+       import { chatStore } from '$lib/stores';
 
        let state = $derived(chatStore.streamConnectionState);
 </script>
index edfa32d48b70d15da33fd60386ffab10175a6b11..04d48f0aa860ae44a2b3f596d6f260ce05a37995 100644 (file)
@@ -45,7 +45,7 @@
        import { ColorMode, UrlProtocol } from '$lib/enums';
        import { FileTypeText } from '$lib/enums/files.enums';
        import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
-       import { config } from '$lib/stores/settings.svelte';
+       import { settingsStore } from '$lib/stores';
        import type { DatabaseMessageExtra } from '$lib/types/database';
        import {
                copyCodeToClipboard,
 <div
        bind:this={containerRef}
        onclick={handleMermaidClick}
-       class="markdown-content {className}{config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
+       class="markdown-content {className}{settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
                ? ' full-height-code-blocks'
                : ''}"
 >
index 24a78de46fe19a9afa07715ce0ca00b5b51432f1..7bc5ca3b8e3a10e7780784ca4d18f49a72f7f29a 100644 (file)
@@ -16,8 +16,7 @@ import {
        PATH_SEPARATOR,
        SETTINGS_KEYS
 } from '$lib/constants';
-import { settingsStore } from '$lib/stores/settings.svelte';
-import { toolsStore } from '$lib/stores/tools.svelte';
+import { settingsStore, toolsStore } from '$lib/stores';
 import { decodeFileLinkPath, getMentionBadgeIconPaths, getMentionBadgeLabel } from '$lib/utils';
 import type { Element, Root } from 'hast';
 import type { Plugin } from 'unified';
index 1af126267cde44b2ce59b083c26744a218d406da..feda3aa43ac662d43a7ec353bbf9e16be44eddf1 100644 (file)
@@ -1,7 +1,6 @@
 <script lang="ts">
        import { SETTINGS_KEYS } from '$lib/constants';
-       import { settingsStore } from '$lib/stores/settings.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { settingsStore, toolsStore } from '$lib/stores';
        import {
                getMentionBadgeIconPaths,
                getMentionBadgeLabel,
index 6931d19ac0516ce99f75459b34e5d6ba7805c949..370676676f89edad9eba08ba222ab0b96cafb8aa 100644 (file)
@@ -5,7 +5,7 @@
        import * as Dialog from '$lib/components/ui/dialog';
        import { DEFAULT_RESOURCE_FILENAME, MIME_TYPE_SUBSTRINGS } from '$lib/constants';
        import { MimeTypeText } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { DatabaseMessageExtraMcpResource } from '$lib/types';
        import {
                downloadResourceContent,
index 7e1b397fc89949411b3eaa623748305fd7e23ab8..1ddad694b6493a437779584279cc620668fbac33 100644 (file)
@@ -8,13 +8,7 @@
        import { Button } from '$lib/components/ui/button';
        import * as Dialog from '$lib/components/ui/dialog';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import {
-               mcpResources,
-               mcpResourceStore,
-               mcpTotalResourceCount
-       } from '$lib/stores/mcp-resources.svelte';
+       import { conversationsStore, mcpResourceStore, mcpStore } from '$lib/stores';
        import type { MCPResourceContent, MCPResourceInfo, MCPResourceTemplateInfo } from '$lib/types';
        import { getResourceDisplayName } from '$lib/utils';
        import { SvelteSet } from 'svelte/reactivity';
@@ -39,7 +33,7 @@
        let templatePreviewLoading = $state(false);
        let templatePreviewError = $state<string | null>(null);
 
-       const totalCount = $derived(mcpTotalResourceCount());
+       const totalCount = $derived(mcpResourceStore.totalResourceCount);
 
        $effect(() => {
                if (open) {
 
        function getAllResourcesFlatInTreeOrder(): MCPResourceInfo[] {
                const allResources: MCPResourceInfo[] = [];
-               const resourcesMap = mcpResources();
+               const resourcesMap = mcpResourceStore.serverResources;
 
                for (const [serverName, serverRes] of resourcesMap.entries()) {
                        for (const resource of serverRes.resources) {
index 03d55cd0f5bb873e047689756d1af69dbcd55ee6..d8d6704253c962ff0951da018137d07770e412b6 100644 (file)
@@ -12,8 +12,7 @@
                RECOMMENDED_MCP_SERVERS
        } from '$lib/constants';
        import { HealthCheckStatus } from '$lib/enums';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { conversationsStore, mcpStore } from '$lib/stores';
        import { canonicalizeServerUrl, parseHeadersToArray, uuid } from '$lib/utils';
 
        interface Props {
index 9318c6bd616988da98c35a95ee39b10eefc448cf..61155fceba418489bb0f05c2d99c81abe9b2de1b 100644 (file)
@@ -2,8 +2,7 @@
        import { ActionIconCopyToClipboard, BadgesModality } from '$lib/components/app';
        import * as Dialog from '$lib/components/ui/dialog';
        import * as Table from '$lib/components/ui/table';
-       import { modelOptions, modelsLoading, modelsStore } from '$lib/stores/models.svelte';
-       import { serverStore } from '$lib/stores/server.svelte';
+       import { modelsStore, serverStore } from '$lib/stores';
        import type { ApiLlamaCppServerProps } from '$lib/types';
        import { formatFileSize, formatNumber, formatParameters } from '$lib/utils';
 
@@ -26,8 +25,8 @@
        let serverProps = $derived(isRouter && modelId ? routerModelProps : serverStore.props);
 
        let modelName = $derived(isRouter && modelId ? modelId : modelsStore.singleModelName);
-       let models = $derived(modelOptions());
-       let isLoadingModels = $derived(modelsLoading());
+       let models = $derived(modelsStore.models);
+       let isLoadingModels = $derived(modelsStore.loading);
 
        // in router mode, find the model option matching modelId
        // in single mode, use the first model as before
index 63210ad723e4c273d7737b62a0c6c4e6462b171a..301c3969914fed48192a7f01b3549da60f513e7b 100644 (file)
@@ -3,8 +3,7 @@
        import * as Tooltip from '$lib/components/ui/tooltip';
        import { ICON_CLASS_DEFAULT, MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants';
        import { HealthCheckStatus } from '$lib/enums';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { conversationsStore, mcpStore } from '$lib/stores';
 
        interface Props {
                class?: string;
index fb57dbfac9a222b3e1002dfa5336bae4e095663e..9a384e397c52ad839034614f912830a19afe5ccb 100644 (file)
@@ -4,7 +4,7 @@
        import { Button } from '$lib/components/ui/button';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
        import { MimeTypeApplication, MimeTypeText } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { MCPResourceContent, MCPResourceInfo } from '$lib/types';
        import {
                createBase64DataUrl,
index a2e7f2be79470fe4eb9544495f0f9c391dc7e8dc..a99cc0e04e0cdeef1ef1a411591322b3dd3ab2b5 100644 (file)
@@ -3,7 +3,7 @@
        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 { mcpStore } from '$lib/stores';
        import type { MCPResourceTemplateInfo } from '$lib/types';
        import {
                debounce,
index f17199a900955842743a7bf6c3555c88f23a6187..18e974653290f623b560db2264a021f3ca36e999 100644 (file)
@@ -2,8 +2,7 @@
        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 { mcpResourceStore, mcpStore } from '$lib/stores';
        import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
        import { parseResourcePath } from '$lib/utils';
        import { SvelteMap, SvelteSet } from 'svelte/reactivity';
@@ -32,8 +31,8 @@
        let expandedFolders = new SvelteSet<string>();
        let searchQuery = $state('');
 
-       const resources = $derived(mcpResources());
-       const isLoading = $derived(mcpResourcesLoading());
+       const resources = $derived(mcpResourceStore.serverResources);
+       const isLoading = $derived(mcpResourceStore.isLoading);
 
        const filteredResources = $derived.by(() => {
                if (!searchQuery.trim()) {
index 7b349043b84f256bd6d6b02f2df149f2790c32c7..f691202362b3745ca5a45298f51958d493efc257 100644 (file)
@@ -10,7 +10,7 @@
        import { Checkbox } from '$lib/components/ui/checkbox';
        import * as Collapsible from '$lib/components/ui/collapsible';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { MCPResourceInfo, MCPResourceTemplateInfo, MCPServerResources } from '$lib/types';
        import { getDisplayName, getResourceIcon } from '$lib/utils';
        import { SvelteSet } from 'svelte/reactivity';
index 6072e38660f834a644fd4086b0dd95443c836662..0ba56caf28bdfb83cb489bd173a156351da13064 100644 (file)
@@ -12,7 +12,7 @@
        import { Skeleton } from '$lib/components/ui/skeleton';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
        import { HealthCheckStatus } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { HealthCheckState, MCPServerSettingsEntry } from '$lib/types';
        import { tick } from 'svelte';
 
index 5a5b01ffe3d4eed8965dfbf38e0cb41c5b6daebd..f3c1551caebe86baecf9de2183b73d9192424e50 100644 (file)
@@ -4,7 +4,7 @@
        import { Switch } from '$lib/components/ui/switch';
        import { CLI_FLAGS, HEADERS, MCP_SERVER_URL_PLACEHOLDER } from '$lib/constants';
        import { UrlProtocol } from '$lib/enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
+       import { mcpStore } from '$lib/stores';
        import type { KeyValuePair } from '$lib/types';
        import { parseHeadersToArray, serializeHeaders } from '$lib/utils';
 
index 969c21d5a4daf8a62d024d32d41c7104d08a37f4..58d2160ec60d292932ae4612c5c7be945486195c 100644 (file)
@@ -3,8 +3,7 @@
        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 { modelsStore, serverStore } from '$lib/stores';
 
        interface Props {
                class?: string;
index 042d72106131c9052d6dbfa9f841e186144859be..cae0a7e3ed14e657b641cae534c7af33db8dc267 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 { settingsStore } from '$lib/stores';
 
        interface Props {
                modelId: string;
                'inline-flex w-fit shrink-0 items-center justify-center whitespace-nowrap rounded-md border border-border/50 px-1 py-0 text-[10px] font-mono text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground';
 
        let parsed = $derived(ModelsService.parseModelId(modelId));
-       let resolvedShowRaw = $derived(showRaw ?? (config().showRawModelNames as boolean) ?? false);
-       let resolvedHideQuantization = $derived(hideQuantization ?? !config().showModelQuantization);
-       let resolvedHideTags = $derived(hideTags ?? !config().showModelTags);
+       let resolvedShowRaw = $derived(
+               showRaw ?? (settingsStore.config.showRawModelNames as boolean) ?? false
+       );
+       let resolvedHideQuantization = $derived(
+               hideQuantization ?? !settingsStore.config.showModelQuantization
+       );
+       let resolvedHideTags = $derived(hideTags ?? !settingsStore.config.showModelTags);
 
        let uniqueAliases = $derived([...new Set(aliases ?? [])]);
        let uniqueTags = $derived([...new Set([...(parsed.tags ?? []), ...(tags ?? [])])]);
index f1dfd60c154bc4cab9ac654f293105703f08b8e0..c23bca1aebff1363d617bf18c80f92116a279a35 100644 (file)
@@ -14,7 +14,7 @@
        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 { modelsStore } from '$lib/stores';
        import { modelLoadFraction } from '$lib/utils';
 
        interface Props {
                        return;
                }
 
-               const model = routerModels().find((m) => m.id === modelId);
+               const model = modelsStore.routerModels.find((m) => m.id === modelId);
                const status = model?.status?.value as ServerModelStatus | undefined;
 
                if (status === ServerModelStatus.LOADING) return;
                {@const selectedOption = ms.getDisplayOption()}
                {@const triggerModel = selectedOption?.model}
                {@const triggerStatus = triggerModel
-                       ? routerModels().find((m) => m.id === triggerModel)?.status?.value
+                       ? modelsStore.routerModels.find((m) => m.id === triggerModel)?.status?.value
                        : undefined}
                {@const triggerLoading =
                        !!triggerModel &&
index c7ce50d29a482fd59a96dcbc154fce16e6998046..38f4db8a727e807f6c4ca87332f1108a7b71aacb 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import type { GroupedModelOptions, ModelItem } from './utils';
        import { ModelsSelectorOption } from '$lib/components/app';
-       import { modelsStore } from '$lib/stores/models.svelte';
+       import { modelsStore } from '$lib/stores';
 
        interface Props {
                groups: GroupedModelOptions;
index 322c03efa8b7b34374ea548dea284dcb8937aaa2..18c885a62cb704725f50d9e919c82dd7daf5e1ac 100644 (file)
@@ -13,7 +13,7 @@
        import { ActionIcon, ModelId } from '$lib/components/app';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
        import { ServerModelStatus } from '$lib/enums';
-       import { modelsStore, routerModels } from '$lib/stores/models.svelte';
+       import { modelsStore } from '$lib/stores';
        import type { ModelOption } from '$lib/types/models';
        import { modelLoadFraction, modelLoadProgressText } from '$lib/utils';
 
@@ -41,7 +41,7 @@
                option
        }: Props = $props();
 
-       let currentRouterModels = $derived(routerModels());
+       let currentRouterModels = $derived(modelsStore.routerModels);
        let serverStatus = $derived.by(() => {
                const model = currentRouterModels.find((m) => m.id === option.model);
 
index db08313b2928372f4a9b2a17dda24f43aab8e20c..7228a2e74a795ca397c2a4bb71cdf2aa9d604ef5 100644 (file)
@@ -10,7 +10,7 @@
        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 { modelsStore } from '$lib/stores';
        import { modelLoadFraction } from '$lib/utils';
 
        interface Props {
@@ -67,7 +67,7 @@
                {@const selectedOption = ms.getDisplayOption()}
                {@const triggerModel = selectedOption?.model}
                {@const triggerStatus = triggerModel
-                       ? routerModels().find((m) => m.id === triggerModel)?.status?.value
+                       ? modelsStore.routerModels.find((m) => m.id === triggerModel)?.status?.value
                        : undefined}
                {@const triggerLoading =
                        !!triggerModel &&
index dfd3d9077a59ac102e1cc701459ac2a131ad8f6d..260ff985b2804882bd045fdfe3441d5eef45c70b 100644 (file)
        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,
-               conversations,
-               conversationsStore
-       } from '$lib/stores/conversations.svelte';
-       import { device } from '$lib/stores/device.svelte';
-       import { config } from '$lib/stores/settings.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { chatStore, conversationsStore, device, isMobile, settingsStore } from '$lib/stores';
+       import { buildConversationTree } from '$lib/utils';
        import { circIn } from 'svelte/easing';
        import { SvelteSet } from 'svelte/reactivity';
        import { fade } from 'svelte/transition';
@@ -44,7 +37,7 @@
 
        const isStripExpanded = $derived(isExpandedMode || hoveredTooltip !== null);
        const isOnMobile = $derived(isMobile.current);
-       const alwaysShowOnDesktop = $derived(config().alwaysShowSidebarOnDesktop as boolean);
+       const alwaysShowOnDesktop = $derived(settingsStore.config.alwaysShowSidebarOnDesktop as boolean);
 
        $effect(() => {
                if (alwaysShowOnDesktop && !isOnMobile) {
@@ -84,7 +77,7 @@
        let filteredConversations = $derived.by(() => {
                if (isSearchModeActive) {
                        if (searchQuery.trim().length > 0) {
-                               return conversations().filter((conversation: { name: string }) =>
+                               return conversationsStore.conversations.filter((conversation: { name: string }) =>
                                        conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
                                );
                        }
@@ -92,7 +85,7 @@
                        return [];
                }
 
-               return conversations();
+               return conversationsStore.conversations;
        });
 
        let isSelectionMode = $state(false);
        const allSelectedArePinned = $derived.by(() => {
                if (selectedIds.size === 0) return false;
 
-               const convs = conversations();
+               const convs = conversationsStore.conversations;
 
                for (const id of selectedIds) {
                        const c = convs.find((conv) => conv.id === id);
        const pinStateIsMixed = $derived.by(() => {
                if (selectedIds.size === 0) return false;
 
-               const convs = conversations();
+               const convs = conversationsStore.conversations;
 
                let anyPinned = false;
                let anyUnpinned = false;
        }
 
        async function handleEditConversation(id: string) {
-               const conversation = conversations().find((conv) => conv.id === id);
+               const conversation = conversationsStore.conversations.find((conv) => conv.id === id);
 
                if (!conversation) return;
 
        }
 
        async function handleDeleteConversation(id: string) {
-               const conversation = conversations().find((conv) => conv.id === id);
+               const conversation = conversationsStore.conversations.find((conv) => conv.id === id);
 
                if (!conversation) return;
 
index dbb0291adcbdf7cecf1053066b4852ae6df3b608..29880f0dde504ef5cce3e8ab7f95971257b6f0c9 100644 (file)
@@ -12,7 +12,7 @@
                SIDEBAR_ACTIONS_ITEMS
        } from '$lib/constants';
        import { TooltipSide } from '$lib/enums';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { isMobile } from '$lib/stores';
        import type { Component } from 'svelte';
        import { onMount } from 'svelte';
        import { circIn } from 'svelte/easing';
index 2f4b297d8556d4410f21ff16f95b34f671066389..5190badd67f2d0b994fc91350fea1fbf4f7fd7a7 100644 (file)
@@ -17,8 +17,7 @@
        import * as Tooltip from '$lib/components/ui/tooltip';
        import { FORK_TREE_DEPTH_PADDING, ICON_CLASS_DEFAULT } from '$lib/constants';
        import { RouterService } from '$lib/services/router.service';
-       import { getAllLoadingChats } from '$lib/stores/chat.svelte';
-       import { conversationsStore } from '$lib/stores/conversations.svelte';
+       import { chatStore, conversationsStore } from '$lib/stores';
        import { onMount } from 'svelte';
 
        interface Props {
@@ -56,7 +55,7 @@
        let renderActionsDropdown = $state(false);
        let dropdownOpen = $state(false);
 
-       let isLoading = $derived(getAllLoadingChats().includes(conversation.id));
+       let isLoading = $derived(chatStore.getAllLoadingChats().includes(conversation.id));
 
        function handleEdit(event: Event) {
                event.stopPropagation();
index fead08c1326d799100ef906e824249693f38b4e2..d502248f93d9ef26b7990d2cc1f7a5e11279eb38 100644 (file)
@@ -3,7 +3,7 @@
        import SidebarNavigationSearchResults from './SidebarNavigationSearchResults.svelte';
        import SidebarNavigationSelectionBar from './SidebarNavigationSelectionBar.svelte';
        import { Pin } from '@lucide/svelte';
-       import { buildConversationTree } from '$lib/stores/conversations.svelte';
+       import { buildConversationTree } from '$lib/utils';
 
        interface Props {
                class: string;
index af952899fc0e34e0a45d72e999ed4704a68c3fee..f56ac6af7f2fd0c91e5205671d3290754d5c96f9 100644 (file)
@@ -1,6 +1,6 @@
 <script lang="ts">
        import SidebarNavigationConversationItem from './SidebarNavigationConversationItem.svelte';
-       import { buildConversationTree } from '$lib/stores/conversations.svelte';
+       import { buildConversationTree } from '$lib/utils';
 
        interface Props {
                class?: string;
index 28bb2cfe144d11d664b6c0221f9f2281ffdd3919..a2e23eecddf25b65f4b010cae163612bf552796f 100644 (file)
@@ -7,8 +7,7 @@
        import Label from '$lib/components/ui/label/label.svelte';
        import { HEADERS, ICON_CLASS_DEFAULT, ROUTES, SETTINGS_KEYS } from '$lib/constants';
        import { KeyboardKey } from '$lib/enums';
-       import { serverLoading, serverStore } from '$lib/stores/server.svelte';
-       import { config, settingsStore } from '$lib/stores/settings.svelte';
+       import { serverStore, settingsStore } from '$lib/stores';
        import { fade, fly, scale } from 'svelte/transition';
 
        interface Props {
@@ -27,7 +26,7 @@
                showTroubleshooting = false
        }: Props = $props();
 
-       let isServerLoading = $derived(serverLoading());
+       let isServerLoading = $derived(serverStore.loading);
        let isAccessDeniedError = $derived(
                error.toLowerCase().includes('access denied') ||
                        error.toLowerCase().includes('invalid api key') ||
@@ -52,7 +51,7 @@
        function handleShowApiKeyInput() {
                showApiKeyInput = true;
                // Pre-fill with current API key if it exists
-               const currentConfig = config();
+               const currentConfig = settingsStore.config;
 
                apiKeyInput = currentConfig.apiKey?.toString() || '';
        }
index acb128d0ba99b8cbe52bc2653b70f493e42d5afe..a26f50b5b9a49a53eec9f2cbc36051216a7c944e 100644 (file)
@@ -3,8 +3,7 @@
        import { Badge } from '$lib/components/ui/badge';
        import { Button } from '$lib/components/ui/button';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
-       import { singleModelName } from '$lib/stores/models.svelte';
-       import { serverError, serverLoading, serverProps } from '$lib/stores/server.svelte';
+       import { modelsStore, serverStore } from '$lib/stores';
 
        interface Props {
                class?: string;
 
        let { class: className = '', showActions = false }: Props = $props();
 
-       let error = $derived(serverError());
-       let loading = $derived(serverLoading());
-       let model = $derived(singleModelName());
-       let serverData = $derived(serverProps());
+       let error = $derived(serverStore.error);
+       let loading = $derived(serverStore.loading);
+       let model = $derived(modelsStore.singleModelName);
+       let serverData = $derived(serverStore.props);
 
        function getStatusColor() {
                if (loading) return 'bg-yellow-500';
index 639e3e5a7562b8320a925fc17c8bb2c0841bf7f4..83ceec93a4be8245b4409e822129d4a11d77fb60 100644 (file)
        import { setChatSettingsConfigContext } from '$lib/contexts';
        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 { modelsStore, serverStore, settingsReferrer, settingsStore } from '$lib/stores';
        import type { SettingsSection } from '$lib/types';
        import { setMode } from 'mode-watcher';
        import { fade } from 'svelte/transition';
                        SETTINGS_CHAT_SECTIONS[0]
        );
 
-       let localConfig: SettingsConfigType = $state({ ...config() });
+       let localConfig: SettingsConfigType = $state({ ...settingsStore.config });
 
        let mobileHeader: { updateCarousel: () => void } | undefined;
 
        let fetchInitiated = false;
 
        $effect(() => {
-               if (isRouterMode() && currentSection.fields && !fetchInitiated) {
+               if (serverStore.isRouterMode && currentSection.fields && !fetchInitiated) {
                        fetchInitiated = true;
 
                        void modelsStore
@@ -71,7 +68,7 @@
        }
 
        function handleReset() {
-               localConfig = { ...config() };
+               localConfig = { ...settingsStore.config };
                setMode(localConfig.theme as ColorMode);
                mobileHeader?.updateCarousel();
        }
        }
 
        export function reset() {
-               localConfig = { ...config() };
+               localConfig = { ...settingsStore.config };
        }
 
        setChatSettingsConfigContext({
index 7d1791e18a7a2b8c15232b537c21319f35fb7f9f..bc9adea4208dd8dfecf54e196e9b4681872a2644 100644 (file)
@@ -9,9 +9,7 @@
        import { Textarea } from '$lib/components/ui/textarea';
        import { ICON_CLASS_DEFAULT, SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants';
        import { SettingsFieldType } from '$lib/enums/settings.enums';
-       import { modelsStore, propsCacheVersion, selectedModelName } from '$lib/stores/models.svelte';
-       import { serverStore } from '$lib/stores/server.svelte';
-       import { settingsStore } from '$lib/stores/settings.svelte';
+       import { modelsStore, serverStore, settingsStore } from '$lib/stores';
        import { normalizeFloatingPoint } from '$lib/utils/precision';
        import type { Component } from 'svelte';
 
        let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
 
        let currentModelParams = $derived.by(() => {
-               propsCacheVersion();
+               void modelsStore.propsCacheVersion;
 
                if (serverStore.isRouterMode) {
-                       const currentModelName = selectedModelName();
+                       const currentModelName = modelsStore.selectedModelName;
 
                        if (currentModelName) {
                                const currentModelProps = modelsStore.getModelProps(currentModelName);
index 42c7ee55aeb3a34efdf882f8996785750269d4eb..7356d3546960f01dbbb5e8cdc525f7fd9b8e8436 100644 (file)
@@ -8,8 +8,7 @@
        } from '$lib/components/app';
        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, settingsStore } from '$lib/stores';
        import { createMessageCountMap } from '$lib/utils';
        import { fade } from 'svelte/transition';
        import { toast } from 'svelte-sonner';
 
        async function handleExportClick() {
                try {
-                       const allConversations = conversations();
+                       const allConversations = conversationsStore.conversations;
 
                        if (allConversations.length === 0) {
                                toast.info('No conversations to export');
 
        async function handleDeleteAllClick() {
                try {
-                       const allConversations = conversations();
+                       const allConversations = conversationsStore.conversations;
 
                        if (allConversations.length === 0) {
                                toast.info('No conversations to delete');
index c4682f174c4e6d67f2b82c99df7142d33e1d01bc..53cd769126454c003c28992eb2161421f5885bb6 100644 (file)
@@ -5,9 +5,7 @@
        import * as Collapsible from '$lib/components/ui/collapsible';
        import { ICON_CLASS_DEFAULT } from '$lib/constants';
        import { ToolSource } from '$lib/enums/tools.enums';
-       import { mcpStore } from '$lib/stores/mcp.svelte';
-       import { permissionsStore } from '$lib/stores/permissions.svelte';
-       import { toolsStore } from '$lib/stores/tools.svelte';
+       import { mcpStore, permissionsStore, toolsStore } from '$lib/stores';
        import { getBuiltinToolUi } from '$lib/utils';
        import { SvelteSet } from 'svelte/reactivity';
 
index 202335f0b08799b0ba6f35a84d60b1d890bb5179..e5c8dcd2e528229ca6c9484eb762f20d29941435 100644 (file)
@@ -2,7 +2,7 @@
        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 { settingsStore } from '$lib/stores';
 
        interface Props {
                onReset?: () => void;
index 89cecb9267c73ee83e8e1c818c8443b59f01e779..4ea4285322b12865e02fafad4fc33ca6dbbc223d 100644 (file)
@@ -10,9 +10,7 @@
        import * as Empty from '$lib/components/ui/empty';
        import { ROUTES } from '$lib/constants';
        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 { conversationsStore, mcpStore, toolsStore } from '$lib/stores';
        import { onMount } from 'svelte';
        import { fade } from 'svelte/transition';
 
index 0f91f822f049da9725f294d850b5dbcc37989343..940d22d4284514449f06f9b18a26596cac248498 100644 (file)
@@ -8,24 +8,21 @@
  * demand if they aren't cached yet.
  */
 
-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';
+import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
 
 export function useChatScreenActiveModel() {
-       const isRouter = $derived(isRouterMode());
+       const isRouter = $derived(serverStore.isRouterMode);
        const conversationModel = $derived(
-               chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
+               chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
        );
        const activeModelId = $derived.by(() => {
-               const options = modelOptions();
+               const options = modelsStore.models;
 
                if (!isRouter) {
                        return options.length > 0 ? options[0].model : null;
                }
 
-               const selectedId = selectedModelId();
+               const selectedId = modelsStore.selectedModelId;
 
                if (selectedId) {
                        const model = options.find((m) => m.id === selectedId);
index e3324f91519180d23419d5c1b8aac4934e3533a2..47356a63fc2b8def2351f6053d67e05d6829fa9e 100644 (file)
@@ -7,7 +7,7 @@
  * caller's onDrop callback.
  */
 
-import { getAddFilesHandler, isEditing } from '$lib/stores/chat.svelte';
+import { chatStore } from '$lib/stores';
 
 interface UseChatScreenDragAndDropOptions {
        /** Called when the user drops files and no message is being edited. */
@@ -49,8 +49,8 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
 
                const files = Array.from(event.dataTransfer.files);
 
-               if (isEditing()) {
-                       const handler = getAddFilesHandler();
+               if (chatStore.isEditing()) {
+                       const handler = chatStore.getAddFilesHandler();
 
                        if (handler) {
                                handler(files);
index 5bb1ea33f8b4814d0f368ca8fbd7c3acb7769b74..30261f73d3ed3d217f0fa0febef2f6ac55e4ef4e 100644 (file)
@@ -88,7 +88,9 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
        }
 
        return {
-               fileErrorData,
+               get fileErrorData() {
+                       return fileErrorData;
+               },
                handleFileRemove,
                handleFileUpload,
                get showFileErrorDialog() {
index 372f314aaeca49ba4c5f688a71495518c6aa635b..07d380224d18b5217bdd5810ca25091cdd021226 100644 (file)
@@ -1,30 +1,14 @@
 /**
- * Reactive state for the context usage gauge: resolves the active model,
- * fetches its cached props, parses live server stats, and exposes per-turn
- * read / fresh / cache / output and cumulative token counts.
+ * View layer over contextStatsStore for the context usage gauge: adds
+ * color levels, transient detail formatting, on-demand /props fetching
+ * and model loading on top of the store's token stats.
  */
 
 import { useProcessingState } from './use-processing-state.svelte';
 import { colorLevelFromPercent } from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge';
 import { STATS_UNITS } from '$lib/constants';
-import { ColorLevel, MessageRole } from '$lib/enums';
-import { chatStore } from '$lib/stores/chat.svelte';
-import { activeMessages } from '$lib/stores/conversations.svelte';
-import {
-       modelOptions,
-       modelsStore,
-       selectedModelId,
-       singleModelName
-} from '$lib/stores/models.svelte';
-import { isRouterMode } from '$lib/stores/server.svelte';
-import type { ChatMessageTimings, DatabaseMessage } from '$lib/types';
-
-interface LiveStats {
-       freshTokens: number;
-       promptTokens: number;
-       cacheTokens: number;
-       outputTokens: number;
-}
+import { ColorLevel } from '$lib/enums';
+import { contextStatsStore, modelsStore } from '$lib/stores';
 
 export interface UseContextGaugeReturn {
        readonly activeModelId: string | null;
@@ -32,6 +16,7 @@ export interface UseContextGaugeReturn {
        readonly isActiveModelLoading: boolean;
        readonly contextTotal: number | null;
        readonly contextUsed: number;
+       readonly contextAvailable: number | null;
        readonly currentRead: number;
        readonly currentFresh: number;
        readonly currentCache: number;
@@ -49,34 +34,6 @@ export interface UseContextGaugeReturn {
        startMonitoring(): void;
 }
 
-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;
-}
-
-function deriveLiveStats(
-       state: ReturnType<typeof useProcessingState>['processingState']
-): LiveStats | null {
-       if (!state || (state.status !== 'preparing' && state.status !== 'generating')) {
-               return null;
-       }
-
-       const promptTokens = state.promptTokens ?? 0;
-       const cacheTokens = state.cacheTokens ?? 0;
-
-       return {
-               cacheTokens,
-               freshTokens: promptTokens,
-               outputTokens: state.outputTokensUsed ?? 0,
-               promptTokens: promptTokens + cacheTokens
-       };
-}
-
 const TRANSIENT_DETAILS_EXCLUDED_PREFIXES = ['Context:', 'Output:'];
 
 function filterTransientDetails(raw: string[]): string[] {
@@ -91,150 +48,39 @@ function filterTransientDetails(raw: string[]): string[] {
 
 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(() => {
-               if (!isRouterMode()) {
-                       return singleModelName();
-               }
-
-               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 && (!isRouterMode() || modelsStore.isModelLoaded(activeModelId))
-       );
-       const isActiveModelLoading = $derived(
-               activeModelId !== null && modelsStore.isModelOperationInProgress(activeModelId)
-       );
 
        // Pull /props on demand so n_ctx surfaces before the first chat request.
        $effect(() => {
-               if (activeModelId && isActiveModelLoaded) {
-                       const cached = modelsStore.getModelProps(activeModelId);
+               const modelId = contextStatsStore.activeModelId;
+
+               if (modelId && contextStatsStore.isActiveModelLoaded) {
+                       const cached = modelsStore.getModelProps(modelId);
 
                        if (!cached) {
-                               void modelsStore.fetchModelProps(activeModelId);
+                               void modelsStore.fetchModelProps(modelId);
                        }
                }
        });
 
-       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(
-                       (m) => m.role === MessageRole.ASSISTANT && m.timings?.agentic?.llm?.predicted_n != null
-               );
-
-               if (agenticMessages.length > 0) {
-                       const llm = agenticMessages[agenticMessages.length - 1].timings!.agentic!.llm;
-                       const output = llm.predicted_n ?? 0;
-                       const outputMs = llm.predicted_ms ?? 0;
-                       const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
-
-                       return {
-                               averageTokensPerSecond,
-                               cacheTotal: 0,
-                               output,
-                               read: llm.prompt_n ?? 0
-                       };
-               }
-
-               let read = 0;
-               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 { 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));
+       const colorLevel = $derived(colorLevelFromPercent(contextStatsStore.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 ||
-                       currentRead > 0 ||
-                       currentOutput > 0 ||
-                       cumulative.averageTokensPerSecond !== null ||
+               contextStatsStore.cumulativeRead > 0 ||
+                       contextStatsStore.cumulativeOutput > 0 ||
+                       contextStatsStore.currentRead > 0 ||
+                       contextStatsStore.currentOutput > 0 ||
+                       contextStatsStore.averageTokensPerSecond !== null ||
                        transientDetails.length > 0
        );
 
        async function loadModel() {
-               if (!activeModelId || isActiveModelLoading) return;
+               const modelId = contextStatsStore.activeModelId;
+
+               if (!modelId || contextStatsStore.isActiveModelLoading) return;
 
                try {
-                       await modelsStore.loadModel(activeModelId);
+                       await modelsStore.loadModel(modelId);
                } catch {
                        // toast already surfaced by modelsStore.loadModel
                }
@@ -242,55 +88,58 @@ export function useContextGauge(): UseContextGaugeReturn {
 
        return {
                get activeModelId() {
-                       return activeModelId;
+                       return contextStatsStore.activeModelId;
                },
                get averageTokensPerSecond() {
-                       return cumulative.averageTokensPerSecond;
+                       return contextStatsStore.averageTokensPerSecond;
                },
                get colorLevel() {
                        return colorLevel;
                },
+               get contextAvailable() {
+                       return contextStatsStore.contextAvailable;
+               },
                get contextPercent() {
-                       return contextPercent;
+                       return contextStatsStore.contextPercent;
                },
                get contextTotal() {
-                       return contextTotal;
+                       return contextStatsStore.contextTotal;
                },
                get contextUsed() {
-                       return contextUsed;
+                       return contextStatsStore.contextUsed;
                },
                get cumulativeCacheTotal() {
-                       return cumulative.cacheTotal;
+                       return contextStatsStore.cumulativeCacheTotal;
                },
                get cumulativeOutput() {
-                       return cumulative.output;
+                       return contextStatsStore.cumulativeOutput;
                },
                get cumulativeRead() {
-                       return cumulative.read;
+                       return contextStatsStore.cumulativeRead;
                },
                get currentCache() {
-                       return currentCache;
+                       return contextStatsStore.currentCache;
                },
                get currentFresh() {
-                       return currentFresh;
+                       return contextStatsStore.currentFresh;
                },
                get currentOutput() {
-                       return currentOutput;
+                       return contextStatsStore.currentOutput;
                },
                get currentRead() {
-                       return currentRead;
+                       return contextStatsStore.currentRead;
                },
                get hasAnyUsage() {
                        return hasAnyUsage;
                },
                get isActiveModelLoaded() {
-                       return isActiveModelLoaded;
+                       return contextStatsStore.isActiveModelLoaded;
                },
                get isActiveModelLoading() {
-                       return isActiveModelLoading;
+                       return contextStatsStore.isActiveModelLoading;
                },
                get kvTotal() {
-                       return kvTotal;
+                       return contextStatsStore.kvTotal;
                },
                loadModel,
                startMonitoring: () => processingState.startMonitoring(),
index d7ef140c5776740be15ba5f8a9d26cb107384cf4..567017747740146bb7e449fbf097c6f0ca6553f3 100644 (file)
@@ -1,5 +1,5 @@
 import { afterNavigate, beforeNavigate } from '$app/navigation';
-import { draftMessagesStore } from '$lib/stores/draft-messages.svelte';
+import { draftMessagesStore } from '$lib/stores';
 import { onMount } from 'svelte';
 
 interface UseDraftMessagesOptions {
index f554643c60f7440081479eae78f037abe89bdd66..d56eeefcd3d2bfac110396ac317837bddc226feb 100644 (file)
@@ -1,14 +1,6 @@
 import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils';
 import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
-import {
-       modelOptions,
-       modelsLoading,
-       modelsStore,
-       modelsUpdating,
-       selectedModelId,
-       singleModelName
-} from '$lib/stores/models.svelte';
-import { isRouterMode } from '$lib/stores/server.svelte';
+import { modelsStore, serverStore } from '$lib/stores';
 import type { ModelOption } from '$lib/types/models';
 import { onMount } from 'svelte';
 
@@ -54,17 +46,17 @@ export interface UseModelsSelectorReturn {
  */
 export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn {
        const options = $derived(
-               modelOptions().filter((option) => {
+               modelsStore.models.filter((option) => {
                        const modelProps = modelsStore.getModelProps(option.model);
 
                        return modelProps?.ui !== false;
                })
        );
-       const loading = $derived(modelsLoading());
-       const updating = $derived(modelsUpdating());
-       const activeId = $derived(selectedModelId());
-       const isRouter = $derived(isRouterMode());
-       const serverModel = $derived(singleModelName());
+       const loading = $derived(modelsStore.loading);
+       const updating = $derived(modelsStore.updating);
+       const activeId = $derived(modelsStore.selectedModelId);
+       const isRouter = $derived(serverStore.isRouterMode);
+       const serverModel = $derived(modelsStore.singleModelName);
        const currentModel = $derived(opts.currentModel());
        const onModelChange = $derived(opts.onModelChange?.());
        const isHighlightedCurrentModelActive = $derived.by(() => {
index 0087c5683daa7c28d6a815cbc00fd0903cfd691f..37e0748bcbe96c3d571c6b184d57fc4d21c815af 100644 (file)
@@ -1,5 +1,5 @@
 import { STATS_UNITS } from '$lib/constants';
-import { activeProcessingState } from '$lib/stores/chat.svelte';
+import { chatStore } from '$lib/stores';
 import type { ApiProcessingState, LiveGenerationStats, LiveProcessingStats } from '$lib/types';
 
 export interface UseProcessingStateReturn {
@@ -42,8 +42,8 @@ export function useProcessingState(): UseProcessingStateReturn {
                        return lastKnownState;
                }
 
-               // Read directly from the reactive state export
-               return activeProcessingState();
+               // Read directly from the reactive state
+               return chatStore.activeProcessingState;
        });
 
        $effect(() => {
index 2bad85aacd7cece491c615a0c80cdcdca0308f91..47359afd9cdd7af191bff8ba4e96fb2145904e4f 100644 (file)
@@ -1,6 +1,6 @@
 import { browser } from '$app/environment';
 import { BUILD_VERSION_LOCALSTORAGE_KEY, SW_CONFIG } from '$lib/constants';
-import { versionStore } from '$lib/stores/version.svelte';
+import { versionStore } from '$lib/stores';
 import { useRegisterSW } from 'virtual:pwa-register/svelte';
 
 /**
index 399a08528d79ce0d1a2bf87a3612594f2fcffe99..0feba69c14395b1c174c4474aff396920d8012f4 100644 (file)
@@ -1,15 +1,6 @@
 import { REASONING_EFFORT_LEVELS, REASONING_EFFORT_TOKENS } from '$lib/constants';
 import { ReasoningEffort } from '$lib/enums';
-import { chatStore } from '$lib/stores/chat.svelte';
-import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
-import {
-       checkModelSupportsThinking,
-       loadedModelIds,
-       modelsStore,
-       propsCacheVersion,
-       supportsThinking
-} from '$lib/stores/models.svelte';
-import { isRouterMode } from '$lib/stores/server.svelte';
+import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
 import type { ReasoningEffortLevel } from '$lib/types';
 import type { DatabaseMessage } from '$lib/types/database';
 
@@ -33,12 +24,14 @@ export interface UseReasoningMenuReturn {
  */
 export function useReasoningMenu(): UseReasoningMenuReturn {
        const conversationModel = $derived(
-               chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
+               chatStore.getConversationModel(conversationsStore.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;
+               const modelId = serverStore.isRouterMode
+                       ? modelsStore.selectedModelName || conversationModel
+                       : null;
 
                if (!modelId) return false;
 
@@ -47,16 +40,18 @@ export function useReasoningMenu(): UseReasoningMenuReturn {
                );
        });
        const modelSupportsThinking = $derived.by(() => {
-               loadedModelIds();
-               propsCacheVersion();
+               void modelsStore.loadedModelIds;
+               void modelsStore.propsCacheVersion;
 
-               if (isRouterMode()) {
+               if (serverStore.isRouterMode) {
                        const modelId = modelsStore.selectedModelName || conversationModel;
 
-                       return checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages;
+                       return (
+                               modelsStore.checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages
+                       );
                }
 
-               return supportsThinking() || modelSupportsThinkingFromMessages;
+               return modelsStore.supportsThinking || modelSupportsThinkingFromMessages;
        });
        const currentEffort = $derived(conversationsStore.getReasoningEffort());
        const thinkingEnabled = $derived(
index efa7bcbc1ec5c9a618956ce906fd63f087f8f1d1..b1b0456a838808b04a69cb311a6dfc17802faa09 100644 (file)
@@ -1,7 +1,7 @@
 import { beforeNavigate } from '$app/navigation';
 import { page } from '$app/state';
 import { ROUTES } from '$lib/constants';
-import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
+import { settingsReferrer } from '$lib/stores';
 
 export interface ChatSettings {
        reset: () => void;
index 1ffb2a4d4f7a4973f75ab188a24d49135e2b4d72..e60c95429a02547218cb2a4d8c998604ec575972 100644 (file)
@@ -1,8 +1,6 @@
 import { CLI_FLAGS } from '$lib/constants';
 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 { conversationsStore, mcpStore, toolsStore } from '$lib/stores';
 import type { ToolGroup } from '$lib/types';
 import { SvelteSet } from 'svelte/reactivity';
 
index 7fb66e339fe01679a3b77b94237e1f2a0d0dd5b4..c0b7e9c60c1851cbf1c6c506072ae753cb695930 100644 (file)
@@ -8,7 +8,7 @@ import {
        SANDBOX_TOOL_NAME,
        SANDBOX_TRUNCATION_NOTICE
 } from '$lib/constants';
-import { config } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import type { ToolExecutionResult } from '$lib/types';
 
 /** Cached harnesses keyed by whether nerdamer is included. */
@@ -20,7 +20,7 @@ const harnessCache: Record<string, string> = {};
  * prelude. Cached per variant so toggling the setting is instant.
  */
 async function getHarness(): Promise<string> {
-       const enabled = !!config().symbolicMathEnabled;
+       const enabled = !!settingsStore.config.symbolicMathEnabled;
        const key = enabled ? 'nerdamer' : 'plain';
 
        if (!harnessCache[key]) {
index cba3b586fa50ad8b6393402b8a79094e2c0e4aa9..075c46c97504a3eba46bac36874c9e2a18738200 100644 (file)
@@ -42,11 +42,12 @@ import { ChatService } from '$lib/services';
 import { ReadMediaService } from '$lib/services/read-media.service';
 import { SandboxService } from '$lib/services/sandbox.service';
 import { ToolsService } from '$lib/services/tools.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 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 { settingsStore } from '$lib/stores/settings.svelte';
 import { toolsStore } from '$lib/stores/tools.svelte';
 import type {
        AgenticConfig,
@@ -89,8 +90,10 @@ function createDefaultSession(): AgenticSession {
        return {
                currentTurn: 0,
                executingToolCallId: null,
+               flowRootMessageId: null,
                isRunning: false,
                lastError: null,
+               liveLlm: null,
                pendingPermissionRequest: null,
                streamingToolCall: null,
                totalToolCalls: 0
@@ -206,6 +209,16 @@ class AgenticStore {
                return this._sessions.get(conversationId)?.isRunning ?? false;
        }
 
+       // read-only: safe to call from derivations, unlike getSession
+       getLiveLlmTotals(conversationId: string): AgenticSession['liveLlm'] {
+               return this._sessions.get(conversationId)?.liveLlm ?? null;
+       }
+
+       // read-only: safe to call from derivations, unlike getSession
+       getFlowRootMessageId(conversationId: string): string | null {
+               return this._sessions.get(conversationId)?.flowRootMessageId ?? null;
+       }
+
        currentTurn(conversationId: string): number {
                return this._sessions.get(conversationId)?.currentTurn ?? 0;
        }
@@ -419,7 +432,15 @@ class AgenticStore {
        }
 
        async runAgenticFlow(params: AgenticFlowParams): Promise<AgenticFlowResult> {
-               const { callbacks, conversationId, messages, options = {}, perChatOverrides, signal } = params;
+               const {
+                       callbacks,
+                       conversationId,
+                       flowRootMessageId,
+                       messages,
+                       options = {},
+                       perChatOverrides,
+                       signal
+               } = params;
 
                // Clear any pending permissions/continue requests for this conversation when starting a new flow
                this._pendingPermissions.set(conversationId, null);
@@ -433,7 +454,7 @@ class AgenticStore {
                        await toolsStore.fetchBuiltinTools();
                }
 
-               const agenticConfig = this.getConfig(config(), perChatOverrides);
+               const agenticConfig = this.getConfig(settingsStore.config, perChatOverrides);
 
                if (!agenticConfig.enabled) return { handled: false };
 
@@ -478,8 +499,10 @@ class AgenticStore {
 
                this.updateSession(conversationId, {
                        currentTurn: 0,
+                       flowRootMessageId: flowRootMessageId ?? null,
                        isRunning: true,
                        lastError: null,
+                       liveLlm: null,
                        totalToolCalls: 0
                });
 
@@ -505,7 +528,11 @@ class AgenticStore {
 
                        return { error: normalizedError, handled: true };
                } finally {
-                       this.updateSession(conversationId, { isRunning: false });
+                       this.updateSession(conversationId, {
+                               flowRootMessageId: null,
+                               isRunning: false,
+                               liveLlm: null
+                       });
 
                        if (hasMcpServers) {
                                await mcpStore
@@ -633,6 +660,16 @@ class AgenticStore {
                                                        if (timings) {
                                                                capturedTimings = timings;
                                                                turnTimings = timings;
+
+                                                               // completed turns + in-flight turn live counts
+                                                               this.updateSession(conversationId, {
+                                                                       liveLlm: {
+                                                                               predicted_ms: agenticTimings.llm.predicted_ms + (timings.predicted_ms ?? 0),
+                                                                               predicted_n: agenticTimings.llm.predicted_n + (timings.predicted_n ?? 0),
+                                                                               prompt_ms: agenticTimings.llm.prompt_ms + (timings.prompt_ms ?? 0),
+                                                                               prompt_n: agenticTimings.llm.prompt_n + (timings.prompt_n ?? 0)
+                                                                       }
+                                                               });
                                                        }
                                                },
                                                onToolCallChunk: (serialized: string) => {
@@ -1157,71 +1194,3 @@ class AgenticStore {
 }
 
 export const agenticStore = new AgenticStore();
-
-export function agenticIsRunning(conversationId: string) {
-       return agenticStore.isRunning(conversationId);
-}
-
-export function agenticCurrentTurn(conversationId: string) {
-       return agenticStore.currentTurn(conversationId);
-}
-
-export function agenticTotalToolCalls(conversationId: string) {
-       return agenticStore.totalToolCalls(conversationId);
-}
-
-export function agenticLastError(conversationId: string) {
-       return agenticStore.lastError(conversationId);
-}
-
-export function agenticStreamingToolCall(conversationId: string) {
-       return agenticStore.streamingToolCall(conversationId);
-}
-
-export function agenticPendingPermissionRequest(conversationId: string) {
-       return agenticStore.pendingPermissionRequest(conversationId);
-}
-
-export function agenticResolvePermission(conversationId: string, decision: ToolPermissionDecision) {
-       agenticStore.resolvePermission(conversationId, decision);
-}
-
-export function agenticPendingContinueRequest(conversationId: string) {
-       return agenticStore.pendingContinueRequest(conversationId);
-}
-
-export function agenticResolveContinue(conversationId: string, shouldContinue: boolean) {
-       agenticStore.resolveContinue(conversationId, shouldContinue);
-}
-
-export function agenticHasPendingSteeringMessage(conversationId: string) {
-       return agenticStore.hasPendingSteeringMessage(conversationId);
-}
-
-export function agenticInjectSteeringMessage(
-       conversationId: string,
-       content: string,
-       extras?: DatabaseMessageExtra[]
-) {
-       agenticStore.injectSteeringMessage(conversationId, content, extras);
-}
-
-export function agenticPendingSteeringMessageContent(conversationId: string) {
-       return agenticStore.pendingSteeringMessageContent(conversationId);
-}
-
-export function agenticPendingSteeringMessageExtras(conversationId: string) {
-       return agenticStore.pendingSteeringMessageExtras(conversationId);
-}
-
-export function agenticClearSteeringMessage(conversationId: string) {
-       agenticStore.clearSteeringMessage(conversationId);
-}
-
-export function agenticIsAnyRunning() {
-       return agenticStore.isAnyRunning;
-}
-
-export function agenticExecutingToolCallId(conversationId: string) {
-       return agenticStore.executingToolCallId(conversationId);
-}
index 3965c4cbb57adc7ab9d28199998f0b8e175ae704..7234d0027641c5d465ee1482eb6c941dead036cd 100644 (file)
 
 import {
        CONVERSATION_ID_SEPARATOR,
+       CWD_CLEARED_TEXT,
        HEADERS,
        INACTIVE_CONVERSATION,
        STREAM_RESUME_RETRY_MS,
        SYSTEM_MESSAGE_PLACEHOLDER,
        TITLE_GENERATION
 } from '$lib/constants';
-import { MimeTypeApplication } from '$lib/enums';
 import {
        ContinueIntentKind,
        ErrorDialogType,
        MessageRole,
        MessageType,
+       MimeTypeApplication,
        ReasoningEffort,
        StreamConnectionState
 } from '$lib/enums';
 import { ChatService } from '$lib/services/chat.service';
 import { DatabaseService } from '$lib/services/database.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { agenticStore } from '$lib/stores/agentic.svelte';
 import { conversationsStore } from '$lib/stores/conversations.svelte';
 import { mcpStore } from '$lib/stores/mcp.svelte';
-import {
-       modelsStore,
-       selectedModelContextSize,
-       selectedModelName
-} from '$lib/stores/models.svelte';
-import { contextSize, isRouterMode } from '$lib/stores/server.svelte';
-import { config } from '$lib/stores/settings.svelte';
+import { modelsStore } from '$lib/stores/models.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import { toolsStore } from '$lib/stores/tools.svelte';
 import type {
        ApiChatMessageData,
        ApiProcessingState,
        ApiStreamSession,
-       DatabaseMessage,
-       DatabaseMessageExtra
-} from '$lib/types';
-import type {
        ChatMessagePromptProgress,
        ChatMessageTimings,
        ChatStreamCallbacks,
+       DatabaseMessage,
+       DatabaseMessageExtra,
        ErrorDialogState
-} from '$lib/types/chat';
+} from '$lib/types';
 import {
-       CWD_CLEARED_TEXT,
+       classifyContinueIntent,
        filterByLeafNodeId,
        findDescendantMessages,
        findLeafNode,
        findMessageById,
        formatCwdMessage,
        generateConversationTitle,
+       getAuthHeaders,
        isAbortError,
-       normalizeModelName
+       normalizeModelName,
+       streamIdentity
 } 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 {
@@ -186,7 +181,9 @@ class ChatStore {
 
                if (convId === conversationsStore.activeConversation?.id) this.currentResponse = '';
        }
-       private getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
+       private getChatStreamingState(
+               convId: string
+       ): { response: string; messageId: string } | undefined {
                return this.chatStreamingStates.get(convId);
        }
        syncLoadingStateForChat(convId: string): void {
@@ -294,7 +291,7 @@ class ChatStore {
                // 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());
+               const id = streamId || streamIdentity(convId, modelsStore.selectedModelName);
 
                let response: Response;
 
@@ -523,7 +520,11 @@ class ChatStore {
                        // persisted state so the lookup key matches what the server stored. null means a single
                        // 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());
+                       const streamId = ChatService.resumeStreamIdentity(
+                               convId,
+                               localState,
+                               modelsStore.selectedModelName
+                       );
                        // primary path: ask the server which sessions exist for this identity
                        const serverTarget = await this.probeServerStream(streamId);
 
@@ -845,18 +846,14 @@ class ChatStore {
                }
        }
 
-       getChatStreamingPublic(convId: string): { response: string; messageId: string } | undefined {
-               return this.getChatStreaming(convId);
+       getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
+               return this.getChatStreamingState(convId);
        }
 
-       isChatLoadingPublic(convId: string): boolean {
+       isChatLoading(convId: string): boolean {
                return this.chatLoadingStates.get(convId) || false;
        }
 
-       isChatReasoningPublic(convId: string): boolean {
-               return this.chatReasoningStates.get(convId) || false;
-       }
-
        private isChatLoadingInternal(convId: string): boolean {
                return this.chatLoadingStates.has(convId) || this.chatStreamingStates.has(convId);
        }
@@ -1235,7 +1232,7 @@ class ChatStore {
 
                        if (isNewConversation) {
                                const rootId = await DatabaseService.createRootMessage(currentConv.id);
-                               const currentConfig = config();
+                               const currentConfig = settingsStore.config;
                                const systemPrompt = currentConfig.systemMessage?.toString().trim();
 
                                let sysOrRootId = rootId;
@@ -1282,7 +1279,10 @@ class ChatStore {
                        if (isNewConversation && content)
                                await conversationsStore.updateConversationName(
                                        currentConv.id,
-                                       generateConversationTitle(content, Boolean(config().titleGenerationUseFirstLine))
+                                       generateConversationTitle(
+                                               content,
+                                               Boolean(settingsStore.config.titleGenerationUseFirstLine)
+                                       )
                                );
 
                        const assistantMessage = await this.createAssistantMessage(userMessage.id);
@@ -1294,7 +1294,7 @@ class ChatStore {
                                undefined,
                                undefined,
                                undefined,
-                               config().titleGenerationUseLLM && isNewConversation ? content : undefined
+                               settingsStore.config.titleGenerationUseLLM && isNewConversation ? content : undefined
                        );
                } catch (error) {
                        if (isAbortError(error)) {
@@ -1334,13 +1334,13 @@ class ChatStore {
                // and reattach all agree, regardless of fresh send vs regenerate passing a resolved model
                let effectiveModel: string | null | undefined = undefined;
 
-               if (isRouterMode()) {
+               if (serverStore.isRouterMode) {
                        const conversationModel = this.getConversationModel(allMessages);
 
-                       effectiveModel = modelOverride || selectedModelName() || conversationModel;
+                       effectiveModel = modelOverride || modelsStore.selectedModelName || conversationModel;
                }
 
-               if (isRouterMode() && effectiveModel) {
+               if (serverStore.isRouterMode && effectiveModel) {
                        if (!modelsStore.getModelProps(effectiveModel))
                                await modelsStore.fetchModelProps(effectiveModel);
                }
@@ -1582,16 +1582,16 @@ class ChatStore {
 
                                if (onComplete) onComplete(streamedContent);
 
-                               if (isRouterMode()) modelsStore.fetchRouterModels().catch(console.error);
+                               if (serverStore.isRouterMode) modelsStore.fetchRouterModels().catch(console.error);
 
                                // Pre-encode conversation in KV cache for faster next turn
-                               if (config().preEncodeConversation) {
+                               if (settingsStore.config.preEncodeConversation) {
                                        this.triggerPreEncode(
                                                allMessages,
                                                assistantMessage,
                                                streamedContent,
                                                effectiveModel,
-                                               !!config().excludeReasoningFromContext
+                                               !!settingsStore.config.excludeReasoningFromContext
                                        );
                                }
                        },
@@ -1672,6 +1672,7 @@ class ChatStore {
                        const agenticResult = await agenticStore.runAgenticFlow({
                                callbacks: streamCallbacks,
                                conversationId: convId,
+                               flowRootMessageId: assistantMessage.id,
                                messages: allMessages,
                                options: {
                                        ...this.getApiOptions(),
@@ -1739,7 +1740,7 @@ class ChatStore {
 
                                        if (onComplete) await onComplete(content);
 
-                                       if (isRouterMode()) modelsStore.fetchRouterModels().catch(console.error);
+                                       if (serverStore.isRouterMode) modelsStore.fetchRouterModels().catch(console.error);
 
                                        // Generate LLM based title for new conversations (avoids stale reference
                                        // issue when user switches conversations while streaming)
@@ -1810,8 +1811,11 @@ class ChatStore {
                assistantContent: string,
                convId: string
        ): Promise<void> {
-               const effectiveModel = isRouterMode() && selectedModelName() ? selectedModelName() : undefined;
-               const configValue = config();
+               const effectiveModel =
+                       serverStore.isRouterMode && modelsStore.selectedModelName
+                               ? modelsStore.selectedModelName
+                               : undefined;
+               const configValue = settingsStore.config;
                const titlePromptTemplate =
                        typeof configValue.titleGenerationPrompt === 'string' &&
                        configValue.titleGenerationPrompt.trim()
@@ -1853,7 +1857,7 @@ class ChatStore {
 
                if (!conversationId) return;
 
-               const streamingState = this.getChatStreaming(conversationId);
+               const streamingState = this.getChatStreamingState(conversationId);
 
                if (!streamingState) return;
 
@@ -1949,7 +1953,10 @@ class ChatStore {
                        if (isFirstUserMessage && newContent.trim())
                                await conversationsStore.updateConversationName(
                                        activeConv.id,
-                                       generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
+                                       generateConversationTitle(
+                                               newContent,
+                                               Boolean(settingsStore.config.titleGenerationUseFirstLine)
+                                       )
                                );
 
                        const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex + 1);
@@ -2522,7 +2529,10 @@ class ChatStore {
                        if (rootMessage && msg.parent === rootMessage.id && newContent.trim()) {
                                await conversationsStore.updateConversationName(
                                        activeConv.id,
-                                       generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
+                                       generateConversationTitle(
+                                               newContent,
+                                               Boolean(settingsStore.config.titleGenerationUseFirstLine)
+                                       )
                                );
                        }
 
@@ -2607,7 +2617,10 @@ class ChatStore {
                        if (isFirstUserMessage && newContent.trim())
                                await conversationsStore.updateConversationName(
                                        activeConv.id,
-                                       generateConversationTitle(newContent, Boolean(config().titleGenerationUseFirstLine))
+                                       generateConversationTitle(
+                                               newContent,
+                                               Boolean(settingsStore.config.titleGenerationUseFirstLine)
+                                       )
                                );
 
                        await conversationsStore.refreshActiveMessages();
@@ -2665,14 +2678,14 @@ class ChatStore {
                if (activeState && typeof activeState.contextTotal === 'number' && activeState.contextTotal > 0)
                        return activeState.contextTotal;
 
-               if (isRouterMode()) {
-                       const modelContextSize = selectedModelContextSize();
+               if (serverStore.isRouterMode) {
+                       const modelContextSize = modelsStore.selectedModelContextSize;
 
                        if (typeof modelContextSize === 'number' && modelContextSize > 0) {
                                return modelContextSize;
                        }
                } else {
-                       const propsContextSize = contextSize();
+                       const propsContextSize = serverStore.contextSize;
 
                        if (typeof propsContextSize === 'number' && propsContextSize > 0) {
                                return propsContextSize;
@@ -2718,7 +2731,7 @@ class ChatStore {
                        | { total: number; cache: number; processed: number; time_ms: number }
                        | undefined;
                const contextTotal = this.getContextTotal();
-               const currentConfig = config();
+               const currentConfig = settingsStore.config;
                const outputTokensMax = currentConfig.max_tokens || -1;
                const contextUsed = promptTokens + cacheTokens + predictedTokens,
                        outputTokensUsed = predictedTokens;
@@ -2786,13 +2799,13 @@ class ChatStore {
        }
 
        private getApiOptions(): Record<string, unknown> {
-               const currentConfig = config();
+               const currentConfig = settingsStore.config;
                const hasValue = (value: unknown): boolean =>
                        value !== undefined && value !== null && value !== '';
                const apiOptions: Record<string, unknown> = { stream: true, timings_per_token: true };
 
-               if (isRouterMode()) {
-                       const modelName = selectedModelName();
+               if (serverStore.isRouterMode) {
+                       const modelName = modelsStore.selectedModelName;
 
                        if (modelName) apiOptions.model = modelName;
                }
@@ -2910,27 +2923,3 @@ class ChatStore {
 }
 
 export const chatStore = new ChatStore();
-
-export const activeProcessingState = () => chatStore.activeProcessingState;
-export const currentResponse = () => chatStore.currentResponse;
-export const errorDialog = () => chatStore.errorDialogState;
-export const getAddFilesHandler = () => chatStore.getAddFilesHandler();
-export const getAllLoadingChats = () => chatStore.getAllLoadingChats();
-export const getAllStreamingChats = () => chatStore.getAllStreamingChats();
-export const getChatStreaming = (convId: string) => chatStore.getChatStreamingPublic(convId);
-export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(convId);
-export const isChatStreaming = () => chatStore.isStreaming();
-export const isEditing = () => chatStore.isEditing();
-export const isLoading = () => chatStore.isLoading;
-export const isReasoning = () => chatStore.isReasoning;
-export const pendingEditMessageId = () => chatStore.pendingEditMessageId;
-export const chatHasPendingMessage = (convId: string) => chatStore.hasPendingMessage(convId);
-export const chatPendingMessageContent = (convId: string) =>
-       chatStore.pendingMessageContent(convId);
-export const chatPendingMessageExtras = (convId: string) => chatStore.pendingMessageExtras(convId);
-export const chatClearPendingMessage = (convId: string) => chatStore.clearPendingMessage(convId);
-export const chatInjectPendingMessage = (
-       convId: string,
-       content: string,
-       extras?: DatabaseMessageExtra[]
-) => chatStore.injectPendingMessage(convId, content, extras);
diff --git a/tools/ui/src/lib/stores/context-stats.svelte.ts b/tools/ui/src/lib/stores/context-stats.svelte.ts
new file mode 100644 (file)
index 0000000..07f0cd7
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * contextStatsStore - Context window usage stats for the active conversation
+ *
+ * Combines token usage persisted in message timings metadata with
+ * server-originating data: model context size from /props (modelsStore)
+ * and live processing state while streaming (chatStore).
+ */
+
+import { MessageRole } from '$lib/enums';
+// direct imports between stores, not via the barrel, to avoid circular deps
+import { agenticStore } from '$lib/stores/agentic.svelte';
+import { chatStore } from '$lib/stores/chat.svelte';
+import { conversationsStore } from '$lib/stores/conversations.svelte';
+import { modelsStore } from '$lib/stores/models.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
+import type { ApiProcessingState, ChatMessageTimings, DatabaseMessage } from '$lib/types';
+
+interface LiveStats {
+       freshTokens: number;
+       promptTokens: number;
+       cacheTokens: number;
+       outputTokens: number;
+}
+
+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;
+}
+
+function deriveLiveStats(state: ApiProcessingState | null): LiveStats | null {
+       if (!state || (state.status !== 'preparing' && state.status !== 'generating')) {
+               return null;
+       }
+
+       const promptTokens = state.promptTokens ?? 0;
+       const cacheTokens = state.cacheTokens ?? 0;
+
+       return {
+               cacheTokens,
+               freshTokens: promptTokens,
+               outputTokens: state.outputTokensUsed ?? 0,
+               promptTokens: promptTokens + cacheTokens
+       };
+}
+
+class ContextStatsStore {
+       // Resolve the model the stats report context for: explicit selection >
+       // last assistant model > single-model mode (mirrors useChatScreenActiveModel).
+       activeModelId = $derived.by(() => {
+               if (!serverStore.isRouterMode) {
+                       return modelsStore.singleModelName;
+               }
+
+               const selectedId = modelsStore.selectedModelId;
+
+               if (selectedId) {
+                       const model = modelsStore.models.find((m) => m.id === selectedId);
+
+                       if (model) return model.model;
+               }
+
+               return chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[]);
+       });
+
+       isActiveModelLoaded = $derived(
+               this.activeModelId !== null &&
+                       (!serverStore.isRouterMode || modelsStore.isModelLoaded(this.activeModelId))
+       );
+
+       isActiveModelLoading = $derived(
+               this.activeModelId !== null && modelsStore.isModelOperationInProgress(this.activeModelId)
+       );
+
+       contextTotal = $derived.by(() => {
+               void modelsStore.propsCacheVersion;
+
+               return this.activeModelId ? modelsStore.getModelContextSize(this.activeModelId) : null;
+       });
+
+       private liveStats = $derived(deriveLiveStats(chatStore.activeProcessingState));
+
+       currentRead = $derived.by(() => {
+               const timings = lastAssistantTimings(conversationsStore.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 (this.liveStats && this.liveStats.promptTokens > 0) {
+                       read = Math.max(read, this.liveStats.promptTokens);
+               }
+
+               return read;
+       });
+
+       currentFresh = $derived.by(() => {
+               const timings = lastAssistantTimings(conversationsStore.activeMessages as DatabaseMessage[]);
+               const fresh = timings?.prompt_n ?? 0;
+
+               return Math.max(fresh, this.liveStats?.freshTokens ?? 0);
+       });
+
+       currentCache = $derived.by(() => {
+               const timings = lastAssistantTimings(conversationsStore.activeMessages as DatabaseMessage[]);
+               const cached = timings?.cache_n ?? 0;
+
+               if (this.liveStats && this.liveStats.promptTokens > 0) {
+                       return Math.max(cached, this.liveStats.cacheTokens);
+               }
+
+               return cached;
+       });
+
+       currentOutput = $derived.by(() => {
+               if (this.liveStats && this.liveStats.outputTokens > 0) return this.liveStats.outputTokens;
+
+               const timings = lastAssistantTimings(conversationsStore.activeMessages as DatabaseMessage[]);
+
+               return timings?.predicted_n ?? 0;
+       });
+
+       kvTotal = $derived(this.currentRead + this.currentOutput);
+
+       contextUsed = $derived(this.currentRead + this.currentOutput);
+
+       contextAvailable = $derived(
+               this.contextTotal !== null ? this.contextTotal - this.contextUsed : null
+       );
+
+       contextPercent = $derived.by(() => {
+               if (this.contextTotal === null || this.contextTotal <= 0) return null;
+
+               return Math.round((this.contextUsed / this.contextTotal) * 100);
+       });
+
+       private cumulative = $derived.by(() => {
+               const messages = conversationsStore.activeMessages as DatabaseMessage[];
+               const convId = conversationsStore.activeConversation?.id;
+               // A running agentic flow stamps llm totals on messages only when it
+               // exits, so read its live session totals instead.
+               const liveLlm = convId ? agenticStore.getLiveLlmTotals(convId) : null;
+
+               if (liveLlm) {
+                       const outputMs = liveLlm.predicted_ms;
+                       const averageTokensPerSecond =
+                               outputMs > 0 && liveLlm.predicted_n > 0 ? (liveLlm.predicted_n / outputMs) * 1000 : null;
+
+                       return {
+                               averageTokensPerSecond,
+                               cacheTotal: 0,
+                               output: liveLlm.predicted_n,
+                               read: liveLlm.prompt_n
+                       };
+               }
+
+               // 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(
+                       (m) => m.role === MessageRole.ASSISTANT && m.timings?.agentic?.llm?.predicted_n != null
+               );
+
+               if (agenticMessages.length > 0) {
+                       const llm = agenticMessages[agenticMessages.length - 1].timings!.agentic!.llm;
+                       const output = llm.predicted_n ?? 0;
+                       const outputMs = llm.predicted_ms ?? 0;
+                       const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
+
+                       return {
+                               averageTokensPerSecond,
+                               cacheTotal: 0,
+                               output,
+                               read: llm.prompt_n ?? 0
+                       };
+               }
+
+               let read = 0;
+               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 { averageTokensPerSecond, cacheTotal, output, read };
+       });
+
+       cumulativeRead = $derived(this.cumulative.read);
+
+       cumulativeOutput = $derived(this.cumulative.output);
+
+       cumulativeCacheTotal = $derived(this.cumulative.cacheTotal);
+
+       averageTokensPerSecond = $derived(this.cumulative.averageTokensPerSecond);
+}
+
+export const contextStatsStore = new ContextStatsStore();
index 54613a27012363bd552d0f3cdda8cebe3c101e15..2d0d0eb1de73506faae3a087231f9eb6c543d722 100644 (file)
@@ -38,19 +38,15 @@ import {
 import { DatabaseService } from '$lib/services/database.service';
 import { MigrationService } from '$lib/services/migration.service';
 import { RouterService } from '$lib/services/router.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { mcpStore } from '$lib/stores/mcp.svelte';
-import { config } from '$lib/stores/settings.svelte';
+import { settingsStore } 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 { SvelteSet } from 'svelte/reactivity';
 import { toast } from 'svelte-sonner';
 
-export interface ConversationTreeItem {
-       conversation: DatabaseConversation;
-       depth: number;
-}
-
 class ConversationsStore {
        /**
         *
@@ -701,7 +697,7 @@ class ConversationsStore {
                                        this.activeConversation.id,
                                        generateConversationTitle(
                                                newFirstUserMessage.content,
-                                               Boolean(config().titleGenerationUseFirstLine)
+                                               Boolean(settingsStore.config.titleGenerationUseFirstLine)
                                        )
                                );
                        }
@@ -1260,75 +1256,3 @@ export const conversationsStore = new ConversationsStore();
 if (browser) {
        conversationsStore.init();
 }
-
-export const conversations = () => conversationsStore.conversations;
-export const activeConversation = () => conversationsStore.activeConversation;
-export const activeMessages = () => conversationsStore.activeMessages;
-export const pendingCwd = () => conversationsStore.pendingCwd;
-export const isConversationsInitialized = () => conversationsStore.isInitialized;
-
-/**
- * Builds a flat tree of conversations with depth levels for nested forks.
- * Accepts a pre-filtered list so search filtering stays in the component.
- *
- * Output order matches the sidebar render exactly: pinned first, then
- * unpinned by lastModified desc, with forks interleaved under their parents.
- * Range-select / marquee in the sidebar rely on this alignment.
- */
-
-// 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;
-};
-
-export function buildConversationTree(convs: DatabaseConversation[]): ConversationTreeItem[] {
-       const childrenByParent = new SvelteMap<string, DatabaseConversation[]>();
-       const forkIds = new SvelteSet<string>();
-
-       for (const conv of convs) {
-               if (conv.forkedFromConversationId) {
-                       forkIds.add(conv.id);
-
-                       const siblings = childrenByParent.get(conv.forkedFromConversationId) || [];
-
-                       siblings.push(conv);
-                       childrenByParent.set(conv.forkedFromConversationId, siblings);
-               }
-       }
-
-       const result: ConversationTreeItem[] = [];
-       const visited = new SvelteSet<string>();
-
-       function walk(conv: DatabaseConversation, depth: number) {
-               visited.add(conv.id);
-               result.push({ conversation: conv, depth });
-
-               const children = childrenByParent.get(conv.id);
-
-               if (children) {
-                       children.sort(comparePinnedThenRecent);
-
-                       for (const child of children) {
-                               walk(child, depth + 1);
-                       }
-               }
-       }
-
-       const roots = convs.filter((c) => !forkIds.has(c.id)).sort(comparePinnedThenRecent);
-
-       for (const root of roots) {
-               walk(root, 0);
-       }
-
-       for (const conv of convs) {
-               if (!visited.has(conv.id)) {
-                       walk(conv, 1);
-               }
-       }
-
-       return result;
-}
diff --git a/tools/ui/src/lib/stores/index.ts b/tools/ui/src/lib/stores/index.ts
new file mode 100644 (file)
index 0000000..8b906c9
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * STORES
+ *
+ * Reactive Svelte runes state layer. Stores own application state and
+ * expose it as plain Svelte 5 runes (`$state`, `$derived`, `$effect`),
+ * consumed by components, routes, hooks and services.
+ *
+ * Import from this barrel in leaf consumers:
+ *
+ * ```ts
+ * import { chatStore, modelsStore } from '$lib/stores';
+ * ```
+ *
+ * Store modules keep direct imports between each other (and from services/
+ * utils they depend on) to avoid circular dependency chains.
+ *
+ * Each store below documents its primary responsibility.
+ */
+
+// CHAT / MESSAGING
+export { chatStore } from './chat.svelte';
+
+export { draftMessagesStore } from './draft-messages.svelte';
+
+// AGENTIC (multi-turn tool orchestration)
+export { agenticStore } from './agentic.svelte';
+
+// CONVERSATIONS
+export { conversationsStore } from './conversations.svelte';
+
+// CONTEXT STATS (active conversation context window usage)
+export { contextStatsStore } from './context-stats.svelte';
+
+// MCP
+export { mcpStore } from './mcp.svelte';
+
+export { mcpResourceStore } from './mcp-resources.svelte';
+
+// MODELS
+export { modelsStore } from './models.svelte';
+
+// SERVER
+export { serverStore } from './server.svelte';
+
+// SETTINGS / UI PREFERENCES
+export { settingsStore } from './settings.svelte';
+
+export { settingsReferrer } from './settings-referrer.svelte';
+
+export { permissionsStore } from './permissions.svelte';
+
+// TOOLS
+export { toolsStore } from './tools.svelte';
+
+// ENVIRONMENT / META
+export { buildInfoStore } from './build-info.svelte';
+
+export { versionStore } from './version.svelte';
+
+export { device } from './device.svelte';
+
+export { viewport, isMobile } from './viewport.svelte';
+
+export { theme } from './theme.svelte';
+
+export {
+       gaugePopup,
+       gaugePopupClose,
+       gaugeTriggerPointerDown,
+       gaugeTriggerClick,
+       gaugeTriggerKeydown,
+       gaugeTriggerEnter,
+       gaugeTriggerLeave,
+       gaugeCardEnter,
+       gaugeCardLeave
+} from './context-gauge-popup.svelte';
+
+export { persisted } from './persisted.svelte';
index 1002c476e53b2641da6be7b8afa9a39b66e911c8..b68def89f5932ba4835564292fd8348a6ac92b53 100644 (file)
@@ -608,11 +608,3 @@ class MCPResourceStore {
 }
 
 export const mcpResourceStore = new MCPResourceStore();
-
-// Export convenience functions
-export const mcpResources = () => mcpResourceStore.serverResources;
-export const mcpResourceAttachments = () => mcpResourceStore.attachments;
-export const mcpResourceAttachmentCount = () => mcpResourceStore.attachmentCount;
-export const mcpHasResourceAttachments = () => mcpResourceStore.hasAttachments;
-export const mcpTotalResourceCount = () => mcpResourceStore.totalResourceCount;
-export const mcpResourcesLoading = () => mcpResourceStore.isLoading;
index 0b669f88b3943a8e8eed8a35ac787681605db491..3e0cb8e1e39e545533354910859131eb8d4d83da 100644 (file)
@@ -42,9 +42,10 @@ import {
        UrlProtocol
 } from '$lib/enums';
 import { MCPService } from '$lib/services/mcp.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
 import { serverStore } from '$lib/stores/server.svelte';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import type {
        ClientCapabilities,
        GetPromptResult,
@@ -161,7 +162,8 @@ class MCPStore {
         */
        #requestTimeoutMs(): number {
                const seconds =
-                       Number(config().mcpRequestTimeoutSeconds) || DEFAULT_MCP_CONFIG.requestTimeoutSeconds;
+                       Number(settingsStore.config.mcpRequestTimeoutSeconds) ||
+                       DEFAULT_MCP_CONFIG.requestTimeoutSeconds;
 
                return Math.round(seconds * 1000);
        }
@@ -310,7 +312,7 @@ class MCPStore {
        }
 
        get isEnabled(): boolean {
-               const mcpConfig = this.#buildMcpClientConfig(config());
+               const mcpConfig = this.#buildMcpClientConfig(settingsStore.config);
 
                return (
                        mcpConfig !== null && mcpConfig !== undefined && Object.keys(mcpConfig.servers).length > 0
@@ -374,7 +376,7 @@ class MCPStore {
        }
 
        getServers(): MCPServerSettingsEntry[] {
-               return parseMcpServerSettings(config().mcpServers);
+               return parseMcpServerSettings(settingsStore.config.mcpServers);
        }
 
        /**
@@ -591,10 +593,12 @@ class MCPStore {
        }
 
        hasAvailableServers(): boolean {
-               return parseMcpServerSettings(config().mcpServers).some((s) => s.enabled && s.url.trim());
+               return parseMcpServerSettings(settingsStore.config.mcpServers).some(
+                       (s) => s.enabled && s.url.trim()
+               );
        }
        hasEnabledServers(perChatOverrides?: McpServerOverride[]): boolean {
-               return Boolean(this.#buildMcpClientConfig(config(), perChatOverrides));
+               return Boolean(this.#buildMcpClientConfig(settingsStore.config, perChatOverrides));
        }
 
        getEnabledServersForConversation(
@@ -610,7 +614,7 @@ class MCPStore {
                        return false;
                }
 
-               const mcpConfig = this.#buildMcpClientConfig(config(), perChatOverrides);
+               const mcpConfig = this.#buildMcpClientConfig(settingsStore.config, perChatOverrides);
                const signature = mcpConfig ? JSON.stringify(mcpConfig) : null;
 
                if (!signature) {
@@ -1978,20 +1982,3 @@ class MCPStore {
 }
 
 export const mcpStore = new MCPStore();
-
-export const mcpIsInitializing = () => mcpStore.isInitializing;
-export const mcpIsInitialized = () => mcpStore.isInitialized;
-export const mcpError = () => mcpStore.error;
-export const mcpIsEnabled = () => mcpStore.isEnabled;
-export const mcpIsProxyAvailable = () => mcpStore.isProxyAvailable;
-export const mcpAvailableTools = () => mcpStore.availableTools;
-export const mcpConnectedServerCount = () => mcpStore.connectedServerCount;
-export const mcpConnectedServerNames = () => mcpStore.connectedServerNames;
-export const mcpToolCount = () => mcpStore.toolCount;
-export const mcpServerInstructions = () => mcpStore.getServerInstructions();
-export const mcpHasServerInstructions = () => mcpStore.hasServerInstructions();
-
-// Resources exports
-export const mcpHasResourcesCapability = () => mcpStore.hasResourcesCapability();
-export const mcpServersWithResources = () => mcpStore.getServersWithResources();
-export const mcpResourceContext = () => mcpStore.getResourceContextForChat();
index b2be636c51c198b0230f7e0fd3fcfe97af8e7a0e..c14db36a5f04bc5a5506775b04174803dea35d43 100644 (file)
@@ -15,8 +15,9 @@ import {
 } from '$lib/enums';
 import { ModelsService } from '$lib/services/models.service';
 import { PropsService } from '$lib/services/props.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { conversationsStore } from '$lib/stores/conversations.svelte';
-import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
 // deep imports, not the '$lib/utils' barrel: it re-exports modules that reach back
 // into the stores, and going through it here would read a half-built module
 import { getAuthHeaders } from '$lib/utils/api-headers';
@@ -129,7 +130,7 @@ class ModelsStore {
         * In ROUTER mode, returns null (model is per-conversation).
         */
        get singleModelName(): string | null {
-               if (isRouterMode()) return null;
+               if (serverStore.isRouterMode) return null;
 
                const props = serverStore.props;
 
@@ -155,7 +156,7 @@ class ModelsStore {
         */
 
        getModelModalities(modelId: string): ModelModalities | null {
-               if (!isRouterMode() && serverStore.props?.modalities) {
+               if (!serverStore.isRouterMode && serverStore.props?.modalities) {
                        return this.buildModalities(serverStore.props.modalities);
                }
 
@@ -267,7 +268,7 @@ class ModelsStore {
         *   triggering an async fetch if not yet cached
         */
        get supportsThinking(): boolean {
-               if (!isRouterMode()) {
+               if (!serverStore.isRouterMode) {
                        return detectThinkingSupport(serverStore.props?.chat_template ?? '');
                }
 
@@ -290,7 +291,7 @@ class ModelsStore {
         * In ROUTER mode, fetches model props if not cached.
         */
        checkModelSupportsThinking(modelId: string): boolean {
-               if (!isRouterMode()) {
+               if (!serverStore.isRouterMode) {
                        return detectThinkingSupport(serverStore.props?.chat_template ?? '');
                }
 
@@ -309,7 +310,7 @@ class ModelsStore {
         * Detailed thinking support detection result with reason for debugging/UI.
         */
        get thinkingSupportDetails(): { supported: boolean; reason: string } {
-               if (!isRouterMode()) {
+               if (!serverStore.isRouterMode) {
                        return detectThinkingSupportWithReason(serverStore.props?.chat_template ?? '');
                }
 
@@ -362,7 +363,7 @@ class ModelsStore {
                                await serverStore.fetch();
                        }
 
-                       const router = isRouterMode();
+                       const router = serverStore.isRouterMode;
 
                        if (router) {
                                const response = await ModelsService.listRouter();
@@ -434,7 +435,7 @@ class ModelsStore {
         * Kept for API compatibility (e.g. handleOpenChange dropdown open handler).
         */
        async fetchRouterModels(): Promise<void> {
-               if (!isRouterMode()) return;
+               if (!serverStore.isRouterMode) return;
 
                try {
                        const response = await ModelsService.listRouter();
@@ -726,7 +727,7 @@ class ModelsStore {
        subscribeStatus(): void {
                if (this.statusReaderActive) return;
 
-               if (!isRouterMode()) return;
+               if (!serverStore.isRouterMode) return;
 
                this.statusReaderActive = true;
                this.statusAbort = new AbortController();
@@ -1119,22 +1120,3 @@ class ModelsStore {
 }
 
 export const modelsStore = new ModelsStore();
-
-export const modelOptions = () => modelsStore.models;
-export const routerModels = () => modelsStore.routerModels;
-export const modelsLoading = () => modelsStore.loading;
-export const modelsUpdating = () => modelsStore.updating;
-export const modelsError = () => modelsStore.error;
-export const selectedModelId = () => modelsStore.selectedModelId;
-export const selectedModelName = () => modelsStore.selectedModelName;
-export const selectedModelOption = () => modelsStore.selectedModel;
-export const loadedModelIds = () => modelsStore.loadedModelIds;
-export const loadingModelIds = () => modelsStore.loadingModelIds;
-export const propsCacheVersion = () => modelsStore.propsCacheVersion;
-export const singleModelName = () => modelsStore.singleModelName;
-export const selectedModelContextSize = () => modelsStore.selectedModelContextSize;
-export const favoriteModelIds = () => modelsStore.favoriteModelIds;
-export const supportsThinking = () => modelsStore.supportsThinking;
-export const checkModelSupportsThinking = (modelId: string) =>
-       modelsStore.checkModelSupportsThinking(modelId);
-export const thinkingSupportDetails = () => modelsStore.thinkingSupportDetails;
index f013c5b4d3bfac7e541b6bafd9818aaba592f406..3a5494a0baf9684d2ebf33a7532dbf030a4ab396 100644 (file)
@@ -1,3 +1,4 @@
+import { browser } from '$app/environment';
 import { ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY } from '$lib/constants';
 import { SvelteSet } from 'svelte/reactivity';
 
@@ -5,6 +6,9 @@ class PermissionsStore {
        private _tools = $state(new SvelteSet<string>());
 
        constructor() {
+               // browser-only init: skip on SSR to avoid localStorage side effects
+               if (!browser) return;
+
                try {
                        const stored = localStorage.getItem(ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY);
 
index 03b6ddc169c3b5679f6131d87e84138cf7e0b8fe..7de5850b9eff278c70518174e49364bb9ead109d 100644 (file)
@@ -1,6 +1,6 @@
 import { ServerRole } from '$lib/enums';
 import { PropsService } from '$lib/services/props.service';
-import { ApiError } from '$lib/utils/api-fetch';
+import { ApiError } from '$lib/utils';
 
 const LOADING_RETRY_INTERVAL_MS = 1000;
 
@@ -169,13 +169,3 @@ class ServerStore {
 }
 
 export const serverStore = new ServerStore();
-
-export const serverProps = () => serverStore.props;
-export const serverLoading = () => serverStore.loading;
-export const serverError = () => serverStore.error;
-export const serverStatus = () => serverStore.status;
-export const serverRole = () => serverStore.role;
-export const defaultParams = () => serverStore.defaultParams;
-export const contextSize = () => serverStore.contextSize;
-export const isRouterMode = () => serverStore.isRouterMode;
-export const isModelMode = () => serverStore.isModelMode;
index c8b9be7a781c9ea7ecdd63e9e024e17db0b040e0..89c9f22cb813ddda05278a87261ace2363809a22 100644 (file)
@@ -40,6 +40,7 @@ import {
 } from '$lib/constants';
 import { ColorMode } from '$lib/enums';
 import { ParameterSyncService } from '$lib/services/parameter-sync.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { serverStore } from '$lib/stores/server.svelte';
 import { isMobile } from '$lib/stores/viewport.svelte';
 import type { SettingsExportType } from '$lib/types';
@@ -575,7 +576,3 @@ class SettingsStore {
 }
 
 export const settingsStore = new SettingsStore();
-
-export const config = () => settingsStore.config;
-export const theme = () => settingsStore.config[SETTINGS_KEYS.THEME];
-export const isInitialized = () => settingsStore.isInitialized;
index ff276d95bae089fc6c737f3415f8dddc37928a49..642662928831edb3aa25aa60ae1d16a4c04f46db 100644 (file)
@@ -1,3 +1,4 @@
+import { browser } from '$app/environment';
 import {
        buildReadMediaToolDefinition,
        DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY,
@@ -14,9 +15,10 @@ import {
        ToolSource
 } from '$lib/enums';
 import { ToolsService } from '$lib/services/tools.service';
+// direct imports between stores, not via the barrel, to avoid circular deps
 import { mcpStore } from '$lib/stores/mcp.svelte';
-import { modelsStore, selectedModelName } from '$lib/stores/models.svelte';
-import { config } from '$lib/stores/settings.svelte';
+import { modelsStore } from '$lib/stores/models.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import type { OpenAIToolDefinition, ToolEntry, ToolGroup } from '$lib/types';
 import { buildSandboxToolDefinition } from '$lib/utils';
 import { SvelteMap, SvelteSet } from 'svelte/reactivity';
@@ -35,6 +37,9 @@ class ToolsStore {
        private _serverHome = $state<string | null | undefined>(undefined);
 
        constructor() {
+               // browser-only init: skip on SSR to avoid localStorage/fetch side effects
+               if (!browser) return;
+
                try {
                        const stored = localStorage.getItem(DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY);
 
@@ -172,8 +177,8 @@ class ToolsStore {
        get frontendTools(): OpenAIToolDefinition[] {
                const tools: OpenAIToolDefinition[] = [];
 
-               if (config().jsSandboxEnabled) {
-                       tools.push(buildSandboxToolDefinition(!!config().symbolicMathEnabled));
+               if (settingsStore.config.jsSandboxEnabled) {
+                       tools.push(buildSandboxToolDefinition(!!settingsStore.config.symbolicMathEnabled));
                }
 
                const readMedia = this.readMediaTool();
@@ -196,7 +201,7 @@ class ToolsStore {
 
                if (!hasReadFile) return null;
 
-               const model = selectedModelName() ?? modelsStore.models[0]?.model ?? '';
+               const model = modelsStore.selectedModelName ?? modelsStore.models[0]?.model ?? '';
 
                if (!model) return null;
 
@@ -209,7 +214,7 @@ class ToolsStore {
        }
 
        get customTools(): OpenAIToolDefinition[] {
-               const raw = config().customJson;
+               const raw = settingsStore.config.customJson;
 
                if (!raw || typeof raw !== 'string') return [];
 
@@ -624,7 +629,3 @@ class ToolsStore {
 }
 
 export const toolsStore = new ToolsStore();
-
-export const allTools = () => toolsStore.allTools;
-export const allToolDefinitions = () => toolsStore.allToolDefinitions;
-export const toolGroups = () => toolsStore.toolGroups;
index cf939371ec4333382ff6cdcca829fc1b09a2ee8c..e7c6d34e1dc5d5461d2d30421e78232dafc7e9c2 100644 (file)
@@ -4,7 +4,11 @@ import type {
        ApiChatMessageContentPart,
        ApiChatMessageData
 } from './api';
-import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat';
+import type {
+       ChatMessageAgenticTimings,
+       ChatMessagePromptProgress,
+       ChatMessageTimings
+} from './chat';
 import type {
        DatabaseMessage,
        DatabaseMessageExtra,
@@ -79,6 +83,12 @@ export interface AgenticSession {
         *  matching tool renderer flip into live-update mode while chunks
         *  arrive; cleared when the tool's terminal event lands. */
        executingToolCallId: string | null;
+       /** Live LLM token totals of the running flow: completed turns plus the
+        *  in-flight turn's streamed counts; null when idle. */
+       liveLlm: ChatMessageAgenticTimings['llm'] | null;
+       /** ID of the flow's first assistant message (the one the UI groups the
+        *  whole run under); null when idle. */
+       flowRootMessageId: string | null;
 }
 
 /**
@@ -154,6 +164,8 @@ export interface AgenticFlowOptions {
  */
 export interface AgenticFlowParams {
        conversationId: string;
+       /** ID of the flow's first assistant message, used to keep its stats live */
+       flowRootMessageId?: string;
        messages: (ApiChatMessageData | (DatabaseMessage & { extra?: DatabaseMessageExtra[] }))[];
        options?: AgenticFlowOptions;
        callbacks: AgenticFlowCallbacks;
index 0038e6f3cb6f46a18fae381082aad97e57b3113e..4b2b19d442e64d1f765fbef3063be7d995112037 100644 (file)
@@ -1,14 +1,14 @@
 import { redactValue } from './redact';
 import { CORS_PROXY, HEADERS } from '$lib/constants';
 import { MimeTypeApplication } from '$lib/enums';
-import { config } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 
 /**
  * Get authorization headers for API requests
  * Includes Bearer token if API key is configured
  */
 export function getAuthHeaders(): Record<string, string> {
-       const currentConfig = config();
+       const currentConfig = settingsStore.config;
        const apiKey = currentConfig.apiKey?.toString().trim();
 
        return apiKey ? { [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey}` } : {};
index 7724bb4f5286ef00e89b8237ec8aa7d6cd3621fd..8cde154fd2f96a65176d89e53e289065f1380460 100644 (file)
@@ -3,7 +3,7 @@ import { browser } from '$app/environment';
 import { base } from '$app/paths';
 import { HEADERS } from '$lib/constants';
 import { MimeTypeApplication } from '$lib/enums';
-import { config } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 
 /**
  * Validates API key by making a request to the server props endpoint
@@ -14,7 +14,7 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
                return;
        }
 
-       const apiKey = config().apiKey;
+       const apiKey = settingsStore.config.apiKey;
 
        try {
                const headers: Record<string, string> = {
index 2c3d838999b27966b0880046ec6c98e316169a54..0bd811f599ecf6db99e231432d47cf5a1f13e972 100644 (file)
@@ -29,3 +29,73 @@ export function createMessageCountMap(
 export function getMessageCount(conversationId: string, countMap: Map<string, number>): number {
        return countMap.get(conversationId) ?? 0;
 }
+
+export interface ConversationTreeItem {
+       conversation: DatabaseConversation;
+       depth: number;
+}
+
+// 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;
+};
+
+/**
+ * Builds a flat tree of conversations with depth levels for nested forks.
+ * Accepts a pre-filtered list so search filtering stays in the component.
+ *
+ * Output order matches the sidebar render exactly: pinned first, then
+ * unpinned by lastModified desc, with forks interleaved under their parents.
+ * Range-select / marquee in the sidebar rely on this alignment.
+ */
+export function buildConversationTree(convs: DatabaseConversation[]): ConversationTreeItem[] {
+       const childrenByParent = new Map<string, DatabaseConversation[]>();
+       const forkIds = new Set<string>();
+
+       for (const conv of convs) {
+               if (conv.forkedFromConversationId) {
+                       forkIds.add(conv.id);
+
+                       const siblings = childrenByParent.get(conv.forkedFromConversationId) || [];
+
+                       siblings.push(conv);
+                       childrenByParent.set(conv.forkedFromConversationId, siblings);
+               }
+       }
+
+       const result: ConversationTreeItem[] = [];
+       const visited = new Set<string>();
+
+       function walk(conv: DatabaseConversation, depth: number) {
+               visited.add(conv.id);
+               result.push({ conversation: conv, depth });
+
+               const children = childrenByParent.get(conv.id);
+
+               if (children) {
+                       children.sort(comparePinnedThenRecent);
+
+                       for (const child of children) {
+                               walk(child, depth + 1);
+                       }
+               }
+       }
+
+       const roots = convs.filter((c) => !forkIds.has(c.id)).sort(comparePinnedThenRecent);
+
+       for (const root of roots) {
+               walk(root, 0);
+       }
+
+       for (const conv of convs) {
+               if (!visited.has(conv.id)) {
+                       walk(conv, 1);
+               }
+       }
+
+       return result;
+}
index 449ebcba609bfe8df492109d557647089c2021ba..e348f25fe9c371b5a112d18dbfcd4288b8a1958a 100644 (file)
@@ -5,7 +5,7 @@ import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
 import { SETTINGS_KEYS } from '$lib/constants';
 import { AttachmentType, FileTypeCategory, SpecialFileType } from '$lib/enums';
 import { modelsStore } from '$lib/stores/models.svelte';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import type { ChatUploadedFile, DatabaseMessageExtra, FileProcessingResult } from '$lib/types';
 import { getFileTypeCategory } from '$lib/utils';
 import { toast } from 'svelte-sonner';
@@ -109,7 +109,7 @@ export async function parseFilesToMessageExtras(
                        try {
                                // Always get base64 data for preview functionality
                                const base64Data = await readFileAsBase64(file.file);
-                               const currentConfig = config();
+                               const currentConfig = settingsStore.config;
                                // Use per-model vision check for router mode
                                const hasVisionSupport = activeModelId
                                        ? modelsStore.modelSupportsVision(activeModelId)
index 35e20aa168f654e2c2d96bee69a2574a136512bc..178dae7a48695b75e9ed72aeee6ff5ae63ad189a 100644 (file)
@@ -9,7 +9,7 @@
 
 // API utilities
 export { getAuthHeaders, getJsonHeaders, sanitizeHeaders } from './api-headers';
-export { apiFetch, apiFetchWithParams, apiPost, type ApiFetchOptions } from './api-fetch';
+export { ApiError, apiFetch, apiFetchWithParams, apiPost } from './api-fetch';
 export { validateApiKey } from './api-key-validation';
 
 // Attachment utilities
@@ -51,7 +51,12 @@ export { extractRootDomain, sanitizeExternalUrl, canonicalizeServerUrl } from '.
 export { modelLoadFraction, modelLoadProgressText } from './progress';
 
 // Conversation utilities
-export { createMessageCountMap, getMessageCount } from './conversation-utils';
+export {
+       createMessageCountMap,
+       getMessageCount,
+       buildConversationTree,
+       type ConversationTreeItem
+} from './conversation-utils';
 
 // Clipboard utilities
 export {
@@ -124,7 +129,10 @@ export { getImageErrorFallbackHtml } from './image-error-fallback';
 
 // SSE-with-JSON stream iterator (used by built-in tool streaming, decoupled
 // from chat.service.ts which embeds its own SSE parser for resume support)
-export { parseSseJsonStream, type SseJsonEvent } from './sse';
+export { parseSseJsonStream } from './sse';
+
+// Stream session identity (conversation-id based)
+export { streamIdentity } from './stream-identity';
 
 // MCP utilities
 export {
@@ -236,6 +244,12 @@ export {
        buildMentionInsertion
 } from './mention-badge';
 
+// Chat template utilities
+export {
+       detectThinkingSupport,
+       detectThinkingSupportWithReason
+} from './chat-template-thinking-detector';
+
 // Agentic content utilities (structured section derivation)
 export {
        deriveAgenticSections,
@@ -243,7 +257,8 @@ export {
        parseToolResultWithMedia,
        splitSearchSummaryList,
        hasAgenticContent,
-       classifyToolResult
+       classifyToolResult,
+       classifyContinueIntent
 } from './agentic';
 
 // Line-level unified diff for tool result rendering (`edit_file` block)
@@ -270,7 +285,7 @@ export {
 } from './search-results';
 
 // Cache utilities
-export { TTLCache, ReactiveTTLMap, type TTLCacheOptions } from './cache-ttl';
+export { TTLCache, ReactiveTTLMap } from './cache-ttl';
 
 // Redaction utilities
 export { redactValue } from './redact';
index c1e49102b7a3bd3ca6c63bf4d991b2485497adfc..c4958b7c167b962203df97dfe895003299880390 100644 (file)
@@ -3,10 +3,7 @@
        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 { modelOptions, modelsStore } from '$lib/stores/models.svelte';
-       import { isRouterMode } from '$lib/stores/server.svelte';
+       import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
        import { onMount } from 'svelte';
 
        let qParam = $derived(page.url.searchParams.get(URL_PARAMS.QUERY));
@@ -17,7 +14,7 @@
        // Dialog state for model not available error
        let showModelNotAvailable = $state(false);
        let requestedModelName = $state('');
-       let availableModelNames = $derived(modelOptions().map((m) => m.model));
+       let availableModelNames = $derived(modelsStore.models.map((m) => m.model));
 
        /**
         * Clear URL params after message is sent to prevent re-sending on refresh
 
                                        // with ?load=true, start loading right away so the model is ready sooner;
                                        // not awaited, so the UI stays usable during the load
-                                       if (loadParam === 'true' && isRouterMode() && !modelsStore.isModelLoaded(model.id)) {
+                                       if (
+                                               loadParam === 'true' &&
+                                               serverStore.isRouterMode &&
+                                               !modelsStore.isModelLoaded(model.id)
+                                       ) {
                                                modelsStore
                                                        .loadModel(model.id)
                                                        .catch((error) => console.error('Failed to load model:', error));
@@ -75,7 +76,7 @@
        }
 
        onMount(async () => {
-               if (!isConversationsInitialized()) {
+               if (!conversationsStore.isInitialized) {
                        await conversationsStore.initialize();
                }
 
index 677bbf03d9c07fd14b967b72b06de5b070c7c67c..2fcff6bf66099d3cc89d0dadb4edf9d72ab9ed1d 100644 (file)
@@ -4,9 +4,7 @@
        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 { activeConversation, conversationsStore } from '$lib/stores/conversations.svelte';
-       import { modelOptions, modelsStore } from '$lib/stores/models.svelte';
+       import { chatStore, conversationsStore, modelsStore } from '$lib/stores';
 
        let chatId = $derived(page.params.id);
        let currentChatId: string | undefined = undefined;
@@ -18,7 +16,7 @@
        // Dialog state for model not available error
        let showModelNotAvailable = $state(false);
        let requestedModelName = $state('');
-       let availableModelNames = $derived(modelOptions().map((m) => m.model));
+       let availableModelNames = $derived(modelsStore.models.map((m) => m.model));
 
        // Track if URL params have been processed for this chat
        let urlParamsProcessed = $state(false);
@@ -86,7 +84,7 @@
                        urlParamsProcessed = false; // Reset for new chat
 
                        // Skip loading if this conversation is already active (e.g., just created)
-                       if (activeConversation()?.id === chatId) {
+                       if (conversationsStore.activeConversation?.id === chatId) {
                                void chatStore.discoverActiveStream(chatId);
 
                                if ((qParam !== null || modelParam !== null) && !urlParamsProcessed) {
 </script>
 
 <svelte:head>
-       <title>{activeConversation()?.name || 'Chat'} - {APP_NAME}</title>
+       <title>{conversationsStore.activeConversation?.name || 'Chat'} - {APP_NAME}</title>
 </svelte:head>
 
 <DialogModelNotAvailable
index 560d36c4d72216b82dfa5d5c854b7c188504618a..f938c7edf414180689f2d2995fcb8399b91844a5 100644 (file)
        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 { 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 { isMobile } from '$lib/stores/viewport.svelte';
+       import {
+               buildInfoStore,
+               chatStore,
+               conversationsStore,
+               isMobile,
+               mcpStore,
+               modelsStore,
+               serverStore,
+               settingsStore,
+               theme
+       } from '$lib/stores';
        import { ModeWatcher } from 'mode-watcher';
        import { untrack } from 'svelte';
        import { onMount } from 'svelte';
@@ -44,7 +46,9 @@
                  }
                | undefined = $state();
 
-       let showBuildVersion = $derived(config()[SETTINGS_KEYS.SHOW_BUILD_VERSION] as boolean);
+       let showBuildVersion = $derived(
+               settingsStore.config[SETTINGS_KEYS.SHOW_BUILD_VERSION] as boolean
+       );
 
        // Keep the hook object intact: destructuring needRefreshByStorage reads the getter once and freezes it
        const pwa = usePwa();
@@ -67,7 +71,7 @@
        }
 
        function navigateToConversation(direction: -1 | 1) {
-               const allConvs = conversations();
+               const allConvs = conversationsStore.conversations;
 
                if (allConvs.length === 0) return;
 
        });
 
        function checkApiKey() {
-               const apiKey = config().apiKey;
+               const apiKey = settingsStore.config.apiKey;
 
                // Without a stored key there is nothing to re-validate here; the keyless
                // 401 case is handled by validateApiKey() at navigation time, and the
        // textContent keeps the value as text, never parsed as HTML
        function customCss(node: HTMLStyleElement) {
                $effect(() => {
-                       node.textContent = (config().customCss as string | undefined) ?? '';
+                       node.textContent = (settingsStore.config.customCss as string | undefined) ?? '';
                });
        }
 
        let routerModelsFetched = false;
 
        $effect(() => {
-               const isRouter = isRouterMode();
+               const isRouter = serverStore.isRouterMode;
                const modelsCount = modelsStore.models.length;
 
                // Only fetch router models once when we have models loaded and in router mode
        $effect(() => {
                if (!browser) return;
 
-               if (!isRouterMode()) return;
+               if (!serverStore.isRouterMode) return;
 
                untrack(() => {
                        modelsStore.subscribeStatus();
                <meta name="theme-color" content={pwaAssetsHead.themeColor.content} />
        {/if}
 
-       {#if config().customCss}
+       {#if settingsStore.config.customCss}
                <style use:customCss></style>
        {/if}
 
index 424b74f67cf91bdba64519ad7a2690ce533b274d..cb7f77f95e070e945e35c6730dbe15a7024e7a44 100644 (file)
@@ -5,9 +5,7 @@
        import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app';
        import { ROUTES } from '$lib/constants';
        import { RouterService } from '$lib/services/router.service';
-       import { chatStore } from '$lib/stores/chat.svelte';
-       import { conversations, conversationsStore } from '$lib/stores/conversations.svelte';
-       import { isMobile } from '$lib/stores/viewport.svelte';
+       import { chatStore, conversationsStore, isMobile } from '$lib/stores';
 
        let searchQuery = $state('');
        let searchInputRef = $state<HTMLInputElement | null>(null);
@@ -19,7 +17,7 @@
 
                if (query.length === 0) return [];
 
-               return conversations().filter((c) => c.name.toLowerCase().includes(query));
+               return conversationsStore.conversations.filter((c) => c.name.toLowerCase().includes(query));
        });
 
        // Search page is intended for mobile; on desktop the sidebar already exposes
@@ -35,7 +33,7 @@
        }
 
        async function handleEditConversation(id: string) {
-               const conversation = conversations().find((c) => c.id === id);
+               const conversation = conversationsStore.conversations.find((c) => c.id === id);
 
                if (!conversation) return;
 
@@ -47,7 +45,7 @@
        }
 
        async function handleDeleteConversation(id: string) {
-               const conversation = conversations().find((c) => c.id === id);
+               const conversation = conversationsStore.conversations.find((c) => c.id === id);
 
                if (!conversation) return;
 
index ddf1393e70e8389e34330295c0cc48008ab7fcb6..45af7e0d151c65989d4865158cc996ff6919d33a 100644 (file)
@@ -1,7 +1,7 @@
 import { CONFIG_LOCALSTORAGE_KEY, SETTING_CONFIG_DEFAULT } from '$lib/constants';
 import { ParameterSyncService } from '$lib/services/parameter-sync.service';
 import { serverStore } from '$lib/stores/server.svelte';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import type { SettingsConfigType } from '$lib/types';
 import { beforeEach, describe, expect, it } from 'vitest';
 
@@ -40,7 +40,7 @@ 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];
+const current = (key: string) => (settingsStore.config as Record<string, unknown>)[key];
 
 describe('registry-wide invariants', () => {
        beforeEach(() => {
index 078cc7cbece388e687c1a5c845546b1cef874257..32f4ff3dd4bdc1918b0f5b391fd5c9df0b049cb4 100644 (file)
@@ -6,7 +6,7 @@
 
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
 import { MigrationService } from '$lib/services/migration.service';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 const RENDER_KEYS_MIGRATION_ID = 'render-keys-unfold-v1';
@@ -33,22 +33,22 @@ describe('renderContentAsRawText unfolding', () => {
 
        it('maps raw text to user content as plain text', async () => {
                await seedConfig({ renderContentAsRawText: true });
-               expect(config().renderUserContentAsMarkdown).toBe(false);
+               expect(settingsStore.config.renderUserContentAsMarkdown).toBe(false);
        });
 
        it('maps markdown to user content as markdown', async () => {
                await seedConfig({ renderContentAsRawText: false });
-               expect(config().renderUserContentAsMarkdown).toBe(true);
+               expect(settingsStore.config.renderUserContentAsMarkdown).toBe(true);
        });
 
        it('leaves thinking on its own default', async () => {
                await seedConfig({ renderContentAsRawText: true });
-               expect(config().renderThinkingAsMarkdown).toBe(true);
+               expect(settingsStore.config.renderThinkingAsMarkdown).toBe(true);
        });
 
        it('keeps an explicit user preference over the toggle', async () => {
                await seedConfig({ renderContentAsRawText: true, renderUserContentAsMarkdown: true });
-               expect(config().renderUserContentAsMarkdown).toBe(true);
+               expect(settingsStore.config.renderUserContentAsMarkdown).toBe(true);
        });
 
        it('drops the toggle from the persisted config', async () => {
@@ -58,7 +58,7 @@ describe('renderContentAsRawText unfolding', () => {
 
        it('leaves both surfaces on markdown when nothing is stored', async () => {
                await seedConfig({});
-               expect(config().renderUserContentAsMarkdown).toBe(true);
-               expect(config().renderThinkingAsMarkdown).toBe(true);
+               expect(settingsStore.config.renderUserContentAsMarkdown).toBe(true);
+               expect(settingsStore.config.renderThinkingAsMarkdown).toBe(true);
        });
 });
index 110011ef0dcb661fbcfb397bd93e61a35288f225..86e0898937d0148dc48a5433afe9e5e91919c6d1 100644 (file)
@@ -1,6 +1,6 @@
 import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants';
 import { serverStore } from '$lib/stores/server.svelte';
-import { config, settingsStore } from '$lib/stores/settings.svelte';
+import { settingsStore } from '$lib/stores/settings.svelte';
 import { beforeEach, describe, expect, it } from 'vitest';
 
 function mockProps(uiSettings: Record<string, string | number | boolean>) {
@@ -25,7 +25,7 @@ describe('server ui_settings application semantics', () => {
 
                settingsStore.syncWithServerDefaults();
 
-               expect(config().theme).toBe('dark');
+               expect(settingsStore.config.theme).toBe('dark');
        });
 
        it('never reapplies on later loads: the user config diverges freely', () => {
@@ -40,8 +40,8 @@ describe('server ui_settings application semantics', () => {
                settingsStore.syncWithServerDefaults();
                settingsStore.syncWithServerDefaults();
 
-               expect(config().theme).toBe('light');
-               expect(config().apiKey).toBe('sk-user-key');
+               expect(settingsStore.config.theme).toBe('light');
+               expect(settingsStore.config.apiKey).toBe('sk-user-key');
                const stored = JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}');
 
                expect(stored.apiKey).toBe('sk-user-key');
@@ -55,8 +55,8 @@ describe('server ui_settings application semantics', () => {
 
                settingsStore.forceSyncWithServerDefaults();
 
-               expect(config().theme).toBe('dark');
-               expect(config().apiKey).toBe('');
+               expect(settingsStore.config.theme).toBe('dark');
+               expect(settingsStore.config.apiKey).toBe('');
        });
 });