ui: Stores architecture improvements (#26910)

* refactor: Stores barrel imports + SSR gates

* refactor: Drop agenticStore wrapper exports

* refactor: Drop chatStore wrapper exports

* refactor: Drop modelsStore wrapper exports

* refactor: Drop serverStore wrapper exports

* refactor: Drop unused mcpStore wrapper exports

* refactor: Drop mcpResourceStore wrapper exports

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

* refactor: Drop settingsStore wrapper exports

* refactor: Drop unused toolsStore wrapper exports

* refactor: Fix lint errors from store wrapper removal

* fix: Missing change

* refactor: Cleanup

* refactor: Context Stats store
This commit is contained in:
Aleksander Grygier
2026-08-13 08:11:30 +02:00
committed by GitHub
parent a6040c925c
commit 094e53db1c
116 changed files with 937 additions and 945 deletions
@@ -8,24 +8,21 @@
* demand if they aren't cached yet.
*/
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte';
import { modelOptions, modelsStore, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
export function useChatScreenActiveModel() {
const isRouter = $derived(isRouterMode());
const isRouter = $derived(serverStore.isRouterMode);
const conversationModel = $derived(
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
);
const activeModelId = $derived.by(() => {
const options = modelOptions();
const options = modelsStore.models;
if (!isRouter) {
return options.length > 0 ? options[0].model : null;
}
const selectedId = selectedModelId();
const selectedId = modelsStore.selectedModelId;
if (selectedId) {
const model = options.find((m) => m.id === selectedId);
@@ -7,7 +7,7 @@
* caller's onDrop callback.
*/
import { getAddFilesHandler, isEditing } from '$lib/stores/chat.svelte';
import { chatStore } from '$lib/stores';
interface UseChatScreenDragAndDropOptions {
/** Called when the user drops files and no message is being edited. */
@@ -49,8 +49,8 @@ export function useChatScreenDragAndDrop(options: UseChatScreenDragAndDropOption
const files = Array.from(event.dataTransfer.files);
if (isEditing()) {
const handler = getAddFilesHandler();
if (chatStore.isEditing()) {
const handler = chatStore.getAddFilesHandler();
if (handler) {
handler(files);
@@ -88,7 +88,9 @@ export function useChatScreenFileUpload(options: UseChatScreenFileUploadOptions)
}
return {
fileErrorData,
get fileErrorData() {
return fileErrorData;
},
handleFileRemove,
handleFileUpload,
get showFileErrorDialog() {
@@ -1,30 +1,14 @@
/**
* Reactive state for the context usage gauge: resolves the active model,
* fetches its cached props, parses live server stats, and exposes per-turn
* read / fresh / cache / output and cumulative token counts.
* View layer over contextStatsStore for the context usage gauge: adds
* color levels, transient detail formatting, on-demand /props fetching
* and model loading on top of the store's token stats.
*/
import { useProcessingState } from './use-processing-state.svelte';
import { colorLevelFromPercent } from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge';
import { STATS_UNITS } from '$lib/constants';
import { ColorLevel, MessageRole } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte';
import {
modelOptions,
modelsStore,
selectedModelId,
singleModelName
} from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import type { ChatMessageTimings, DatabaseMessage } from '$lib/types';
interface LiveStats {
freshTokens: number;
promptTokens: number;
cacheTokens: number;
outputTokens: number;
}
import { ColorLevel } from '$lib/enums';
import { contextStatsStore, modelsStore } from '$lib/stores';
export interface UseContextGaugeReturn {
readonly activeModelId: string | null;
@@ -32,6 +16,7 @@ export interface UseContextGaugeReturn {
readonly isActiveModelLoading: boolean;
readonly contextTotal: number | null;
readonly contextUsed: number;
readonly contextAvailable: number | null;
readonly currentRead: number;
readonly currentFresh: number;
readonly currentCache: number;
@@ -49,34 +34,6 @@ export interface UseContextGaugeReturn {
startMonitoring(): void;
}
function lastAssistantTimings(messages: DatabaseMessage[]): ChatMessageTimings | undefined {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.role === MessageRole.ASSISTANT && m.timings) return m.timings;
}
return undefined;
}
function deriveLiveStats(
state: ReturnType<typeof useProcessingState>['processingState']
): LiveStats | null {
if (!state || (state.status !== 'preparing' && state.status !== 'generating')) {
return null;
}
const promptTokens = state.promptTokens ?? 0;
const cacheTokens = state.cacheTokens ?? 0;
return {
cacheTokens,
freshTokens: promptTokens,
outputTokens: state.outputTokensUsed ?? 0,
promptTokens: promptTokens + cacheTokens
};
}
const TRANSIENT_DETAILS_EXCLUDED_PREFIXES = ['Context:', 'Output:'];
function filterTransientDetails(raw: string[]): string[] {
@@ -91,150 +48,39 @@ function filterTransientDetails(raw: string[]): string[] {
export function useContextGauge(): UseContextGaugeReturn {
const processingState = useProcessingState();
// Resolve the model the gauge reports context for: explicit selection >
// last assistant model > single-model mode (mirrors useChatScreenActiveModel).
const activeModelId = $derived.by(() => {
if (!isRouterMode()) {
return singleModelName();
}
const selectedId = selectedModelId();
if (selectedId) {
const model = modelOptions().find((m) => m.id === selectedId);
if (model) return model.model;
}
return chatStore.getConversationModel(activeMessages() as DatabaseMessage[]);
});
const isActiveModelLoaded = $derived(
activeModelId !== null && (!isRouterMode() || modelsStore.isModelLoaded(activeModelId))
);
const isActiveModelLoading = $derived(
activeModelId !== null && modelsStore.isModelOperationInProgress(activeModelId)
);
// Pull /props on demand so n_ctx surfaces before the first chat request.
$effect(() => {
if (activeModelId && isActiveModelLoaded) {
const cached = modelsStore.getModelProps(activeModelId);
const modelId = contextStatsStore.activeModelId;
if (modelId && contextStatsStore.isActiveModelLoaded) {
const cached = modelsStore.getModelProps(modelId);
if (!cached) {
void modelsStore.fetchModelProps(activeModelId);
void modelsStore.fetchModelProps(modelId);
}
}
});
const contextTotal = $derived.by(() => {
void modelsStore.propsCacheVersion;
return activeModelId ? modelsStore.getModelContextSize(activeModelId) : null;
});
const liveStats = $derived(deriveLiveStats(processingState.processingState));
const currentRead = $derived.by(() => {
const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
let read = 0;
if (timings) {
read = (timings.prompt_n ?? 0) + (timings.cache_n ?? 0);
}
// live.promptTokens is already the combined reading (prompt + cache),
// so do not also add live.cacheTokens.
if (liveStats && liveStats.promptTokens > 0) {
read = Math.max(read, liveStats.promptTokens);
}
return read;
});
const currentFresh = $derived.by(() => {
const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
const fresh = timings?.prompt_n ?? 0;
return Math.max(fresh, liveStats?.freshTokens ?? 0);
});
const currentCache = $derived.by(() => {
const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
const cached = timings?.cache_n ?? 0;
if (liveStats && liveStats.promptTokens > 0) {
return Math.max(cached, liveStats.cacheTokens);
}
return cached;
});
const currentOutput = $derived.by(() => {
if (liveStats && liveStats.outputTokens > 0) return liveStats.outputTokens;
const timings = lastAssistantTimings(activeMessages() as DatabaseMessage[]);
return timings?.predicted_n ?? 0;
});
const kvTotal = $derived(currentRead + currentOutput);
const contextUsed = $derived(currentRead + currentOutput);
const cumulative = $derived.by(() => {
const messages = activeMessages() as DatabaseMessage[];
// Agentic sessions stamp the same agentic.llm totals onto every
// assistant message; cache_n is never per-turn so cache_total stays 0.
const agenticMessages = messages.filter(
(m) => m.role === MessageRole.ASSISTANT && m.timings?.agentic?.llm?.predicted_n != null
);
if (agenticMessages.length > 0) {
const llm = agenticMessages[agenticMessages.length - 1].timings!.agentic!.llm;
const output = llm.predicted_n ?? 0;
const outputMs = llm.predicted_ms ?? 0;
const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
return {
averageTokensPerSecond,
cacheTotal: 0,
output,
read: llm.prompt_n ?? 0
};
}
let read = 0;
let output = 0;
let outputMs = 0;
let cacheTotal = 0;
for (const m of messages) {
if (m.role !== MessageRole.ASSISTANT || !m.timings) continue;
read += m.timings.prompt_n ?? 0;
cacheTotal += m.timings.cache_n ?? 0;
output += m.timings.predicted_n ?? 0;
outputMs += m.timings.predicted_ms ?? 0;
}
const averageTokensPerSecond = outputMs > 0 && output > 0 ? (output / outputMs) * 1000 : null;
return { averageTokensPerSecond, cacheTotal, output, read };
});
const contextPercent = $derived.by(() => {
if (contextTotal === null || contextTotal <= 0) return null;
return Math.round((contextUsed / contextTotal) * 100);
});
const colorLevel = $derived(colorLevelFromPercent(contextPercent));
const colorLevel = $derived(colorLevelFromPercent(contextStatsStore.contextPercent));
// Drop lines the surrounding Context / Output / speed rows already render.
const transientDetails = $derived(filterTransientDetails(processingState.getTechnicalDetails()));
const hasAnyUsage = $derived(
cumulative.read > 0 ||
cumulative.output > 0 ||
currentRead > 0 ||
currentOutput > 0 ||
cumulative.averageTokensPerSecond !== null ||
contextStatsStore.cumulativeRead > 0 ||
contextStatsStore.cumulativeOutput > 0 ||
contextStatsStore.currentRead > 0 ||
contextStatsStore.currentOutput > 0 ||
contextStatsStore.averageTokensPerSecond !== null ||
transientDetails.length > 0
);
async function loadModel() {
if (!activeModelId || isActiveModelLoading) return;
const modelId = contextStatsStore.activeModelId;
if (!modelId || contextStatsStore.isActiveModelLoading) return;
try {
await modelsStore.loadModel(activeModelId);
await modelsStore.loadModel(modelId);
} catch {
// toast already surfaced by modelsStore.loadModel
}
@@ -242,55 +88,58 @@ export function useContextGauge(): UseContextGaugeReturn {
return {
get activeModelId() {
return activeModelId;
return contextStatsStore.activeModelId;
},
get averageTokensPerSecond() {
return cumulative.averageTokensPerSecond;
return contextStatsStore.averageTokensPerSecond;
},
get colorLevel() {
return colorLevel;
},
get contextAvailable() {
return contextStatsStore.contextAvailable;
},
get contextPercent() {
return contextPercent;
return contextStatsStore.contextPercent;
},
get contextTotal() {
return contextTotal;
return contextStatsStore.contextTotal;
},
get contextUsed() {
return contextUsed;
return contextStatsStore.contextUsed;
},
get cumulativeCacheTotal() {
return cumulative.cacheTotal;
return contextStatsStore.cumulativeCacheTotal;
},
get cumulativeOutput() {
return cumulative.output;
return contextStatsStore.cumulativeOutput;
},
get cumulativeRead() {
return cumulative.read;
return contextStatsStore.cumulativeRead;
},
get currentCache() {
return currentCache;
return contextStatsStore.currentCache;
},
get currentFresh() {
return currentFresh;
return contextStatsStore.currentFresh;
},
get currentOutput() {
return currentOutput;
return contextStatsStore.currentOutput;
},
get currentRead() {
return currentRead;
return contextStatsStore.currentRead;
},
get hasAnyUsage() {
return hasAnyUsage;
},
get isActiveModelLoaded() {
return isActiveModelLoaded;
return contextStatsStore.isActiveModelLoaded;
},
get isActiveModelLoading() {
return isActiveModelLoading;
return contextStatsStore.isActiveModelLoading;
},
get kvTotal() {
return kvTotal;
return contextStatsStore.kvTotal;
},
loadModel,
startMonitoring: () => processingState.startMonitoring(),
@@ -1,5 +1,5 @@
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { draftMessagesStore } from '$lib/stores/draft-messages.svelte';
import { draftMessagesStore } from '$lib/stores';
import { onMount } from 'svelte';
interface UseDraftMessagesOptions {
@@ -1,14 +1,6 @@
import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils';
import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
import {
modelOptions,
modelsLoading,
modelsStore,
modelsUpdating,
selectedModelId,
singleModelName
} from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { modelsStore, serverStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { onMount } from 'svelte';
@@ -54,17 +46,17 @@ export interface UseModelsSelectorReturn {
*/
export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn {
const options = $derived(
modelOptions().filter((option) => {
modelsStore.models.filter((option) => {
const modelProps = modelsStore.getModelProps(option.model);
return modelProps?.ui !== false;
})
);
const loading = $derived(modelsLoading());
const updating = $derived(modelsUpdating());
const activeId = $derived(selectedModelId());
const isRouter = $derived(isRouterMode());
const serverModel = $derived(singleModelName());
const loading = $derived(modelsStore.loading);
const updating = $derived(modelsStore.updating);
const activeId = $derived(modelsStore.selectedModelId);
const isRouter = $derived(serverStore.isRouterMode);
const serverModel = $derived(modelsStore.singleModelName);
const currentModel = $derived(opts.currentModel());
const onModelChange = $derived(opts.onModelChange?.());
const isHighlightedCurrentModelActive = $derived.by(() => {
@@ -1,5 +1,5 @@
import { STATS_UNITS } from '$lib/constants';
import { activeProcessingState } from '$lib/stores/chat.svelte';
import { chatStore } from '$lib/stores';
import type { ApiProcessingState, LiveGenerationStats, LiveProcessingStats } from '$lib/types';
export interface UseProcessingStateReturn {
@@ -42,8 +42,8 @@ export function useProcessingState(): UseProcessingStateReturn {
return lastKnownState;
}
// Read directly from the reactive state export
return activeProcessingState();
// Read directly from the reactive state
return chatStore.activeProcessingState;
});
$effect(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { browser } from '$app/environment';
import { BUILD_VERSION_LOCALSTORAGE_KEY, SW_CONFIG } from '$lib/constants';
import { versionStore } from '$lib/stores/version.svelte';
import { versionStore } from '$lib/stores';
import { useRegisterSW } from 'virtual:pwa-register/svelte';
/**
@@ -1,15 +1,6 @@
import { REASONING_EFFORT_LEVELS, REASONING_EFFORT_TOKENS } from '$lib/constants';
import { ReasoningEffort } from '$lib/enums';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
import {
checkModelSupportsThinking,
loadedModelIds,
modelsStore,
propsCacheVersion,
supportsThinking
} from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore, conversationsStore, modelsStore, serverStore } from '$lib/stores';
import type { ReasoningEffortLevel } from '$lib/types';
import type { DatabaseMessage } from '$lib/types/database';
@@ -33,12 +24,14 @@ export interface UseReasoningMenuReturn {
*/
export function useReasoningMenu(): UseReasoningMenuReturn {
const conversationModel = $derived(
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
);
// a router chat can carry reasoning from an earlier turn before the props
// cache is primed, so a model that already produced thinking still qualifies
const modelSupportsThinkingFromMessages = $derived.by(() => {
const modelId = isRouterMode() ? modelsStore.selectedModelName || conversationModel : null;
const modelId = serverStore.isRouterMode
? modelsStore.selectedModelName || conversationModel
: null;
if (!modelId) return false;
@@ -47,16 +40,18 @@ export function useReasoningMenu(): UseReasoningMenuReturn {
);
});
const modelSupportsThinking = $derived.by(() => {
loadedModelIds();
propsCacheVersion();
void modelsStore.loadedModelIds;
void modelsStore.propsCacheVersion;
if (isRouterMode()) {
if (serverStore.isRouterMode) {
const modelId = modelsStore.selectedModelName || conversationModel;
return checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages;
return (
modelsStore.checkModelSupportsThinking(modelId ?? '') || modelSupportsThinkingFromMessages
);
}
return supportsThinking() || modelSupportsThinkingFromMessages;
return modelsStore.supportsThinking || modelSupportsThinkingFromMessages;
});
const currentEffort = $derived(conversationsStore.getReasoningEffort());
const thinkingEnabled = $derived(
@@ -1,7 +1,7 @@
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import { ROUTES } from '$lib/constants';
import { settingsReferrer } from '$lib/stores/settings-referrer.svelte';
import { settingsReferrer } from '$lib/stores';
export interface ChatSettings {
reset: () => void;
@@ -1,8 +1,6 @@
import { CLI_FLAGS } from '$lib/constants';
import { ToolSource } from '$lib/enums';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { toolsStore } from '$lib/stores/tools.svelte';
import { conversationsStore, mcpStore, toolsStore } from '$lib/stores';
import type { ToolGroup } from '$lib/types';
import { SvelteSet } from 'svelte/reactivity';