logancyang_obsidian-copilot/AGENTS.md
Zero Liu c67c4fc200
fix(popout): document ownership and selection listener fixes (W4/9) (#2406)
* 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>
2026-05-12 21:28:04 -07:00

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)

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:

/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 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.

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.cssstyles.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 onlygetSettings(), 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:

# 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.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:

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:

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