This document describes the new message management and context processing architecture that replaced the legacy SharedState system. The new design follows clean architecture principles with a single source of truth, computed views, and complete project isolation.
- Each message is stored exactly once in
MessageRepository - All UI and LLM views are computed from this single storage
- No complex dual-array synchronization or ID matching
User Input → ChatUIState → ChatManager → getCurrentMessageRepo() → MessageRepository + ContextManager
↓ ↓
ChatPersistenceManager Project-specific storage
↓
UI Components ← ChatUIState ← Computed Views ← MessageRepository
↓
LLM Processing ← Chain Memory ← getLLMMessages() ← MessageRepository
- Context is reprocessed when messages are edited
- No stale context issues from cached processing
- Ensures accurate context for LLM interactions
- Each project maintains its own isolated chat history
- Automatic detection and switching when project changes
- Zero configuration required - works automatically
- Non-project chats use a default repository
Purpose: Single source of truth for all messages
Key Concepts:
- Stores
StoredMessageobjects with bothdisplayTextandprocessedText displayText: What the user typed or AI responded (for UI display)processedText: For user messages, includes context. For AI messages, same as display
Core Methods:
// Add new message
addMessage(displayText: string, processedText: string, sender: string, context?: MessageContext): string
// Get computed views
getDisplayMessages(): ChatMessage[] // For UI rendering
getLLMMessages(): ChatMessage[] // For AI processing
// Edit operations
editMessage(id: string, newDisplayText: string): boolean
updateProcessedText(id: string, processedText: string): boolean
// Bulk operations
truncateAfterMessageId(messageId: string): void
loadMessages(messages: ChatMessage[]): voidPurpose: Central business logic coordinator
Responsibilities:
- Orchestrates MessageRepository, ContextManager, and LLM operations
- Handles all message CRUD operations with proper error handling
- Synchronizes with chain memory for conversation history
- Manages context processing lifecycle
- Project Isolation: Maintains separate MessageRepository per project
- Persistence: Integrates with ChatPersistenceManager for saving/loading
Key Operations:
// Send new message with context processing
async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise<string>
// Edit message and reprocess context
async editMessage(messageId: string, newText: string, chainType: ChainType, includeActiveNote?: boolean): Promise<boolean>
// Regenerate AI response
async regenerateMessage(messageId: string, onUpdateMessage: Function, onAddMessage: Function): Promise<boolean>
// Memory synchronization
private async updateChainMemory(): Promise<void>
// Project management
private getCurrentMessageRepo(): MessageRepository // Auto-detects current project
async handleProjectSwitch(): Promise<void> // Forces project detection
// Persistence
async saveChat(modelKey: string): Promise<{ success: boolean; path?: string; error?: string }>Project Isolation Implementation:
// Internal structure
private projectMessageRepos: Map<string, MessageRepository>
// Automatic project detection
getCurrentMessageRepo() {
const currentProjectId = ProjectManager.getCurrentProjectId() || defaultProjectKey;
if (!this.projectMessageRepos.has(currentProjectId)) {
// Create new repository for this project
const repo = new MessageRepository();
this.projectMessageRepos.set(currentProjectId, repo);
}
return this.projectMessageRepos.get(currentProjectId)!;
}Purpose: Clean UI-only state manager
Design Philosophy:
- Delegates ALL business logic to ChatManager
- Provides React integration with subscription mechanism
- Replaces legacy SharedState with minimal, focused approach
React Integration:
// Subscribe to state changes
subscribe(listener: () => void): () => void
// Delegate operations to ChatManager
async sendMessage(displayText: string, context: MessageContext, chainType: ChainType, includeActiveNote?: boolean): Promise<string>
getMessages(): ChatMessage[] // Computed view for UI
// Project and persistence operations
async handleProjectSwitch(): Promise<void> // Handle UI updates for project switch
async saveChat(modelKey: string): Promise<{ success: boolean; path?: string; error?: string }>
// Legacy compatibility (for backward compatibility)
get chatHistory(): ChatMessage[]
addMessage(message: ChatMessage): void
clearChatHistory(): void
// Notify React components of changes
private notifyListeners(): voidPurpose: Handles context processing and reprocessing
Key Features:
- Processes message context (notes, URLs, selected text)
- Reprocesses context when messages are edited
- Ensures fresh context for LLM processing
Purpose: Handles saving and loading chat history to/from markdown files
Key Features:
- Project-aware file naming (prefixes with project ID)
- Filters chat history files based on current project
- Parses and formats chat content for storage
- Integrated with ChatManager for seamless persistence
Core Methods:
// Save chat to markdown file
async saveChat(messages: ChatMessage[], modelKey: string, projectId?: string): Promise<{ success: boolean; path?: string; error?: string }>
// Get available chat history files
async getChatHistoryFiles(): Promise<TFile[]>
// File naming convention
// Project chats: `[projectId]-[timestamp]-[modelKey]-chat.md`
// Non-project chats: `[timestamp]-[modelKey]-chat.md`┌─────────────────────────────────────────────────────────────────────────────────────┐
│ User Interface Layer │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Chat.tsx │ ◄────── uses ──────────► │ CopilotView.tsx │ │
│ │ │ │ │ │
│ └────────┬────────┘ └──────────────────┘ │
│ │ │
│ │ subscribes to & calls │
│ ▼ │
└───────────┬─────────────────────────────────────────────────────────────────────────┘
│
┌───────────┴─────────────────────────────────────────────────────────────────────────┐
│ State Layer │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │ │
│ ┌────────▼────────┐ │
│ │ ChatUIState │ - React state management │
│ │ │ - Subscription mechanism for UI updates │
│ │ │ - Delegates all business logic to ChatManager │
│ └────────┬────────┘ │
│ │ │
└───────────┴─────────────────────────────────────────────────────────────────────────┘
│ delegates to
┌───────────▼─────────────────────────────────────────────────────────────────────────┐
│ Business Logic Layer │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ orchestrates ┌─────────────────────────────┐ │
│ │ ChatManager │ ◄──────────────────────────► │ ContextManager (singleton) │ │
│ │ │ │ │ │
│ │ - Message CRUD │ │ - Process message context │ │
│ │ - Project │ │ - Handle note attachments │ │
│ │ isolation │ │ - Reprocess on edit │ │
│ │ - Memory sync │ └─────────────────────────────┘ │
│ └────────┬────────┘ │
│ │ │
│ │ manages ┌─────────────────────────────┐ │
│ │ │ ChatPersistenceManager │ │
│ ├──────────────────────────────────────►│ │ │
│ │ │ - Save/load chat history │ │
│ │ │ - Project-aware file naming │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ coordinates ┌─────────────────────────────┐ │
│ ├──────────────────────────────────────►│ ChainManager │ │
│ │ │ │ │
│ │ │ - Memory management │ │
│ │ │ - LLM chain operations │ │
│ │ └──────────┬──────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────┐ │
│ │ │ MemoryManager │ │
│ │ │ │ │
│ │ │ - Chain memory storage │ │
│ │ │ - Conversation history │ │
│ │ └─────────────────────────────┘ │
└───────────┴─────────────────────────────────────────────────────────────────────────┘
│
┌───────────▼─────────────────────────────────────────────────────────────────────────┐
│ Data Storage Layer │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ MessageRepository │ │
│ │ │ │
│ │ ┌─────────────────┐ Computed Views ┌────────────────────────────┐ │ │
│ │ │ StoredMessage[] │ ──────────────────────► │ getDisplayMessages() │ │ │
│ │ │ │ │ (for UI rendering) │ │ │
│ │ │ - id │ └────────────────────────────┘ │ │
│ │ │ - displayText │ │ │
│ │ │ - processedText │ ──────────────────────► ┌────────────────────────────┐ │ │
│ │ │ - sender │ │ getLLMMessages() │ │ │
│ │ │ - timestamp │ │ (for AI processing) │ │ │
│ │ │ - context │ └────────────────────────────┘ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ Single source of truth - no dual storage! │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ ChatManager │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ projectMessageRepos: Map<string, MessageRepository> │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ "defaultProject" │ │ "project-1" │ │ "project-2" │ │
│ │ │ │ │ │ │ │
│ │ MessageRepo │ │ MessageRepo │ │ MessageRepo │ │
│ │ - Non-project │ │ - Project 1 │ │ - Project 2 │ │
│ │ messages │ │ messages only │ │ messages only │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ └─────────────────────────┴─────────────────────────┘ │
│ │ │
│ getCurrentMessageRepo() │
│ (auto-detects active project) │
│ │
└──────────────────────────────────────────────────────────────────────────────────────┘
Project Switch Detected (via Obsidian workspace)
↓
ProjectManager.getCurrentProjectId() returns new ID
↓
ChatManager.getCurrentMessageRepo()
↓
Check if repository exists for project
↓ (if not)
Create new MessageRepository
↓
Store in projectMessageRepos Map
↓
Return project-specific repository
When a user types "Summarize this note" and attaches "meeting-notes.md":
- Input: User text + attached file → Chat component
- Storage: MessageRepository stores displayText: "Summarize this note"
- Processing: ContextManager reads the note and creates processedText with proper XML structure
- Memory Sync: Chain memory receives the processed version for LLM
- UI Update: Shows message with context badge, displays only "Summarize this note"
- LLM Processing: AI receives full context and generates response
All context is wrapped in semantic XML tags for clear structure:
<note_context>
<title>meeting-notes</title>
<path>docs/meeting-notes.md</path>
<ctime>2024-01-15T10:00:00.000Z</ctime>
<mtime>2024-01-15T14:30:00.000Z</mtime>
<content>
[actual note content here]
</content>
</note_context><url_content>
<url>https://example.com/article</url>
<content>
[fetched content from URL]
</content>
</url_content><selected_text>
<title>Source Note Title</title>
<path>path/to/source.md</path>
<start_line>45</start_line>
<end_line>52</end_line>
<content>
[selected text content]
</content>
</selected_text><note_context_error>
<title>filename</title>
<path>path/to/file.ext</path>
<error>[Error: Could not process file]</error>
</note_context_error>This separation ensures:
- Clean UI (shows what user typed)
- Rich context for AI (includes note content)
- Reprocessable context on message edits
For detailed examples, see:
src/core/MessageLifecycle.test.ts- Complete lifecycle demonstration with context notessrc/core/MessageLifecycle.xmltags.test.ts- XML tag formatting tests and examples
User Input
↓ (via Chat component)
ChatUIState.sendMessage()
↓
ChatManager.sendMessage()
↓
MessageRepository.addMessage() // Store with basic content
↓
ContextManager.processMessageContext() // Add context
↓
MessageRepository.updateProcessedText() // Update with context
↓
ChatManager.updateChainMemory() // Sync to LLM
↓
ChatUIState.notifyListeners() // Update UI
User Edit
↓
ChatUIState.editMessage()
↓
ChatManager.editMessage()
↓
MessageRepository.editMessage() // Update display text
↓
ContextManager.reprocessMessageContext() // Fresh context
↓
ChatManager.updateChainMemory() // Sync to LLM
↓
ChatUIState.notifyListeners() // Update UI
React Component Render
↓
ChatUIState.getMessages()
↓
ChatManager.getDisplayMessages()
↓
ChatManager.getCurrentMessageRepo() // Project-aware
↓
MessageRepository.getDisplayMessages() // Computed view
↓
Filter visible messages → Map to ChatMessage format
User Save Action
↓
Chat.tsx → ChatUIState.saveChat(modelKey)
↓
ChatManager.saveChat(modelKey)
↓
Get current project ID and messages
↓
ChatPersistenceManager.saveChat(messages, modelKey, projectId)
↓
Create markdown file with project prefix
↓
Return success with file path
Project Change in Obsidian
↓
ChatUIState.handleProjectSwitch()
↓
ChatManager.handleProjectSwitch()
↓
Force getCurrentMessageRepo() to re-detect project
↓
Switch to different MessageRepository
↓
Update chain memory with new project's messages
↓
Notify UI listeners for refresh
interface StoredMessage {
id: string;
displayText: string; // What user typed/AI responded
processedText: string; // With context for user, same as display for AI
sender: string;
timestamp: FormattedDateTime;
context?: MessageContext;
isVisible: boolean;
isErrorMessage?: boolean;
sources?: { title: string; score: number }[];
content?: any[];
}interface ChatMessage {
id?: string;
message: string; // Display text
originalMessage?: string; // Processed text
sender: string;
timestamp: FormattedDateTime | null;
isVisible: boolean;
context?: MessageContext;
isErrorMessage?: boolean;
sources?: { title: string; score: number }[];
content?: any[];
}interface MessageContext {
notes: TFile[];
urls: string[];
selectedTextContexts: SelectedTextContext[];
}The new architecture uses a "pending message" pattern for loading chat history:
main.ts.loadChatHistory()
↓
Parse messages from file
↓
CopilotView.setPendingMessages()
↓
Chat component receives pendingMessages prop
↓
useEffect detects pendingMessages
↓
ChatUIState.loadMessages()
↓
onPendingMessagesProcessed() callback clears pending
When loading chat history:
- ChatPersistenceManager filters files based on current project
- Only shows chat files prefixed with current project ID
- Non-project chats visible when no project is active
- MessageRepository: 23 comprehensive tests including bug prevention
- ChatManager: 25+ tests covering all critical functionality
- Component Tests: MessageContext duplicate key prevention
- Context Badge Bug: Ensures context displays correctly
- Memory Synchronization: Prevents chat memory count mismatches
- Edit Message Bug: Verifies proper context reprocessing
- Duplicate Notes: Prevents React key conflicts in context display
// Multiple sources of truth
const sharedState = {
currentChatMessages: ChatMessage[],
chatHistory: ChatMessage[],
// Complex sync logic between arrays
}// Single source of truth
const messageRepository = new MessageRepository();
const chatManager = new ChatManager(messageRepository, ...);
const chatUIState = new ChatUIState(chatManager);
// Computed views
const displayMessages = chatUIState.getMessages(); // For UI
const llmMessages = chatManager.getLLMMessages(); // For AI- Single storage eliminates duplicate message objects
- Computed views are generated on-demand
- Context processing only when needed
- Subscription-based updates minimize re-renders
- Unique keys prevent React reconciliation issues
- State changes are batched through ChatUIState
- Complete Separation: Each project has entirely separate chat history
- Automatic Management: No user configuration needed
- Seamless Switching: Instant context switch when changing projects
- Memory Efficient: Only active project's messages in memory
- Fresh Start: Each project starts with empty chat history
- Project-Aware Naming: Files prefixed with project ID
- Filtered File Lists: Only shows relevant chat files
- Consistent Format: Same markdown format across all projects
- Error Handling: Graceful fallbacks for save/load failures
- Context not updating: Check if
updateChainMemory()is called after edits - UI not refreshing: Ensure
notifyListeners()is called after state changes - Memory count mismatch: Verify
truncateAfterMessageId()updates chain memory - Duplicate context badges: Check React keys in MessageContext component
- Wrong project messages: Check
getCurrentProjectId()returns expected value - Missing chat history: Verify project ID in filename matches current project
// Check message repository state
messageRepo.getDebugInfo();
// Check chat manager state
chatManager.getDebugInfo();
// Check LLM vs display message counts
console.log({
display: chatUIState.getMessages().length,
llm: chatManager.getLLMMessages().length,
});
// Check current project and repository
const debugInfo = chatManager.getDebugInfo();
console.log({
currentProject: debugInfo.currentProjectId,
totalProjects: debugInfo.projectCount,
messagesByProject: debugInfo.messageCountByProject,
});src/core/MessageRepository.ts- Message storagesrc/core/ChatManager.ts- Business logic with project isolationsrc/state/ChatUIState.ts- UI state managementsrc/core/ContextManager.ts- Context processingsrc/core/ChatPersistenceManager.ts- Chat history persistence
src/components/Chat.tsx- Main chat componentsrc/hooks/useChatManager.ts- React hook for ChatUIStatesrc/components/chat-components/ChatSingleMessage.tsx- Message display
src/core/MessageRepository.test.ts- Repository testssrc/core/ChatManager.test.ts- Manager testssrc/core/MessageLifecycle.test.ts- Complete lifecycle examples with context notessrc/core/MessageLifecycle.xmltags.test.ts- XML tag formatting tests and examplessrc/components/chat-components/MessageContext.test.tsx- Context display tests