Adds a one-command dev workflow: install + build + symlink main.js / manifest.json / styles.css from the current worktree into $COPILOT_TEST_VAULT_PATH/.obsidian/plugins/copilot/, then reload the plugin via the Obsidian CLI. Removes the manual build/copy/reload loop across Conductor worktrees. Docs added to CONTRIBUTING.md and AGENTS.md.
19 KiB
AGENTS.md
This file provides guidance to any coding agent when working with code in this repository.
Overview
Copilot for Obsidian is an AI-powered assistant plugin that integrates various LLM providers (OpenAI, Anthropic, Google, etc.) with Obsidian. It provides chat interfaces, autocomplete, semantic search, and various AI-powered commands for note-taking and knowledge management.
Development Commands
Build & Development
- NEVER RUN
npm run dev- The user will handle all builds manually npm run build- Production build (TypeScript check + minified output)npm run test:vault- macOS only. Installs deps, builds, symlinksmain.js/manifest.json/styles.cssfrom the current worktree into$COPILOT_TEST_VAULT_PATH/.obsidian/plugins/copilot/, then reloads the plugin via the Obsidian CLI. Requires the user-level env varCOPILOT_TEST_VAULT_PATHto be set to a vault that has been opened in Obsidian at least once. Use this when the user asks you to load the plugin into their test vault — it replaces manual build + copy + reload.
Code Quality
npm run lint- Run ESLint checksnpm run lint:fix- Auto-fix ESLint issuesnpm run format- Format code with Prettiernpm run format:check- Check formatting without changing files- Before PR: Always run
npm run format && npm run lint
Testing
npm run test- Run unit tests (excludes integration tests)npm run test:integration- Run integration tests (requires API keys)- Run single test:
npm test -- -t "test name"
Obsidian CLI (Live Testing)
The Obsidian desktop app includes a CLI for plugin development. Use the full path:
/Applications/Obsidian.app/Contents/MacOS/obsidian <command>
Plugin reload (after npm run build):
/Applications/Obsidian.app/Contents/MacOS/obsidian plugin:reload id=copilot
Console debugging (requires attaching debugger first):
/Applications/Obsidian.app/Contents/MacOS/obsidian dev:debug on
/Applications/Obsidian.app/Contents/MacOS/obsidian dev:console limit=30
/Applications/Obsidian.app/Contents/MacOS/obsidian dev:console level=error limit=10
/Applications/Obsidian.app/Contents/MacOS/obsidian dev:errors
Other useful dev commands:
dev:dom selector=<css>— Query DOM elementsdev:screenshot path=<file>— Take a screenshoteval code=<js>— Execute JS in the app contextplugin:disable id=copilot/plugin:enable id=copilot
Run obsidian help for the full command list.
High-Level Architecture
Core Systems
-
LLM Provider System (
src/LLMProviders/)- Provider implementations for OpenAI, Anthropic, Google, Azure, local models
LLMProviderManagerhandles provider lifecycle and switching- Stream-based responses with error handling and rate limiting
- Custom model configuration support
-
Chain Factory Pattern (
src/chainFactory.ts)- Different chain types for various AI operations (chat, copilot, adhoc prompts)
- LangChain integration for complex workflows
- Memory management for conversation context
- Tool integration (search, file operations, time queries)
-
Vector Store & Search (
src/search/)VectorStoreManagermanages embeddings and semantic searchChunkedStoragefor efficient large document handling- Event-driven index updates via
IndexManager - Multiple embedding providers support
-
UI Component System (
src/components/)- React functional components with Radix UI primitives
- Tailwind CSS with class variance authority (CVA)
- Modal system for user interactions
- Chat interface with streaming support
- Settings UI with versioned components
-
Message Management Architecture (
src/core/,src/state/)- MessageRepository (
src/core/MessageRepository.ts): Single source of truth for all messages- Stores each message once with both
displayTextandprocessedText - Provides computed views for UI display and LLM processing
- No complex dual-array synchronization
- Stores each message once with both
- ChatManager (
src/core/ChatManager.ts): Central business logic coordinator- Orchestrates MessageRepository, ContextManager, and LLM operations
- Handles message sending, editing, regeneration, and deletion
- Manages context processing and chain memory synchronization
- Project Chat Isolation: Maintains separate MessageRepository per project
- Automatically detects project switches via
getCurrentMessageRepo() - Each project has its own isolated message history
- Non-project chats use
defaultProjectKeyrepository
- Automatically detects project switches via
- ChatUIState (
src/state/ChatUIState.ts): Clean UI-only state manager- Delegates all business logic to ChatManager
- Provides React integration with subscription mechanism
- Replaces legacy SharedState with minimal, focused approach
- ContextManager (
src/core/ContextManager.ts): Handles context processing- Processes message context (notes, URLs, selected text)
- Reprocesses context when messages are edited
- MessageRepository (
-
Settings Management
- Jotai for atomic settings state management
- React contexts for feature-specific state
-
Plugin Integration
- Main entry:
src/main.tsextends Obsidian Plugin - Command registration system
- Event handling for Obsidian lifecycle
- Settings persistence and migration
- Chat history loading via pending message mechanism
- Main entry:
Key Patterns
- Single Source of Truth: MessageRepository stores each message once with computed views
- Clean Architecture: Repository → Manager → UIState → React Components
- Context Reprocessing: Automatic context updates when messages are edited
- Computed Views: Display messages for UI, LLM messages for AI processing
- Project Isolation: Each project maintains its own MessageRepository instance
- Error Handling: Custom error types with detailed interfaces
- Async Operations: Consistent async/await pattern with proper error boundaries
- Caching: Multi-layer caching for files, PDFs, and API responses
- Streaming: Real-time streaming for LLM responses
- Testing: Unit tests adjacent to implementation, integration tests for API calls
Message Management Architecture
For detailed architecture diagrams and documentation, see MESSAGE_ARCHITECTURE.md.
Core Classes and Flow
-
MessageRepository (
src/core/MessageRepository.ts)- Single source of truth for all messages
- Stores
StoredMessageobjects with bothdisplayTextandprocessedText - Provides computed views via
getDisplayMessages()andgetLLMMessages() - No complex dual-array synchronization or ID matching
-
ChatManager (
src/core/ChatManager.ts)- Central business logic coordinator
- Orchestrates MessageRepository, ContextManager, and LLM operations
- Handles all message CRUD operations with proper error handling
- Synchronizes with chain memory for conversation history
- Project Chat Isolation Implementation:
- Maintains
projectMessageRepos: Map<string, MessageRepository>for project-specific storage getCurrentMessageRepo()automatically detects current project and returns correct repository- Seamlessly switches between project repositories when project changes
- Creates new empty repository for each project (no message caching)
- Maintains
-
ChatUIState (
src/state/ChatUIState.ts)- Clean UI-only state manager
- Delegates all business logic to ChatManager
- Provides React integration with subscription mechanism
- Replaces legacy SharedState with minimal, focused approach
-
ContextManager (
src/core/ContextManager.ts)- Handles context processing (notes, URLs, selected text)
- Reprocesses context when messages are edited
- Ensures fresh context for LLM processing
-
ChatPersistenceManager (
src/core/ChatPersistenceManager.ts)- Handles saving and loading chat history to/from markdown files
- Project-aware file naming (prefixes with project ID)
- Parses and formats chat content for storage
- Integrated with ChatManager for seamless persistence
Code Style Guidelines
MAJOR PRINCIPLES
- ALWAYS WRITE GENERALIZABLE SOLUTIONS: Never add edge-case handling or hardcoded logic for specific scenarios (like "piano notes" or "daily notes"). Solutions must work for all cases.
- NEVER MODIFY AI PROMPT CONTENT: Do not update, edit, or change any AI prompts, system prompts, or model adapter prompts unless explicitly asked to do so by the user
- Avoid hardcoding: No hardcoded folder names, file patterns, or special-case logic
- Configuration over convention: If behavior needs to vary, make it configurable, not hardcoded
- Universal patterns: Solutions should work equally well for any folder structure, naming convention, or content type
TypeScript
- Strict mode enabled (no implicit any, strict null checks)
- Use absolute imports with
@/prefix:import { ChainType } from "@/chainFactory" - Prefer const assertions and type inference where appropriate
- Use interface for object shapes, type for unions/aliases
React
- Functional components only (no class components)
- Custom hooks for reusable logic
- Props interfaces defined above components
- Avoid inline styles, use Tailwind classes
General
- File naming: PascalCase for components, camelCase for utilities
- Async/await over promises
- Early returns for error conditions
- Always add JSDoc comments for all functions and methods
- Organize imports: React → external → internal
- Avoid language-specific lists (like stopwords or action verbs) - use language-agnostic approaches instead
Logging
- NEVER use console.log - Use the logging utilities instead:
logInfo()for informational messageslogWarn()for warningslogError()for errors
- Import from logger:
import { logInfo, logWarn, logError } from "@/logger"
CSS & Styling
- NEVER edit
styles.cssdirectly - This is a generated file - Source file:
src/styles/tailwind.css- Edit this file for custom CSS - Build process:
npm run build:tailwindcompilessrc/styles/tailwind.css→styles.css - Tailwind classes: Use Tailwind utility classes in components (see
tailwind.config.jsfor available classes) - Custom CSS: Add custom styles to
src/styles/tailwind.cssafter the@importstatements - After editing CSS, always run
npm run buildto regeneratestyles.css
Testing Guidelines
- Unit tests use Jest with TypeScript support
- Mock Obsidian API for plugin testing
- Integration tests require API keys in
.env.test - Test files adjacent to implementation (
.test.ts) - Use
@testing-library/reactfor component testing
Avoiding Deep Dependency Chains in Tests
This codebase has deep transitive import chains (e.g. a utility → cache → searchUtils → embeddingManager → brevilabsClient → plusUtils → Modal). Importing any module in this chain from a test requires mocking the entire tree, which is brittle and verbose.
Rules for new code:
- Pass data, not services — If a function only needs a string (like
outputFolder), accept it as a parameter. Don't give it access to the entire settings singleton. - Singletons at the edges only —
getSettings(),PDFCache.getInstance(),BrevilabsClient.getInstance()should only be called in top-level orchestration (constructors, main entry points). Inner functions receive what they need as parameters. - Pure logic in leaf modules — Extract testable logic into small files with minimal imports. The orchestration file (which has heavy imports) calls the leaf function and passes in the dependencies. See
src/tools/convertedDocOutput.tsas an example. - Litmus test before writing a function — "Can I test this by calling it directly with plain arguments?" If the answer is no because of an import, that dependency should be a parameter instead.
Development Session Planning
Using TODO.md for Session Management
IMPORTANT: When working on a development session, maintain a comprehensive TODO.md file that serves as the central plan and tracker:
- Session Goal: Define the high-level objective at the start
- Task Tracking:
- List all completed tasks with [x] checkboxes
- Track pending tasks with [ ] checkboxes
- Group related tasks into logical sections
- Architecture Decisions: Document key design choices and rationale
- Progress Updates: Keep the TODO.md updated as tasks complete
- Testing Checklist: Include verification steps for the session
The TODO.md should be:
- The single source of truth for session progress
- Updated frequently as work progresses
- Clear enough that another developer can understand what was done
- Comprehensive enough to serve as a migration guide
Structure Example:
# Development Session TODO
## Session Goal
[Clear statement of what this session aims to achieve]
## Completed Tasks ✅
- [x] Task description with key details
- [x] Another completed task
## Pending Tasks 📋
- [ ] Next task to work on
- [ ] Future enhancement
## Architecture Summary
[Key design decisions and rationale]
## Testing Checklist
- [ ] Functionality verification
- [ ] Performance checks
Important Notes
- The plugin supports multiple LLM providers with custom endpoints
- Vector store requires rebuilding when switching embedding providers
- Settings are versioned - migrations may be needed
- Local model support available via Ollama/LM Studio
- Rate limiting is implemented for all API calls
- For technical debt and known issues, see
TECHDEBT.md - For current development session planning, see
TODO.md
User-Facing Documentation
- When modifying user-facing behavior (new features, changed settings, removed functionality), update the corresponding doc in
docs/. The doc filenames match their topics (e.g.,llm-providers.mdfor provider changes,agent-mode-and-tools.mdfor tool changes). - Docs are written for non-technical users — no source code references, explain behavior and concepts.
- If a change affects multiple docs, update all of them.
- If you're unsure which doc to update, check
docs/index.mdfor the full list with descriptions.
AWS Bedrock Usage
IMPORTANT: When using AWS Bedrock, always use cross-region inference profile IDs for better reliability and availability:
- Global (recommended):
global.anthropic.claude-sonnet-4-5-20250929-v1:0- Routes to any commercial AWS region automatically
- Best for reliability and performance
- US:
us.anthropic.claude-sonnet-4-5-20250929-v1:0 - EU:
eu.anthropic.claude-sonnet-4-5-20250929-v1:0 - APAC:
apac.anthropic.claude-sonnet-4-5-20250929-v1:0
❌ Avoid regional model IDs (without prefix): anthropic.claude-sonnet-4-5-20250929-v1:0
- These only work in specific regions and often fail
- Not recommended for production use
References:
Obsidian Plugin Environment
- Global
appvariable: In Obsidian plugins,appis a globally available variable that provides access to the Obsidian API. It's automatically available in all files without needing to import or declare it.
Picking the right document / window (popout-window safety)
Obsidian supports pop-out windows. The plugin loads in the main window but views can live in any window. Picking the wrong Document / Window produces stale references, off-screen popovers, listeners on the wrong window, or DOM nodes that never render. Use this decision order:
element.doc/element.win— preferred. Obsidian augments everyNodewith.doc: Documentand.win: Windowthat always reflect the element's current owner. Use whenever you have any DOM node in scope (a ref, an event target, a component's container, aRange'sstartContainer).containerRef.current?.doc.addEventListener(...)range.startContainer.win.innerWidtheditor.getRootElement()?.doc
global activeDocument/activeWindow— fallback only. These point to whichever window is focused right now. Correct semantics for actions that follow user focus (e.g., the AddImageModal file picker; selectionchange registration at plugin load), but wrong when the action belongs to a specific view (a chat in a popout while the user clicks back to the main window).document/windowglobals — almost always wrong. They are aliases for the main window even when the user is interacting with a popout. Avoid in new code. If you find yourself reaching for them, it's a sign the surrounding code should be taking aDocument/Windowparameter or deriving from a DOM ref.element.ownerDocument— works (standard DOM), but prefer.docfor consistency with the codebase. They return the sameDocumentfor any mountedHTMLElement..docis shorter and typed non-nullable.
Listeners that may outlive a window migration: capture the Document / Window at registration and remove on the same one:
const doc = containerRef.current?.doc;
if (!doc) return;
doc.addEventListener("keydown", handler);
return () => doc.removeEventListener("keydown", handler);
Do not rely on activeDocument at registration and removal — it can shift between the two calls if focus moves.
View migrated to a new window: for a view that owns React or other long-lived renderers, register this.containerEl.onWindowMigrated((win) => { ... }) in onOpen. The callback fires when Obsidian reparents the element into a different window's document. Tear down and rebuild the renderer there so it captures the new window. Save the returned destroy function and call it in onClose to avoid leaks. CopilotView is the canonical example — it unmounts and recreates the React root on migration so Lexical re-binds to the popout's window.
Cross-realm instanceof: popout windows have their own Element, MouseEvent, etc., so standard instanceof checks fail across windows. Use Obsidian's element.instanceOf(HTMLElement) and event.instanceOf(MouseEvent) when checking type across realms.
Tests (jsdom): jest.setup.js polyfills Node.doc / Node.win so plugin code using these properties works under jsdom. Don't add instanceof guards that depend on the Obsidian-augmented globals without considering the test environment.
Architecture Migration Notes
- SharedState Removed: The legacy
src/sharedState.tshas been completely removed - Clean Architecture: New architecture follows Repository → Manager → UIState → UI pattern
- Single Source of Truth: All messages stored once in MessageRepository with computed views
- Context Always Fresh: Context is reprocessed when messages are edited to ensure accuracy
- Chat History Loading: Uses pending message mechanism through CopilotView → Chat component props
- Project Chat Isolation: Each project now has completely isolated chat history
- Automatic detection of project switches via
ProjectManager.getCurrentProjectId() - Separate MessageRepository instances per project ID
- Non-project chats stored in default repository
- Backwards compatible - loads existing messages from ProjectManager cache
- Zero configuration required - works automatically
- Automatic detection of project switches via
- Check @tailwind.config.js to understand what tailwind css classnames are available