ui: Stores architecture improvements (#26910)

* 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
This commit is contained in:
Aleksander Grygier
2026-08-13 08:11:30 +02:00
committed by GitHub
parent a6040c925c
commit 094e53db1c
116 changed files with 937 additions and 945 deletions
@@ -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';
@@ -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,
@@ -26,19 +26,16 @@
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,
@@ -143,7 +140,7 @@
// 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,
@@ -184,7 +181,7 @@
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);
@@ -192,18 +189,18 @@
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);
@@ -220,7 +217,9 @@
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)
@@ -634,7 +633,7 @@
/>
{/if}
{#if mcpHasResourceAttachments()}
{#if mcpResourceStore.hasAttachments}
<ChatFormMcpResourcesList
class="mb-3"
onResourceClick={(uri) => {
@@ -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 {
@@ -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 {
@@ -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);
@@ -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;
@@ -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;
@@ -35,17 +26,17 @@
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 {
@@ -73,24 +64,24 @@
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);
@@ -140,21 +131,23 @@
});
$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);
}
}
});
@@ -13,14 +13,7 @@
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();
@@ -103,7 +96,7 @@
let hasProcessedTokens = $derived.by(() => {
if (!page.params.id) return false;
const messages = activeMessages() as DatabaseMessage[];
const messages = conversationsStore.activeMessages as DatabaseMessage[];
let totalHistoricalTokens = 0;
@@ -125,9 +118,9 @@
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;
@@ -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 {
@@ -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));
@@ -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}
@@ -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);
@@ -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;
});
@@ -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';
@@ -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';
@@ -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';
@@ -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,
@@ -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';
@@ -185,7 +183,7 @@
});
$effect(() => {
const pendingId = pendingEditMessageId();
const pendingId = chatStore.pendingEditMessageId;
if (pendingId && pendingId === message.id && !isEditing) {
handleEdit();
@@ -11,10 +11,7 @@
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';
@@ -69,15 +66,15 @@
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);
@@ -175,7 +172,7 @@
<ChatMessageAgenticContent
{message}
{toolMessages}
isStreaming={isChatStreaming()}
isStreaming={chatStore.isStreaming()}
{isLastAssistantMessage}
/>
{/if}
@@ -190,14 +187,14 @@
<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}
/>
@@ -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 {
@@ -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>
@@ -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;
@@ -11,9 +12,26 @@
}
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}
@@ -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';
@@ -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);
@@ -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';
@@ -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);
@@ -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';
@@ -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';
@@ -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';
@@ -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,
@@ -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';
@@ -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';
@@ -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(() => {
@@ -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;
@@ -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;
@@ -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;
@@ -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,
@@ -41,19 +33,25 @@
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;
@@ -69,13 +67,15 @@
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;
@@ -91,13 +91,13 @@
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 = {
@@ -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();
@@ -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...';
@@ -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) => {
@@ -108,7 +94,7 @@
});
function refreshAllMessages() {
const conversation = activeConversation();
const conversation = conversationsStore.activeConversation;
if (conversation) {
conversationsStore.getConversationMessages(conversation.id).then((messages) => {
@@ -121,7 +107,7 @@
// Refresh messages whenever the active conversation changes
$effect(() => {
if (activeConversation()) {
if (conversationsStore.activeConversation) {
refreshAllMessages();
}
});
@@ -251,32 +237,33 @@
/>
{/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}
@@ -20,26 +20,20 @@
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);
@@ -48,12 +42,15 @@
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;
}
}
@@ -100,7 +97,7 @@
}
async function handleDeleteConfirm() {
const conversation = activeConversation();
const conversation = conversationsStore.activeConversation;
if (conversation) {
await conversationsStore.deleteConversation(conversation.id);
@@ -148,7 +145,7 @@
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;
@@ -168,7 +165,7 @@
const settle = () => {
if (autoScroll.userScrolledUp) return;
if (activeConversation()?.id !== id) return;
if (conversationsStore.activeConversation?.id !== id) return;
autoScroll.scrollToBottom();
const height = container.scrollHeight;
@@ -246,7 +243,7 @@
$effect(() => {
const shouldDisableAutoScroll =
config().disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
settingsStore.config.disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
autoScroll.setDisabled(shouldDisableAutoScroll);
@@ -310,7 +307,7 @@
>
{#if !isEmpty}
<ChatMessages
messages={activeMessages()}
messages={conversationsStore.activeMessages}
onMessagesReady={handleMessagesReady}
onUserAction={() => {
handleSendLikeScroll();
@@ -354,7 +351,7 @@
<ChatScreenForm
class="pointer-events-auto conversation-chat-form"
disabled={hasPropsError || isEditing()}
disabled={hasPropsError || chatStore.isEditing()}
{initialMessage}
isLoading={isCurrentConversationLoading}
onFileRemove={fileUpload.handleFileRemove}
@@ -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 {
@@ -1,5 +1,5 @@
<script lang="ts">
import { serverStore } from '$lib/stores/server.svelte';
import { serverStore } from '$lib/stores';
interface Props {
isEmpty: boolean;
@@ -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}
@@ -23,17 +23,17 @@
{#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>
@@ -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>
@@ -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,
@@ -864,7 +864,7 @@
<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'
: ''}"
>
@@ -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';
@@ -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,
@@ -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,
@@ -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) {
@@ -205,7 +199,7 @@
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) {
@@ -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 {
@@ -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
@@ -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;
@@ -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,
@@ -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,
@@ -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()) {
@@ -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';
@@ -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';
@@ -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';
@@ -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;
@@ -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;
@@ -32,9 +32,13 @@
'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 ?? [])])]);
@@ -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 {
@@ -111,7 +111,7 @@
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;
@@ -169,7 +169,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 &&
@@ -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;
@@ -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);
@@ -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 &&
@@ -14,15 +14,8 @@
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);
@@ -110,7 +103,7 @@
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);
@@ -124,7 +117,7 @@
const pinStateIsMixed = $derived.by(() => {
if (selectedIds.size === 0) return false;
const convs = conversations();
const convs = conversationsStore.conversations;
let anyPinned = false;
let anyUnpinned = false;
@@ -242,7 +235,7 @@
}
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;
@@ -275,7 +268,7 @@
}
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;
@@ -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';
@@ -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();
@@ -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;
@@ -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;
@@ -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() || '';
}
@@ -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;
@@ -13,10 +12,10 @@
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';
@@ -20,10 +20,7 @@
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';
@@ -43,14 +40,14 @@
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();
}
@@ -123,7 +120,7 @@
}
export function reset() {
localConfig = { ...config() };
localConfig = { ...settingsStore.config };
}
setChatSettingsConfigContext({
@@ -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';
@@ -25,10 +23,10 @@
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);
@@ -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';
@@ -112,7 +111,7 @@
async function handleExportClick() {
try {
const allConversations = conversations();
const allConversations = conversationsStore.conversations;
if (allConversations.length === 0) {
toast.info('No conversations to export');
@@ -231,7 +230,7 @@
async function handleDeleteAllClick() {
try {
const allConversations = conversations();
const allConversations = conversationsStore.conversations;
if (allConversations.length === 0) {
toast.info('No conversations to delete');
@@ -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';
@@ -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;
@@ -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';
@@ -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);
@@ -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);
@@ -88,7 +88,9 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
}
return {
fileErrorData,
get fileErrorData() {
return fileErrorData;
},
handleFileRemove,
handleFileUpload,
get showFileErrorDialog() {
@@ -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(),
@@ -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 {
@@ -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(() => {
@@ -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(() => {
+1 -1
View 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';
/**
@@ -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(
@@ -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;
@@ -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';
+2 -2
View 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]) {
+41 -72
View 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);
}
+64 -75
View File
@@ -13,61 +13,56 @@
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);
@@ -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();
@@ -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;
}
+78
View File
@@ -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';
@@ -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;
+11 -24
View 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();
+10 -28
View 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;
@@ -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);

Some files were not shown because too many files have changed in this diff Show More