mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-06 17:57:44 +02:00
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:
+1
-1
@@ -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) => {
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+16
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+5
-12
@@ -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 {
|
||||
|
||||
+7
-7
@@ -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));
|
||||
|
||||
+2
-7
@@ -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);
|
||||
|
||||
+2
-4
@@ -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
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-2
@@ -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();
|
||||
|
||||
+8
-11
@@ -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
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+19
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+3
-4
@@ -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(() => {
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+19
-19
@@ -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'
|
||||
: ''}"
|
||||
>
|
||||
|
||||
+1
-2
@@ -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()) {
|
||||
|
||||
+1
-1
@@ -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 &&
|
||||
|
||||
+9
-16
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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);
|
||||
|
||||
+3
-4
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user