mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* fix(popout): document ownership and selection listener fixes (W4/9) Fifth of nine workspaces splitting #2397. Real popout-window correctness fixes — semantic choices about which window owns a DOM operation, plus selection-listener captured-document fixes. - document → activeDocument where a generic active-window lookup is correct (SourcesModal, ChatViewLayout, ItemView/Notice mocks, TabContext, ChatInput keydown listener, CopilotView keyboard observer, TypeaheadMenuPortal, AddImageModal, and ?? document fallbacks across draggable-modal, menu-command-modal, CustomCommandChatModal, use-draggable, use-resizable, and QuickAskOverlay). - document → root.ownerDocument in ChatSingleMessage's linkInlineCitations so DOM nodes end up in the citation's window; same pattern in the processMessage streaming block via contentRef.current.ownerDocument. - New getEditorDocument(editor) helper in BasePillNode. createDOM and exportDOM now accept the LexicalEditor; pills create DOM in the editor's window (correct popout behavior). All 6 pill subclasses updated to match the new signature. - main.ts: selectionListenerDocument captured at listener registration; remove uses the same document (fixes leak when activeDocument has drifted before unload). - chatSelectionHighlightController.ts: getActiveViewOfType(MarkdownView) replaces the brittle activeLeaf type comparison. - Test setup: (window as any).activeDocument = window.document in ChatSingleMessage.test; window.document.createElement in AgentPrompt.test Notice mock. Behavior fix: chat in a popout window now creates DOM nodes in the popout's document, not the focused window's document. W0, W1, and #2402 already merged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(popout): prefer element.doc/.win over ownerDocument and activeDocument Switch popout-window-sensitive call sites to Obsidian's `.doc` / `.win` augmentations so DOM ops, listeners, portals, and positioning follow the element's actual owner window. Recreate Lexical root on view migration to rebind input handling after a leaf moves between windows. Document the decision order in AGENTS.md and polyfill `Node.doc` / `Node.win` in jest.setup.js so tests still run under jsdom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * improve image upload implementation --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
390 lines
19 KiB
Markdown
390 lines
19 KiB
Markdown
# 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)
|
|
|
|
### Code Quality
|
|
|
|
- `npm run lint` - Run ESLint checks
|
|
- `npm run lint:fix` - Auto-fix ESLint issues
|
|
- `npm run format` - Format code with Prettier
|
|
- `npm 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:
|
|
|
|
```bash
|
|
/Applications/Obsidian.app/Contents/MacOS/obsidian <command>
|
|
```
|
|
|
|
**Plugin reload** (after `npm run build`):
|
|
|
|
```bash
|
|
/Applications/Obsidian.app/Contents/MacOS/obsidian plugin:reload id=copilot
|
|
```
|
|
|
|
**Console debugging** (requires attaching debugger first):
|
|
|
|
```bash
|
|
/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 elements
|
|
- `dev:screenshot path=<file>` — Take a screenshot
|
|
- `eval code=<js>` — Execute JS in the app context
|
|
- `plugin:disable id=copilot` / `plugin:enable id=copilot`
|
|
|
|
Run `obsidian help` for the full command list.
|
|
|
|
## High-Level Architecture
|
|
|
|
### Core Systems
|
|
|
|
1. **LLM Provider System** (`src/LLMProviders/`)
|
|
|
|
- Provider implementations for OpenAI, Anthropic, Google, Azure, local models
|
|
- `LLMProviderManager` handles provider lifecycle and switching
|
|
- Stream-based responses with error handling and rate limiting
|
|
- Custom model configuration support
|
|
|
|
2. **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)
|
|
|
|
3. **Vector Store & Search** (`src/search/`)
|
|
|
|
- `VectorStoreManager` manages embeddings and semantic search
|
|
- `ChunkedStorage` for efficient large document handling
|
|
- Event-driven index updates via `IndexManager`
|
|
- Multiple embedding providers support
|
|
|
|
4. **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
|
|
|
|
5. **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 `displayText` and `processedText`
|
|
- Provides computed views for UI display and LLM processing
|
|
- No complex dual-array synchronization
|
|
- **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 `defaultProjectKey` repository
|
|
- **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
|
|
|
|
6. **Settings Management**
|
|
|
|
- Jotai for atomic settings state management
|
|
- React contexts for feature-specific state
|
|
|
|
7. **Plugin Integration**
|
|
- Main entry: `src/main.ts` extends Obsidian Plugin
|
|
- Command registration system
|
|
- Event handling for Obsidian lifecycle
|
|
- Settings persistence and migration
|
|
- Chat history loading via pending message mechanism
|
|
|
|
### 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`](./designdocs/MESSAGE_ARCHITECTURE.md).
|
|
|
|
### Core Classes and Flow
|
|
|
|
1. **MessageRepository** (`src/core/MessageRepository.ts`)
|
|
|
|
- Single source of truth for all messages
|
|
- Stores `StoredMessage` objects with both `displayText` and `processedText`
|
|
- Provides computed views via `getDisplayMessages()` and `getLLMMessages()`
|
|
- No complex dual-array synchronization or ID matching
|
|
|
|
2. **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)
|
|
|
|
3. **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
|
|
|
|
4. **ContextManager** (`src/core/ContextManager.ts`)
|
|
|
|
- Handles context processing (notes, URLs, selected text)
|
|
- Reprocesses context when messages are edited
|
|
- Ensures fresh context for LLM processing
|
|
|
|
5. **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 messages
|
|
- `logWarn()` for warnings
|
|
- `logError()` for errors
|
|
- Import from logger: `import { logInfo, logWarn, logError } from "@/logger"`
|
|
|
|
### CSS & Styling
|
|
|
|
- **NEVER edit `styles.css` directly** - This is a generated file
|
|
- **Source file**: `src/styles/tailwind.css` - Edit this file for custom CSS
|
|
- **Build process**: `npm run build:tailwind` compiles `src/styles/tailwind.css` → `styles.css`
|
|
- **Tailwind classes**: Use Tailwind utility classes in components (see `tailwind.config.js` for available classes)
|
|
- **Custom CSS**: Add custom styles to `src/styles/tailwind.css` after the `@import` statements
|
|
- After editing CSS, always run `npm run build` to regenerate `styles.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/react` for 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:**
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **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.ts` as an example.
|
|
4. **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:
|
|
|
|
1. **Session Goal**: Define the high-level objective at the start
|
|
2. **Task Tracking**:
|
|
- List all completed tasks with [x] checkboxes
|
|
- Track pending tasks with [ ] checkboxes
|
|
- Group related tasks into logical sections
|
|
3. **Architecture Decisions**: Document key design choices and rationale
|
|
4. **Progress Updates**: Keep the TODO.md updated as tasks complete
|
|
5. **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:
|
|
|
|
```markdown
|
|
# 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`](./designdocs/todo/TECHDEBT.md)
|
|
- For current development session planning, see [`TODO.md`](./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.md` for provider changes, `agent-mode-and-tools.md` for 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.md` for 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:**
|
|
|
|
- [AWS Bedrock Cross-Region Inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html)
|
|
- [Supported Inference Profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html)
|
|
|
|
### Obsidian Plugin Environment
|
|
|
|
- **Global `app` variable**: In Obsidian plugins, `app` is 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:
|
|
|
|
1. **`element.doc` / `element.win`** — preferred. Obsidian augments every `Node` with `.doc: Document` and `.win: Window` that 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, a `Range`'s `startContainer`).
|
|
- `containerRef.current?.doc.addEventListener(...)`
|
|
- `range.startContainer.win.innerWidth`
|
|
- `editor.getRootElement()?.doc`
|
|
2. **`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).
|
|
3. **`document` / `window` globals** — 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 a `Document`/`Window` parameter or deriving from a DOM ref.
|
|
4. **`element.ownerDocument`** — works (standard DOM), but prefer `.doc` for consistency with the codebase. They return the same `Document` for any mounted `HTMLElement`. `.doc` is shorter and typed non-nullable.
|
|
|
|
**Listeners that may outlive a window migration:** capture the `Document` / `Window` at registration and remove on the same one:
|
|
|
|
```ts
|
|
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.ts` has 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
|
|
- Check @tailwind.config.js to understand what tailwind css classnames are available
|