# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Type This is an Obsidian community plugin written in TypeScript and bundled to JavaScript using esbuild. ## Build Commands ```bash # Install dependencies npm install # Development mode (watch mode, auto-recompiles on changes) npm run dev # Production build (type checks, then bundles with minification) npm run build # Lint code npm run lint ``` ## Architecture ### Entry Points & Build Process - Source code lives in `src/` - Main entry point: `src/main.ts` (plugin lifecycle management) - Build target: `main.js` (bundled output at root) - Bundler: **esbuild** (configured in `esbuild.config.mjs`) - Required release artifacts: `main.js`, `manifest.json`, `styles.css` (if present) ### Code Organization Pattern - **Keep `main.ts` minimal**: Only plugin lifecycle (onload, onunload, command registration) - Delegate feature logic to separate modules - Settings are defined in `src/settings.ts` - Organize larger features into subdirectories: - `commands/` for command implementations - `ui/` for modals, views, and UI components - `utils/` for helper functions ### Plugin Architecture - Extends `Plugin` class from `obsidian` package - Settings are loaded/saved using `this.loadData()` / `this.saveData()` - All event listeners, intervals, and DOM events must use `this.register*()` helpers for proper cleanup - Commands are registered via `this.addCommand()` with stable IDs ## Key Constraints ### Obsidian-Specific - **Bundle everything**: No external runtime dependencies (except `obsidian`, `electron`, CodeMirror packages) - **External packages**: Listed in `esbuild.config.mjs` external array; never bundle these - **Mobile compatibility**: Avoid Node/Electron APIs unless `isDesktopOnly: true` in manifest - **Network requests**: Default to offline; require explicit user consent for any external calls - **No remote code execution**: Never fetch/eval scripts or auto-update outside normal releases ### Manifest (`manifest.json`) - Never change `id` after initial release - Use semantic versioning for `version` - Keep `minAppVersion` accurate when using newer Obsidian APIs - Update both `manifest.json` and `versions.json` when bumping versions ## Development Workflow ### Testing Locally 1. Build the plugin: `npm run dev` or `npm run build` 2. Copy `main.js`, `manifest.json`, `styles.css` to: ``` /.obsidian/plugins// ``` 3. Reload Obsidian and enable plugin in Settings → Community plugins ### Type Checking - TypeScript strict mode enabled in `tsconfig.json` - Build command runs `tsc -noEmit -skipLibCheck` before bundling - Fix type errors before considering build successful ## Important Files - `src/main.ts` - Plugin class and lifecycle - `src/settings.ts` - Settings interface, defaults, and settings tab UI - `manifest.json` - Plugin metadata (never commit with wrong version) - `esbuild.config.mjs` - Build configuration - `eslint.config.mts` - ESLint configuration with Obsidian-specific rules - `styles.css` - Optional plugin styles ## Code Quality Guidelines From AGENTS.md and Obsidian best practices: - Split files when they exceed ~200-300 lines - Use clear module boundaries (single responsibility per file) - Prefer `async/await` over promise chains - Register all cleanup via `this.register*()` helpers to prevent memory leaks - Keep startup lightweight; defer heavy work until needed - Use stable command IDs (don't rename after release) - Provide sensible defaults for all settings ## Sidebar View Architecture ### Key Files - `src/views/todo-sidebar-view.ts` — Main sidebar panel (`TodoSidebarView` extends `ItemView`) - `src/views/todo-item-component.ts` — Pure DOM factory for individual todo `
  • ` elements - `src/core/todo-parser.ts` — Parses markdown into `TodoGroup[]` with nested `TodoItem` trees - `src/core/todo-transformer.ts` — Pure functions for modifying file content (toggle, move, indent) - `src/modals/note-select-modal.ts` — First step of cross-file move: pick target note - `src/modals/heading-select-modal.ts` — Second step: pick heading within target note ### Data Flow 1. `parseTodosGroupedByHeading(content)` → `TodoGroup[]` (each group has a heading + tree of todos) 2. `buildTodoTree(flatTodos)` — stack-based algorithm: todos whose indent > parent's become children 3. `renderGroups()` renders each group; `createTodoItemEl()` renders items recursively 4. `flatTodoList: FlatTodoEntry[]` is rebuilt each render for keyboard navigation ### Event Listener Lifecycle (Critical) - **Register keydown listener ONCE in `onOpen()`**, not in `renderGroups()` — otherwise listeners accumulate on every file change and arrow keys skip items - Use `this.currentFile` inside handlers instead of closure variables (file changes between renders) - `registerEvent()` wrappers handle cleanup automatically on view close ### Keyboard Navigation - `focusedIndex = -1` means panel is not yet activated (no visual focus) - First ArrowDown/ArrowUp when `focusedIndex === -1` sets it to 0 (activates focus like Outline view) - `flatTodoList` flattens the entire tree depth-first; arrow keys walk this list linearly - Container has `tabindex="0"` so it can receive keyboard events without stealing focus on render ### Drag and Drop - Parent `
  • ` and all nested `
  • ` are `draggable="true"`; `stopPropagation` on `dragstart` prevents child drag from bubbling to parent - `handleDragStart` stores `draggedTodo` and `draggedChildLines` (via `collectChildLineNumbers`) - Item drop zones use 3 zones (top/middle/bottom thirds): - Top → insert above - Middle → nest as child (increase indent, insert after target's last descendant) - Bottom → insert below - Group (heading) drop zones allow dropping onto the heading area to move to end of that section - `performNest` adjusts indentation with `indentTodoLines(lines, delta)` before inserting ### Hotkey Matching (Platform-Aware) - Uses `app.hotkeyManager.getHotkeys(commandId)` and `getDefaultHotkeys(commandId)` (internal API) - `Platform.isMacOS` distinguishes Mod (Cmd on Mac, Ctrl on Windows) from Ctrl - See `matchesHotkey()` in `todo-sidebar-view.ts` for the full matching logic ### Cross-File Move - Always pass `addBlankLine: true` to `insertTodoUnderHeading` from modals — inserts a blank line between the heading and the moved todo for readability - In-file drag moves use `moveTodoWithChildren` directly (no blank line needed)