mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* Implement context engine for a layered context * Wire in new context library to LLM chain - Updated the context engine to maintain a cumulative library of all context items (L2) without deduplication, enhancing cache stability. - Introduced smart referencing in the user message (L3) to reference existing items in L2 by ID and include full content for new items. - Improved the LayerToMessagesConverter to format and handle layered context envelopes for better debugging and clarity. - Fixed critical bugs related to context deduplication that previously broke cache stability. - Enhanced tests to validate the new context handling and ensure proper functionality across various scenarios. * Enhance VaultQAChainRunner with tag extraction and inline citation detection - Integrated tag extraction from user queries to improve context-aware retrieval. - Implemented `hasInlineCitations` function to detect inline citation markers in responses. - Updated message construction logic to utilize envelope-based context, enhancing the quality of AI interactions. - Added comprehensive tests for the new inline citation detection functionality, ensuring robustness and accuracy. - Improved logging for better debugging and traceability of the message processing flow. Refactor VaultQAChainRunner to enhance context handling and citation instructions - Integrated LayerToMessagesConverter for improved message construction, ensuring smart referencing and context preservation. - Updated logging to reflect the use of the new converter in envelope-based context construction. - Revised comments for clarity on context preparation and citation instructions. - Minor adjustment in citation utility to improve regex handling for citation updates. * Complete migration to envelope-based context in CopilotPlus and LLM chain runners - Removed all legacy fallback paths, ensuring that context envelopes are now mandatory for operations in CopilotPlusChainRunner and LLMChainRunner. - Enhanced image extraction to only pull from the active note, preventing unintended leaks from attached context notes. - Fixed critical issues with context-in-context (CiC) formatting and added early envelope guards in VaultQAChainRunner to prevent silent failures. - Updated message construction logic to utilize LayerToMessagesConverter for improved smart referencing and context preservation. - Comprehensive tests added to validate the new envelope-based context handling and ensure robustness across all chain runners. * Integrate Plus chain with context envelope * Enhance AutonomousAgentChainRunner with envelope-based context integration - Introduced `LayerToMessagesConverter` for improved message construction, ensuring system and user messages are derived from the context envelope. - Implemented envelope validation to ensure context is available before processing. - Updated message preparation logic to maintain consistency with the envelope-first architecture, preserving tool execution and multimodal support. - Added comprehensive logging for envelope-based context construction to aid debugging. - Documented changes in the context engineering documentation to reflect the new integration and design points. * Support loading saved chat context to L2 context library * Fix ChatPersistenceManager tests for context handling
755 lines
33 KiB
Markdown
755 lines
33 KiB
Markdown
# Message Architecture & Context Design
|
||
|
||
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.
|
||
|
||
**Note**: For detailed information about the layered context system (L1-L5 layers, L2 auto-promotion, cache optimization), see [CONTEXT_ENGINEERING.md](./CONTEXT_ENGINEERING.md).
|
||
|
||
## Architecture Principles
|
||
|
||
### Single Source of Truth
|
||
|
||
- 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
|
||
|
||
### Clean Architecture Flow
|
||
|
||
```
|
||
User Input → ChatUIState → ChatManager → getCurrentMessageRepo() → MessageRepository + ContextManager
|
||
↓ ↓
|
||
ChatPersistenceManager Project-specific storage
|
||
↓
|
||
UI Components ← ChatUIState ← Computed Views ← MessageRepository
|
||
↓
|
||
LLM Processing ← Chain Memory ← getLLMMessages() ← MessageRepository
|
||
```
|
||
|
||
### Context Always Fresh
|
||
|
||
- Context is reprocessed when messages are edited
|
||
- No stale context issues from cached processing
|
||
- Ensures accurate context for LLM interactions
|
||
|
||
### Project Isolation
|
||
|
||
- 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
|
||
|
||
## Core Components
|
||
|
||
### 1. MessageRepository (`src/core/MessageRepository.ts`)
|
||
|
||
**Purpose**: Single source of truth for all messages
|
||
|
||
**Key Concepts**:
|
||
|
||
- Stores `StoredMessage` objects with both `displayText` and `processedText`
|
||
- `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**:
|
||
|
||
```typescript
|
||
// Add new message
|
||
addMessage(
|
||
displayText: string,
|
||
processedText: string,
|
||
sender: string,
|
||
context?: MessageContext,
|
||
content?: MessageContent[]
|
||
): 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,
|
||
contextEnvelope?: PromptContextEnvelope
|
||
): boolean
|
||
|
||
// Bulk operations
|
||
truncateAfterMessageId(messageId: string): void
|
||
loadMessages(messages: ChatMessage[]): void
|
||
```
|
||
|
||
> **Storing envelopes**: When `ChatManager` calls `addMessage` with a full `NewChatMessage`, it includes the `contextEnvelope` property so the repository keeps both the legacy `processedText` and the canonical layered representation. The string-based overload remains for low-level utilities and test fixtures.
|
||
|
||
### 2. ChatManager (`src/core/ChatManager.ts`)
|
||
|
||
**Purpose**: 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
|
||
|
||
**Envelope Lifecycle**:
|
||
|
||
1. User sends a message → `ContextManager.processMessageContext()` returns both `processedContent` **and** a `PromptContextEnvelope`.
|
||
2. `ChatManager` stores the envelope with `MessageRepository.updateProcessedText(...)`.
|
||
3. When any chain runner executes, it reads `userMessage.contextEnvelope` and feeds it to `LayerToMessagesConverter.convert()` to materialize the L1-L5 prompt structure.
|
||
4. Regeneration or edits call `reprocessMessageContext`, ensuring a fresh envelope replaces the stale one.
|
||
|
||
**Key Operations**:
|
||
|
||
```typescript
|
||
// 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**:
|
||
|
||
```typescript
|
||
// 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)!;
|
||
}
|
||
```
|
||
|
||
### 3. ChatUIState (`src/state/ChatUIState.ts`)
|
||
|
||
**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**:
|
||
|
||
```typescript
|
||
// 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(): void
|
||
```
|
||
|
||
### 4. ContextManager (`src/core/ContextManager.ts`)
|
||
|
||
**Purpose**: Handles context processing and reprocessing with layered context architecture
|
||
|
||
**Key Features**:
|
||
|
||
- **Layered Context System**: Builds structured L1-L5 context layers (see CONTEXT_ENGINEERING.md)
|
||
- **L2 Auto-Promotion**: Automatically promotes previous turn's context to L2 for cache stability
|
||
- **Deduplication**: Ensures context appears only once (L3 takes priority over L2)
|
||
- **Context Processing**: Handles notes, URLs, selected text, tags, and folders
|
||
- **Reprocessing**: Regenerates fresh context when messages are edited
|
||
- **Envelope Building**: Creates `PromptContextEnvelope` with structured layers and hashes
|
||
- **Chain-Aware Processing**: Applies chain-specific rules (e.g., Copilot Plus URL processing, active-note handling for vision models)
|
||
|
||
**Core Methods**:
|
||
|
||
```typescript
|
||
// Process context for new message (includes L2 building from history)
|
||
async processMessageContext(
|
||
message: ChatMessage,
|
||
fileParserManager: FileParserManager,
|
||
vault: Vault,
|
||
chainType: ChainType,
|
||
includeActiveNote: boolean,
|
||
activeNote: TFile | null,
|
||
messageRepo: MessageRepository,
|
||
systemPrompt?: string
|
||
): Promise<ContextProcessingResult>
|
||
|
||
// Reprocess context for edited message
|
||
async reprocessMessageContext(messageId: string, ...): Promise<void>
|
||
```
|
||
|
||
### 5. ChatPersistenceManager (`src/core/ChatPersistenceManager.ts`)
|
||
|
||
**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**:
|
||
|
||
```typescript
|
||
// 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`
|
||
```
|
||
|
||
## Architecture Diagrams
|
||
|
||
### Complete System Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||
│ 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! │ │
|
||
│ └─────────────────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## Project Isolation Architecture
|
||
|
||
### Multi-Repository Design
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
||
│ 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 Flow
|
||
|
||
```
|
||
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
|
||
```
|
||
|
||
## Message Lifecycle
|
||
|
||
### Example: User Message with Context Note
|
||
|
||
When a user types "Summarize this note" and attaches "meeting-notes.md":
|
||
|
||
1. **Input**: User text + attached file → Chat component
|
||
2. **Storage**: MessageRepository stores displayText: "Summarize this note"
|
||
3. **Processing**: ContextManager reads the note and creates processedText with proper XML structure
|
||
4. **Memory Sync**: Chain memory receives the processed version for LLM
|
||
5. **UI Update**: Shows message with context badge, displays only "Summarize this note"
|
||
6. **LLM Processing**: AI receives full context and generates response
|
||
|
||
### Context XML Format
|
||
|
||
All context is wrapped in semantic XML tags for clear structure:
|
||
|
||
> Layered Prompting: During Phase 3 the raw XML is still captured in `processedText` for backward compatibility, but the canonical representation sent to the LLM comes from the envelope layers:
|
||
>
|
||
> - **L1_SYSTEM / L2_PREVIOUS**: Stable prefixes rendered from accumulated context
|
||
> - **L3_TURN**: Turn-specific smart references that either link back to L2 or embed full content
|
||
> - **L4_STRIP**: Chat history managed by memory
|
||
> - **L5_USER**: The user’s raw message (plus composer directives when present)
|
||
|
||
#### Note Context
|
||
|
||
```xml
|
||
<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 Context
|
||
|
||
```xml
|
||
<url_content>
|
||
<url>https://example.com/article</url>
|
||
<content>
|
||
[fetched content from URL]
|
||
</content>
|
||
</url_content>
|
||
```
|
||
|
||
#### Selected Text Context
|
||
|
||
```xml
|
||
<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>
|
||
```
|
||
|
||
#### Error Cases
|
||
|
||
```xml
|
||
<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 notes
|
||
- `src/core/MessageLifecycle.xmltags.test.ts` - XML tag formatting tests and examples
|
||
|
||
### 1. Sending a New Message
|
||
|
||
```
|
||
User Input
|
||
↓ (via Chat component)
|
||
ChatUIState.sendMessage()
|
||
↓
|
||
ChatManager.sendMessage()
|
||
↓
|
||
MessageRepository.addMessage() // Store with basic content
|
||
↓
|
||
ContextManager.processMessageContext() // Add context
|
||
↓
|
||
MessageRepository.updateProcessedText() // Update with processed text + context envelope
|
||
↓
|
||
ChatManager.updateChainMemory() // Sync to LLM
|
||
↓
|
||
ChatUIState.notifyListeners() // Update UI
|
||
```
|
||
|
||
### 2. Editing a Message
|
||
|
||
```
|
||
User Edit
|
||
↓
|
||
ChatUIState.editMessage()
|
||
↓
|
||
ChatManager.editMessage()
|
||
↓
|
||
MessageRepository.editMessage() // Update display text
|
||
↓
|
||
ContextManager.reprocessMessageContext() // Fresh context
|
||
↓
|
||
ChatManager.updateChainMemory() // Sync to LLM
|
||
↓
|
||
ChatUIState.notifyListeners() // Update UI
|
||
```
|
||
|
||
### 3. Message Display
|
||
|
||
```
|
||
React Component Render
|
||
↓
|
||
ChatUIState.getMessages()
|
||
↓
|
||
ChatManager.getDisplayMessages()
|
||
↓
|
||
ChatManager.getCurrentMessageRepo() // Project-aware
|
||
↓
|
||
MessageRepository.getDisplayMessages() // Computed view
|
||
↓
|
||
Filter visible messages → Map to ChatMessage format
|
||
```
|
||
|
||
### 4. Saving Chat History
|
||
|
||
```
|
||
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
|
||
```
|
||
|
||
### 5. Project Switch
|
||
|
||
```
|
||
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
|
||
```
|
||
|
||
## Data Structures
|
||
|
||
### StoredMessage (Internal)
|
||
|
||
```typescript
|
||
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[];
|
||
}
|
||
```
|
||
|
||
### ChatMessage (External Interface)
|
||
|
||
```typescript
|
||
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[];
|
||
}
|
||
```
|
||
|
||
### MessageContext
|
||
|
||
```typescript
|
||
interface MessageContext {
|
||
notes: TFile[];
|
||
urls: string[];
|
||
selectedTextContexts: SelectedTextContext[];
|
||
}
|
||
```
|
||
|
||
## Chat History Loading
|
||
|
||
### Pending Message Mechanism
|
||
|
||
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
|
||
```
|
||
|
||
### Project-Aware Loading
|
||
|
||
When loading chat history:
|
||
|
||
1. ChatPersistenceManager filters files based on current project
|
||
2. Only shows chat files prefixed with current project ID
|
||
3. Non-project chats visible when no project is active
|
||
|
||
## Testing Strategy
|
||
|
||
### Unit Tests
|
||
|
||
- **MessageRepository**: 23 comprehensive tests including bug prevention
|
||
- **ChatManager**: 25+ tests covering all critical functionality
|
||
- **Component Tests**: MessageContext duplicate key prevention
|
||
|
||
### Bug Prevention Tests
|
||
|
||
1. **Context Badge Bug**: Ensures context displays correctly
|
||
2. **Memory Synchronization**: Prevents chat memory count mismatches
|
||
3. **Edit Message Bug**: Verifies proper context reprocessing
|
||
4. **Duplicate Notes**: Prevents React key conflicts in context display
|
||
|
||
## Migration from SharedState
|
||
|
||
### Before (Legacy)
|
||
|
||
```typescript
|
||
// Multiple sources of truth
|
||
const sharedState = {
|
||
currentChatMessages: ChatMessage[],
|
||
chatHistory: ChatMessage[],
|
||
// Complex sync logic between arrays
|
||
}
|
||
```
|
||
|
||
### After (Clean Architecture)
|
||
|
||
```typescript
|
||
// 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
|
||
```
|
||
|
||
## Performance Considerations
|
||
|
||
### Memory Efficiency
|
||
|
||
- Single storage eliminates duplicate message objects
|
||
- Computed views are generated on-demand
|
||
- Context processing only when needed
|
||
|
||
### React Optimization
|
||
|
||
- Subscription-based updates minimize re-renders
|
||
- Unique keys prevent React reconciliation issues
|
||
- State changes are batched through ChatUIState
|
||
|
||
## Key Architectural Features
|
||
|
||
### Project Isolation Benefits
|
||
|
||
1. **Complete Separation**: Each project has entirely separate chat history
|
||
2. **Automatic Management**: No user configuration needed
|
||
3. **Seamless Switching**: Instant context switch when changing projects
|
||
4. **Memory Efficient**: Only active project's messages in memory
|
||
5. **Fresh Start**: Each project starts with empty chat history
|
||
|
||
### Persistence Integration
|
||
|
||
1. **Project-Aware Naming**: Files prefixed with project ID
|
||
2. **Filtered File Lists**: Only shows relevant chat files
|
||
3. **Consistent Format**: Same markdown format across all projects
|
||
4. **Error Handling**: Graceful fallbacks for save/load failures
|
||
|
||
## Troubleshooting
|
||
|
||
### Common Issues
|
||
|
||
1. **Context not updating**: Check if `updateChainMemory()` is called after edits
|
||
2. **UI not refreshing**: Ensure `notifyListeners()` is called after state changes
|
||
3. **Memory count mismatch**: Verify `truncateAfterMessageId()` updates chain memory
|
||
4. **Duplicate context badges**: Check React keys in MessageContext component
|
||
5. **Wrong project messages**: Check `getCurrentProjectId()` returns expected value
|
||
6. **Missing chat history**: Verify project ID in filename matches current project
|
||
|
||
### Debug Methods
|
||
|
||
```typescript
|
||
// 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,
|
||
});
|
||
```
|
||
|
||
## Related Files
|
||
|
||
### Documentation
|
||
|
||
- `docs/CONTEXT_ENGINEERING.md` - Layered context system design and L2 auto-promotion details
|
||
- `docs/MESSAGE_ARCHITECTURE.md` - This document (message management architecture)
|
||
|
||
### Core Implementation
|
||
|
||
- `src/core/MessageRepository.ts` - Message storage
|
||
- `src/core/ChatManager.ts` - Business logic with project isolation
|
||
- `src/state/ChatUIState.ts` - UI state management
|
||
- `src/core/ContextManager.ts` - Context processing
|
||
- `src/core/ChatPersistenceManager.ts` - Chat history persistence
|
||
|
||
### React Integration
|
||
|
||
- `src/components/Chat.tsx` - Main chat component
|
||
- `src/hooks/useChatManager.ts` - React hook for ChatUIState
|
||
- `src/components/chat-components/ChatSingleMessage.tsx` - Message display
|
||
|
||
### Testing
|
||
|
||
- `src/core/MessageRepository.test.ts` - Repository tests
|
||
- `src/core/ChatManager.test.ts` - Manager tests
|
||
- `src/core/MessageLifecycle.test.ts` - Complete lifecycle examples with context notes
|
||
- `src/core/MessageLifecycle.xmltags.test.ts` - XML tag formatting tests and examples
|
||
- `src/components/chat-components/MessageContext.test.tsx` - Context display tests
|