From 7ba1b458d50365900b8fd8ca958b0a773277756c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 27 Jan 2026 14:27:13 +0100 Subject: [PATCH] refactor: Create shared ActiveConversationStore to avoid circular dependency between ChatStore and ConversationsStore --- .../webui/src/lib/stores/chat.svelte.ts | 52 +++++++++++-------- .../src/lib/stores/conversations.svelte.ts | 16 +++++- .../shared/active-conversation.svelte.ts | 49 +++++++++++++++++ .../webui/src/lib/stores/shared/index.ts | 18 +++++++ 4 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 tools/server/webui/src/lib/stores/shared/active-conversation.svelte.ts create mode 100644 tools/server/webui/src/lib/stores/shared/index.ts diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index dafa908f16..09852e5f60 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -33,6 +33,7 @@ import { MAX_INACTIVE_CONVERSATION_STATES, INACTIVE_CONVERSATION_STATE_MAX_AGE_MS } from '$lib/constants/cache'; +import { isActiveConversation } from '$lib/stores/shared'; interface ConversationStateEntry { lastAccessed: number; @@ -56,6 +57,14 @@ class ChatStore { private addFilesHandler: ((files: File[]) => void) | null = $state(null); pendingEditMessageId = $state(null); + /** + * Callback for updating message content in conversationsStore. + * Registered by conversationsStore to avoid circular dependency. + */ + private messageUpdateCallback: + | ((messageId: string, updates: Partial) => void) + | null = null; + // Draft preservation for navigation (e.g., when adding system prompt from welcome page) private _pendingDraftMessage = $state(''); private _pendingDraftFiles = $state([]); @@ -84,30 +93,24 @@ class ChatStore { private setChatLoading(convId: string, loading: boolean): void { this.touchConversationState(convId); - import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => { - if (loading) { - this.chatLoadingStates.set(convId, true); - if (conversationsStore.activeConversation?.id === convId) this.isLoading = true; - } else { - this.chatLoadingStates.delete(convId); - if (conversationsStore.activeConversation?.id === convId) this.isLoading = false; - } - }); + if (loading) { + this.chatLoadingStates.set(convId, true); + if (isActiveConversation(convId)) this.isLoading = true; + } else { + this.chatLoadingStates.delete(convId); + if (isActiveConversation(convId)) this.isLoading = false; + } } private setChatStreaming(convId: string, response: string, messageId: string): void { this.touchConversationState(convId); this.chatStreamingStates.set(convId, { response, messageId }); - import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => { - if (conversationsStore.activeConversation?.id === convId) this.currentResponse = response; - }); + if (isActiveConversation(convId)) this.currentResponse = response; } private clearChatStreaming(convId: string): void { this.chatStreamingStates.delete(convId); - import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => { - if (conversationsStore.activeConversation?.id === convId) this.currentResponse = ''; - }); + if (isActiveConversation(convId)) this.currentResponse = ''; } private getChatStreaming(convId: string): { response: string; messageId: string } | undefined { @@ -121,16 +124,21 @@ class ChatStore { // If there's an active stream for this conversation, update the message content // This ensures streaming content is visible when switching back to a conversation - if (streamingState?.response && streamingState?.messageId) { - import('$lib/stores/conversations.svelte').then(({ conversationsStore }) => { - const idx = conversationsStore.findMessageIndex(streamingState.messageId); - if (idx !== -1) { - conversationsStore.updateMessageAtIndex(idx, { content: streamingState.response }); - } - }); + if (streamingState?.response && streamingState?.messageId && this.messageUpdateCallback) { + this.messageUpdateCallback(streamingState.messageId, { content: streamingState.response }); } } + /** + * Register a callback for updating message content. + * Called by conversationsStore during initialization to avoid circular dependency. + */ + registerMessageUpdateCallback( + callback: (messageId: string, updates: Partial) => void + ): void { + this.messageUpdateCallback = callback; + } + clearUIState(): void { this.isLoading = false; this.currentResponse = ''; diff --git a/tools/server/webui/src/lib/stores/conversations.svelte.ts b/tools/server/webui/src/lib/stores/conversations.svelte.ts index 786d9263e5..1e992219b8 100644 --- a/tools/server/webui/src/lib/stores/conversations.svelte.ts +++ b/tools/server/webui/src/lib/stores/conversations.svelte.ts @@ -21,6 +21,8 @@ import { browser } from '$app/environment'; import type { McpServerOverride } from '$lib/types/database'; +import { setActiveConversationId } from '$lib/stores/shared'; +import { chatStore } from '$lib/stores/chat.svelte'; class ConversationsStore { /** List of all conversations */ @@ -65,11 +67,23 @@ class ConversationsStore { const { conversationsClient } = await import('$lib/clients/conversations.client'); this._client = conversationsClient; + // Register message update callback with chatStore to avoid circular dependency + chatStore.registerMessageUpdateCallback((messageId, updates) => { + const idx = this.findMessageIndex(messageId); + if (idx !== -1) { + this.updateMessageAtIndex(idx, updates); + } + }); + conversationsClient.setStoreCallbacks({ getConversations: () => this.conversations, setConversations: (conversations) => (this.conversations = conversations), getActiveConversation: () => this.activeConversation, - setActiveConversation: (conversation) => (this.activeConversation = conversation), + setActiveConversation: (conversation) => { + this.activeConversation = conversation; + // Update shared state for chatStore to use without circular dependency + setActiveConversationId(conversation?.id ?? null); + }, getActiveMessages: () => this.activeMessages, setActiveMessages: (messages) => (this.activeMessages = messages), updateActiveMessages: (updater) => (this.activeMessages = updater(this.activeMessages)), diff --git a/tools/server/webui/src/lib/stores/shared/active-conversation.svelte.ts b/tools/server/webui/src/lib/stores/shared/active-conversation.svelte.ts new file mode 100644 index 0000000000..0b69fcb6fe --- /dev/null +++ b/tools/server/webui/src/lib/stores/shared/active-conversation.svelte.ts @@ -0,0 +1,49 @@ +/** + * Shared Active Conversation State + * + * This module provides a dependency-free shared state for tracking the active conversation. + * It eliminates circular dependencies between chatStore and conversationsStore. + * + * **Why this exists:** + * - chatStore needs to know the active conversation ID to sync global loading/streaming state + * - conversationsStore manages the active conversation + * - Direct imports between stores would create circular dependencies + * + * **Usage:** + * - conversationsStore: calls setId() when switching conversations + * - chatStore: calls isActive() to check if state should sync to global + */ + +class ActiveConversationStore { + private _id = $state(null); + + /** + * Get the currently active conversation ID. + * Returns null if no conversation is active. + */ + get id(): string | null { + return this._id; + } + + /** + * Set the active conversation ID. + * Should only be called by conversationsStore when switching conversations. + */ + setId(id: string | null): void { + this._id = id; + } + + /** + * Check if the given conversation ID is the currently active one. + */ + isActive(convId: string): boolean { + return this._id === convId; + } +} + +export const activeConversationStore = new ActiveConversationStore(); + +// Convenience exports for backward compatibility +export const getActiveConversationId = () => activeConversationStore.id; +export const setActiveConversationId = (id: string | null) => activeConversationStore.setId(id); +export const isActiveConversation = (convId: string) => activeConversationStore.isActive(convId); diff --git a/tools/server/webui/src/lib/stores/shared/index.ts b/tools/server/webui/src/lib/stores/shared/index.ts new file mode 100644 index 0000000000..10f11bd2e9 --- /dev/null +++ b/tools/server/webui/src/lib/stores/shared/index.ts @@ -0,0 +1,18 @@ +/** + * Shared State Modules + * + * This directory contains dependency-free state modules that can be safely + * imported by any store without creating circular dependencies. + * + * **Rules for modules in this folder:** + * - NO imports from other stores + * - NO imports from clients or services + * - Only pure reactive state with no business logic + */ + +export { + activeConversationStore, + getActiveConversationId, + setActiveConversationId, + isActiveConversation +} from './active-conversation.svelte';