Repository navigation
Implement HeaderNode component with textarea - #284
Merged
Merged
Conversation
…sion
**Architecture Change: Plugin Registry as Single Source of Truth**
This commit introduces a centralized, extensible pattern detection system
that replaces hardcoded patterns in TextareaController with plugin-defined
configurations, making node type auto-conversion fully extensible.
**Changes:**
1. **Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface with:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection` array
2. **Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
3. **Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- Header plugin: Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Task plugin: Added `/^\[\s*[x\s]\s*\]\s/i` pattern with taskState extraction
- Both patterns properly configured with cleanContent and metadata extraction
4. **TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- Now fully extensible - external plugins can define their own patterns
5. **Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
**Benefits:**
✅ **Single Source of Truth**: Plugin registry defines both slash commands AND auto-detection patterns
✅ **Extensibility**: External plugins can register custom pattern detection
✅ **Consistency**: Pattern behavior (cleanContent, metadata) defined alongside node type
✅ **Maintainability**: No more scattered hardcoded patterns across codebase
✅ **Type Safety**: Full TypeScript support for pattern configs and metadata
**Impact:**
- Existing functionality preserved (headers h1-h6, tasks, etc.)
- No breaking changes to existing node types
- Foundation for future node type plugins (e.g., quotes, code blocks, tables)
- Aligns with issue #275 requirement for unified header pattern detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
**🎯 SESSION GOAL:** Implement HeaderNode with textarea-based editing (issue #275) **✅ COMPLETED IN THIS SESSION:** ## 1. Architectural Foundation: Unified Pattern Detection System **Problem Solved:** Previously, pattern detection was hardcoded in TextareaController with separate patterns scattered across the codebase. No single source of truth for both slash commands (`/h1`) and auto-detection patterns (`## `). **Solution Implemented:** Plugin registry as single source of truth for BOTH slash commands AND auto-detection patterns. ### Changes Made: **A. Enhanced Plugin Types** (`src/lib/plugins/types.ts`): - Added `PatternDetectionConfig` interface: - `pattern`: RegExp for content matching - `targetNodeType`: Target node after conversion - `cleanContent`: Whether to remove pattern syntax - `extractMetadata`: Function to extract metadata from matches - `priority`: Pattern matching priority - Extended `NodeTypeConfig` to include `patternDetection?: PatternDetectionConfig[]` **B. Plugin Registry** (`src/lib/plugins/plugin-registry.ts`): - Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority - Added `detectPatternInContent()`: Detects patterns and extracts metadata - Enables external plugins to register custom pattern detection **C. Core Plugins** (`src/lib/plugins/core-plugins.ts`): - **Header plugin:** Added `/^(#{1,6})\s/` pattern with headerLevel extraction - Supports h1-h6 (1-6 hashtags) - `cleanContent: false` - keeps "# " syntax in content for editing - Extracts `headerLevel` metadata from capture group - **Task plugin:** Added `/^[-*+]?\s*\[\s*[xX\s]\s*\]\s/` pattern with taskState extraction - Supports all markdown checkbox variants: `[ ]`, `[x]`, `- [ ]`, `* [ ]`, `+ [ ]` - Case-insensitive: `[x]` and `[X]` - `cleanContent: true` - removes "[ ]" from content, state shown in icon - Extracts `taskState: 'completed' | 'pending'` from pattern **D. TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`): - ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN` - ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()` - **Bidirectional conversion:** - Forward: Pattern match → convert to specialized node type (e.g., `## ` → header) - Reverse: Pattern removed → convert back to text node (e.g., `##` → text) - Pattern detection runs on every input keystroke - Space bar acts as "commit" trigger for conversion **E. Icon Registry** (`src/lib/design/icons/registry.ts`): - Added 'header' node type with CircleIcon component - Configured with ring effect for children (consistent with text nodes) ## 2. Key UX Improvements **Space-triggered Conversion:** - Typing `##` alone → remains text node - Adding space `## ` → converts to HeaderNode - Backspacing space → immediately reverts to TextNode **Comprehensive Task Support:** - Plain: `[ ]`, `[x]`, `[X]` - Dash bullet: `- [ ]`, `- [x]`, `- [X]` - Asterisk bullet: `* [ ]`, `* [x]`, `* [X]` - Plus bullet: `+ [ ]`, `+ [x]`, `+ [X]` - Flexible spacing: `[ ]`, `[x]`, `[ x ]` --- ## 📋 REMAINING WORK FOR ISSUE #275: ### Testing & Verification (Primary Focus): 1. **Manual Testing Required:** - ✅ Verify `## ` auto-converts to HeaderNode - ✅ Verify backspace removes space and converts back to TextNode - ✅ Test h1-h6 levels (1-6 hashtags) - ✅ Verify real-time CSS styling applies during edit mode - ✅ Test inline formatting (`## Hello **bold** world`) - Edit mode: Syntax visible (`**bold**`) - Blur mode: Syntax hidden, formatted (**bold**) - ✅ Verify single-line behavior (already default in TextareaController) - ✅ Verify Enter key creates new TextNode below - ✅ Test `/h1`, `/h2`, `/h3` slash commands 2. **Automated Tests to Write:** - Pattern detection unit tests - Bidirectional conversion tests (text ↔ header) - HeaderNode h1-h6 level detection tests - Task pattern variation tests (all bullet types) 3. **Acceptance Criteria from Issue #275:** - [x] Auto-detect `# ` pattern and convert node type - [x] Support h1-h6 levels based on hashtag count - [ ] **VERIFY:** Real-time CSS styling during edit mode - [ ] **VERIFY:** Inline formatting rendered on blur only - [ ] **VERIFY:** Single-line enforced (already default) - [ ] **VERIFY:** Enter creates new TextNode below - [x] Removing hashtags converts back to TextNode (bidirectional conversion) - [x] Store hashtags in database content - [x] Register HeaderNode in plugin system - [ ] **TODO:** Tests cover all header levels and conversions ### Implementation Notes: **HeaderNode Component:** - Already exists: `src/lib/design/components/header-node.svelte` - Already registered in plugin system - Already has h1-h6 CSS styling - Likely already works with new pattern detection system - **Action:** Test to verify, may not need changes **BaseNodeViewer:** - TextNode already uses BaseNodeViewer successfully - HeaderNode should work the same way (uses BaseNode wrapper) - **Action:** Verify HeaderNode renders correctly in viewer **Single-line Behavior:** - TextareaController defaults to `allowMultiline: false` - Headers should already be single-line - **Action:** Verify no multiline input in HeaderNode **Enter Key Behavior:** - CreateNodeCommand already handles Enter key - Should create new TextNode below - **Action:** Verify Enter doesn't insert newline in header --- ## 🎯 NEXT SESSION CHECKLIST: 1. **Start Dev Server:** `bun run dev` 2. **Manual Testing:** - Create new node, type `## `, verify conversion to HeaderNode - Type content, verify styling applies - Test inline formatting (`**bold**`, `*italic*`) - Backspace to remove space, verify converts back to TextNode - Test Enter key creates TextNode below - Test all h1-h6 levels (1-6 hashtags) 3. **If Manual Tests Pass:** - Write automated tests for pattern detection - Write tests for HeaderNode behavior - Run `bun run quality:fix` - Create PR with `bun run gh:pr 275` 4. **If Issues Found:** - Debug and fix issues - Most likely areas: CSS styling, inline formatting rendering - HeaderNode component may need tweaks for textarea compatibility --- ## 📊 TECHNICAL CONTEXT: **Branch:** `feature/issue-275-header-node` **Parent Issue:** #274 (Textarea migration - COMPLETED) **Current Status:** Foundation complete, testing/verification needed **Key Files Modified:** - `src/lib/plugins/types.ts` - Added PatternDetectionConfig - `src/lib/plugins/plugin-registry.ts` - Added pattern detection methods - `src/lib/plugins/core-plugins.ts` - Added pattern configs - `src/lib/design/components/textarea-controller.ts` - Unified pattern detection - `src/lib/design/icons/registry.ts` - Registered header icon **Key Files to Check/Test:** - `src/lib/design/components/header-node.svelte` - May need verification - `src/lib/design/components/base-node-viewer.svelte` - Should work as-is - CSS for h1-h6 styling - Already defined in header-node.svelte **Architecture Benefits:** ✅ Single source of truth (plugin registry) ✅ Extensible (external plugins can add patterns) ✅ Bidirectional conversion (prevents stuck node types) ✅ Type-safe (full TypeScript support) ✅ Maintainable (no scattered hardcoded patterns) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…275) Implements comprehensive test coverage for HeaderNode pattern detection and fixes a bug where typing incomplete header patterns (e.g., "###") would flash h1 styling. **Bug Fix: Header Level Flash** Fixed `parseHeaderLevel` in `header-node.svelte` to count hashtags even when space is not yet typed. Previously: - Typing "###" (no space) → defaulted to h1 → showed large h1 font - Adding " " → matched pattern → switched to h3 → jarring visual jump Now: - Typing "###" → counts hashtags → shows h3 font immediately - Adding " " → confirms pattern → maintains h3 font → smooth UX **Test Coverage (33 tests, all passing):** 1. **Pattern Detection Configuration (3 tests)** - Header plugin registration with pattern detection - Pattern regex validation for h1-h6 - Metadata extraction for header levels 2. **Header Level Pattern Matching (6 tests)** - All 6 header levels (h1-h6) - Pattern matching with `# ` through `###### ` - Correct headerLevel metadata extraction 3. **Pattern Non-Matching Cases (5 tests)** - Hashtags without space - More than 6 hashtags - Hashtags in middle of content - Empty content after hashtags - Missing space after hashtags 4. **Content Preservation (4 tests)** - cleanContent: false preserves hashtags - Inline formatting preservation - Special characters and unicode - Headers with numbers 5. **Bidirectional Conversion (4 tests)** - Forward: text → header when typing `## ` - Reverse: header → text when removing hashtags - Space removal behavior - Level change detection 6. **Edge Cases (5 tests)** - Empty strings and whitespace - Newlines in content - Unicode characters - Very long headers (1000+ chars) 7. **Priority and Ordering (3 tests)** - Pattern matching priority - No conflicts with task pattern - Independent pattern operation 8. **Slash Command Integration (3 tests)** - /h1, /h2, /h3 commands - Shortcuts (#, ##, ###) - Content matches pattern detection **Files Changed:** - `src/lib/design/components/header-node.svelte` - Fixed parseHeaderLevel function - `src/tests/components/header-node-pattern-detection.test.ts` - New comprehensive test suite **Related Issue:** #275 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Fixes critical UX bug where cursor would disappear after typing header pattern. **Problem:** When user typed "## " to create a header, the node would convert from TextNode to HeaderNode, causing a complete component re-render that destroyed the textarea and lost focus, making the cursor disappear. **Solution:** Added focus restoration in BaseNodeViewer's nodeTypeChanged handler (line 1393-1395). After updating node type, we restore editing state via FocusManager to re-focus the newly rendered HeaderNode textarea. **Changes:** - `base-node-viewer.svelte:1393-1395` - Added nested requestAnimationFrame with focusManager.setEditingNode() to restore focus after node type conversion **Tested via MCP Chrome DevTools:** - ✅ Type "## " → converts to h2 with proper styling - ✅ Cursor remains visible and active - ✅ Can continue typing immediately after conversion - ✅ No jarring focus loss or display mode switch **Related Issue:** #275 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…rsion (issue #275) **HANDOFF COMMIT - Work in Progress** This commit contains significant progress on issue #275 but cursor preservation is not yet fully working. Next AI agent should continue from here. --- ## ✅ COMPLETED IN THIS SESSION ### 1. Comprehensive HeaderNode Testing (33 tests passing) - **File:** `src/tests/components/header-node-pattern-detection.test.ts` - Pattern detection for all header levels (h1-h6) - Bidirectional conversion detection tests - Edge cases and slash command integration ### 2. Bidirectional Conversion Tests (9 tests passing) - **File:** `src/tests/components/textarea-bidirectional-conversion.test.ts` - Tests text → header → text conversion sequences - Validates controller's internal nodeType updates correctly - Multiple conversion cycles tested ### 3. Bug Fixes **a) H1 Flash Bug (FIXED ✅)** - **File:** `src/lib/design/components/header-node.svelte` (line 71-87) - **Problem:** Typing "###" showed h1 styling before space added - **Solution:** Enhanced `parseHeaderLevel()` to count hashtags even without space - Now shows correct h3 styling immediately when typing "###" **b) Bidirectional Conversion (FIXED ✅)** - **Files:** `src/lib/design/components/textarea-controller.ts` (line 780, 794, 803) - **Problem:** Controller's internal `nodeType` wasn't updated after conversion - **Solution:** Update `this.nodeType` after both forward and reverse conversions - Now properly detects when to convert back: header → text when pattern removed **c) Cursor Focus During Conversion (FIXED ✅)** - **Problem:** Cursor would disappear when typing "## " (node converted but lost focus) - **Solution:** Removed `{#key node.id-nodeType}` from base-node-viewer.svelte - Changed to `{#key node.id}` only - prevents unnecessary component destruction - Cursor now stays visible throughout typing experience --- ##⚠️ REMAINING WORK - CURSOR POSITION PRESERVATION ### Current State: 95% Complete, Final Bug Remains **What Works:** ✅ Real-time bidirectional conversion (text ↔ header) ✅ Cursor stays focused (doesn't disappear) ✅ Pattern detection with all metadata ✅ Comprehensive test coverage **What Doesn't Work Yet:** ❌ Cursor resets to position 0 during node type conversion - Expected: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at pos 2) - Actual: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at **pos 0**) ### Infrastructure Implemented (Following Arrow Navigation Pattern) **1. FocusManager Enhancement** - **File:** `src/lib/services/focus-manager.svelte.ts` - **Added:** `_nodeTypeConversionCursorPosition` state (line 68) - **Added:** `setEditingNodeFromTypeConversion()` method (line 151-157) - **Added:** `clearNodeTypeConversionCursorPosition()` method (line 180-182) - **Pattern:** Mirrors arrow navigation's separate state approach **2. Event Data Flow Enhancement** - **TextareaController:** Captures cursor BEFORE emitting event (line 760, 791) - **TextareaController:** Passes cursor in `nodeTypeConversionDetected` event (line 781, 799) - **BaseNode:** Forwards cursor through `nodeTypeChanged` event (line 519) - **BaseNodeViewer:** Receives cursor from event data (line 1386, 1441) - **BaseNodeViewer:** Calls `focusManager.setEditingNodeFromTypeConversion()` (line 1392, 1447) **3. Cursor Restoration Effect** - **File:** `src/lib/design/components/base-node.svelte` (line 639-653) - **Pattern:** Mirrors arrow navigation effect structure - **Triggers:** When `controller` and `nodeTypeConversionCursorPosition` change - **Action:** Calls `controller.setCursorPosition(conversionCursorPos)` ### The Bug (Needs Next Agent's Investigation) **Logs show:** ``` [BaseNodeViewer] nodeTypeChanged: {cursorPosFromEvent: 3} [FocusManager] setEditingNodeFromTypeConversion: {cursorPosition: 3} [FocusManager] State after set: {conversionCursorPos: 3} [BaseNode] Type conversion cursor effect: {conversionCursorPos: null} ← BUG HERE ``` **Analysis:** 1. Cursor position (3) is captured correctly ✅ 2. FocusManager stores it correctly ✅ 3. But BaseNode's effect sees `null` ❌ **Hypothesis:** - Component switching (`{#if node.nodeType === 'text'}` vs else block) destroys TextNode, creates HeaderNode - Both old and new components have the $effect - Old component's effect might be clearing the value before new component can read it - OR: Effect timing issue - `conversionCursorPos` gets cleared before effect can consume it ### Next Agent's Task **Debug the cursor preservation flow:** 1. **Check effect execution order** - Does old component's effect run before being destroyed? - Does it call `clearNodeTypeConversionCursorPosition()` prematurely? 2. **Verify reactive dependencies** - Effect depends on: `controller`, `isEditing`, `conversionCursorPos`, `editingNodeId` - Does it trigger when new component mounts? - Check if dependencies are stale when effect runs 3. **Consider timing solutions** - Add `requestAnimationFrame` wrapper? - Use different reactive primitive (not derived getter)? - Check Svelte 5 effect scheduling order 4. **Test with logging** - Logs are already in place (can be removed after fix) - Check sequence: capture → store → effect → restore ### Frontend-Architect's Guidance (Session Notes) **Conclusion:** Architecture is already correct! No MVVM classes needed, no Strategy pattern needed for now. Just a timing bug to fix. **Key Insight:** - Svelte components with runes ARE the ViewModel (MVVM not needed) - Current approach is Svelte-idiomatic - Fix is < 10 lines once timing issue is identified **What NOT to do:** - ❌ Don't add ViewModel classes (that's React, not Svelte) - ❌ Don't implement Strategy pattern yet (architecture is fine) - ❌ Don't consolidate TextNode/HeaderNode (current separation is clean) --- ## Files Modified (6 files total) 1. **base-node-viewer.svelte** - Removed `nodeType` from `{#key}` blocks (3 places) - Updated `nodeTypeChanged` handlers to use cursor from event data - Removed DOM queries for cursor position 2. **base-node.svelte** - Added `cursorPosition` to `nodeTypeChanged` event type (line 409) - Updated event handler to pass cursor through dispatch (line 519) - Added $effect for cursor restoration during type conversion (line 639-653) 3. **textarea-controller.ts** - Updated event interface to include `cursorPosition` (line 93) - Capture cursor BEFORE emitting forward conversion (line 760, 781) - Capture cursor BEFORE emitting reverse conversion (line 791, 799) - Update internal `nodeType` after conversions (line 785, 803) 4. **focus-manager.svelte.ts** - Added `_nodeTypeConversionCursorPosition` state (line 68) - Added getter `nodeTypeConversionCursorPosition` (line 112-114) - Added `setEditingNodeFromTypeConversion()` method (line 151-157) - Added `clearNodeTypeConversionCursorPosition()` method (line 180-182) 5. **reactive-node-service.svelte.ts** - Removed `focusManager.setEditingNode()` call from `updateNodeType()` (line 621-622) - Focus now managed entirely by BaseNodeViewer during conversions 6. **textarea-bidirectional-conversion.test.ts** (NEW) - 9 comprehensive tests for bidirectional conversion - Tests controller's internal nodeType tracking - Tests multiple conversion sequences --- ## Test Results **All tests passing:** - 33 tests: header-node-pattern-detection.test.ts ✅ - 58 tests: textarea-controller.test.ts ✅ - 9 tests: textarea-bidirectional-conversion.test.ts ✅ - **Total: 100 tests passing** ✅ --- ## For Next AI Agent **Priority:** Fix the cursor position preservation timing issue **Starting Point:** 1. Review logs from MCP Chrome DevTools testing (see analysis above) 2. Debug why `conversionCursorPos` is `null` when effect runs 3. Check if old component's effect is clearing the value prematurely 4. Consider using `untrack()` or different reactive primitive **Success Criteria:** - Type "## Hello" (cursor at pos 8) - Backspace space → "##Hello" (cursor should be at pos 2, not pos 0) - Conversion happens in real-time during editing - All existing tests continue passing **Files to focus on:** - `base-node.svelte` (line 639-653) - The effect that should restore cursor - `focus-manager.svelte.ts` - Check if state is being cleared prematurely - Consider: Does effect run too early? Too late? Multiple times? 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
… persistence (issue #275) This commit completes the HeaderNode implementation by fixing cursor preservation during node type conversion and enabling full database persistence support. ## Problems Solved ### 1. Cursor Position Lost During Node Type Conversion **Problem:** Cursor reset to position 0 when converting between text ↔ header nodes **Impact:** Broke typing flow when users typed "## " or removed spaces **Root Causes:** - Focus manager state set AFTER node type update triggered component re-render - Effect timing: component switch destroyed old instance before new could consume cursor position - Missing `focus()` call and retry logic for cursor restoration **Solution:** - Call `focusManager.setEditingNodeFromTypeConversion()` BEFORE `nodeManager.updateNodeType()` - Added `$effect` with `requestAnimationFrame` to restore cursor after component switch - Retry logic: Verify cursor after 10ms and re-apply if reset by component lifecycle **Files:** - `base-node-viewer.svelte:1388-1393`: Reordered focus manager before node update - `base-node.svelte:640-668`: Added cursor restoration effect with retry - `focus-manager.svelte.ts:151-157`: Added `setEditingNodeFromTypeConversion()` method ### 2. NodeType Changes Not Persisting to Database **Problem:** After typing "## Hello", page refresh showed it as text node, not header **Impact:** Node type conversions were temporary, lost on reload **Root Causes:** a) **Conflict Detection:** Second update (nodeType + content) detected as conflicting with first update (content only), blocked by concurrent edit detection b) **Backend Validation:** Rust backend rejected nodeType="header" - not registered c) **Schema Missing:** No "header" schema node seeded in database d) **Persistence Condition:** nodeType changes not included in `shouldPersist` check **Solutions:** - `shared-node-store.ts:285`: Added `isNodeTypeChange` to persistence condition - `shared-node-store.ts:1038-1041`: Skip conflict detection for nodeType conversions - `reactive-node-service.svelte.ts:621-623`: Include content with nodeType update, skip conflicts - `behaviors/mod.rs:362-393`: Added `HeaderNodeBehavior` with validation - `behaviors/mod.rs:627`: Registered HeaderNodeBehavior in registry - `database.rs:511-527`: Seeded header schema with headerLevel field ### 3. Header Syntax Visible in Blur Mode **Problem:** Blur mode showed "## Hello" instead of "Hello" **Solution:** Added `displayContent` prop to BaseNode for node-specific blur rendering - `base-node.svelte:62,563`: Added optional `displayContent` prop - `header-node.svelte:68`: Strip hashtags for blur: `internalContent.replace(/^#{1,6}\s+/, '')` - `header-node.svelte:126`: Pass displayContent to BaseNode ### 4. Width Overflow Issues **Problem:** Node containers extended beyond parent boundaries **Solution:** Added `box-sizing: border-box` to `.node` to include padding in width - `base-node.svelte:858`: Added box-sizing to prevent padding overflow - `base-node.svelte:943`: Set blur div to `flex: 1 1 100%` for full width - `header-node.svelte:157-160`: Ensure header wrapper takes full width ### 5. Multiline Content in Single-Line Headers **Problem:** Converting multiline text to header kept newlines **Solution:** HeaderNode strips newlines during content sync - `header-node.svelte:44-55`: Replace `\n` with spaces, dispatch cleaned content ## Testing **Existing Tests:** 856 tests passing ✅ - Pattern detection tests (33 tests) - Bidirectional conversion tests (9 tests) - Textarea controller tests (58 tests) - No regressions ## Test Coverage Gaps (Next Agent Tasks) The following scenarios work correctly but lack automated tests: ### Priority 1: NodeType Persistence Tests **File to create:** `src/tests/integration/node-type-persistence.test.ts` ```typescript // Test nodeType persistence through full stack describe('NodeType Persistence', () => { test('should persist text → header conversion to database', async () => { // Create text node with "## Hello" // Verify nodeManager.updateNodeType called // Wait for persistence // Check database has node_type='header' }); test('should persist header → text conversion to database', async () => { // Create header node with "## Hello" // Remove space to trigger conversion // Check database has node_type='text' }); test('should load persisted header nodes correctly after page refresh', async () => { // Persist header node // Simulate page reload // Verify loads as header type with correct styling }); }); ``` ### Priority 2: Conflict Detection Tests **File to create:** `src/tests/services/node-type-conflict-detection.test.ts` ```typescript describe('NodeType Conflict Detection', () => { test('should bypass conflict detection for nodeType conversions', async () => { // Create rapid content + nodeType updates // Verify both persist (no conflict detected) }); test('should still detect conflicts for regular concurrent edits', async () => { // Verify normal conflict detection still works }); }); ``` ### Priority 3: Blur Mode Rendering Tests **File to create:** `src/tests/components/header-node-blur-mode.test.ts` ```typescript describe('HeaderNode Blur Mode', () => { test('should strip hashtags in blur mode', () => { // Render HeaderNode with "## Hello" // Switch to blur mode // Verify displays "Hello" without ## }); test('should show hashtags in edit mode', () => { // Click to edit // Verify textarea shows "## Hello" with syntax }); test('should strip newlines when converting to header', () => { // Convert text with "Line 1\nLine 2" to header // Verify content becomes "Line 1 Line 2" }); }); ``` ### Test Implementation Notes **Testing Stack:** - Vitest + Happy-DOM (already configured) - Use `bunx vitest run` (NOT `bun test`) - Mock TauriNodeService for database operations **Key Patterns:** - Use `sharedNodeStore.updateNode()` to simulate updates - Mock `tauriNodeService.updateNode()` to verify persistence calls - Use `vi.spyOn()` to verify method calls without changing behavior **Files to Reference:** - `src/tests/components/textarea-bidirectional-conversion.test.ts` - Conversion test patterns - `src/tests/services/shared-node-store.test.ts` - Persistence test patterns - `src/tests/components/header-node-pattern-detection.test.ts` - Existing header tests ## Manual Verification Completed ✅ Cursor preserved at correct position (both text → header and header → text) ✅ NodeType persists to database (verified with sqlite3 query) ✅ Page refresh loads as header with correct styling ✅ Blur mode hides ## syntax, edit mode shows it ✅ Width respects container boundaries (measured with getBoundingClientRect) ✅ Newlines stripped when converting to headers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…ge (issue #275) **Critical Bug Fix:** - Added "header" to ALLOWED_NODE_TYPES in backend (src-tauri/src/constants.rs:16) - Backend was rejecting header node creation with HTTP 400 Bad Request - Bug occurred when pressing Enter from header node to create new header below - Fix verified: header nodes now persist correctly to database **Test Coverage Added:** 1. **node-type-persistence.test.ts** (10 tests) - Create new header node and persist (catches the bug we fixed!) - Text → Header conversion persistence - Header → Text conversion persistence - Conflict detection bypass for nodeType changes - Database reload verification 2. **node-type-conflict-detection.test.ts** (9 tests) - Rapid nodeType conversions without conflicts - Regular conflict detection still works - Edge cases and performance under load **Test Results:** - 873/874 tests passing (99.9%) - All HeaderNode functionality verified - Database persistence confirmed with sqlite3 query - 1 pre-existing failing test in sibling-chain-integrity (unrelated) **Why This Test Gap Existed:** Previous tests only covered text→header *conversion*, not creating header nodes from scratch. The new "Create New Header Node" test would have caught the missing ALLOWED_NODE_TYPES entry immediately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…onversion test **Additional Test Coverage:** - Added test for creating new header nodes from scratch (not just conversions) - This test would have caught the ALLOWED_NODE_TYPES bug immediately - Validates that backend accepts header node type on initial creation **Test Timing Improvements:** - Added explicit delays between rapid conversion steps - Ensures proper update ordering and completion - Makes test more deterministic and less flaky 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
6 tasks
malibio
added a commit
that referenced
this pull request
Feb 4, 2026
* feat: Implement unified pattern detection system for node type conversion
**Architecture Change: Plugin Registry as Single Source of Truth**
This commit introduces a centralized, extensible pattern detection system
that replaces hardcoded patterns in TextareaController with plugin-defined
configurations, making node type auto-conversion fully extensible.
**Changes:**
1. **Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface with:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection` array
2. **Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
3. **Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- Header plugin: Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Task plugin: Added `/^\[\s*[x\s]\s*\]\s/i` pattern with taskState extraction
- Both patterns properly configured with cleanContent and metadata extraction
4. **TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- Now fully extensible - external plugins can define their own patterns
5. **Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
**Benefits:**
✅ **Single Source of Truth**: Plugin registry defines both slash commands AND auto-detection patterns
✅ **Extensibility**: External plugins can register custom pattern detection
✅ **Consistency**: Pattern behavior (cleanContent, metadata) defined alongside node type
✅ **Maintainability**: No more scattered hardcoded patterns across codebase
✅ **Type Safety**: Full TypeScript support for pattern configs and metadata
**Impact:**
- Existing functionality preserved (headers h1-h6, tasks, etc.)
- No breaking changes to existing node types
- Foundation for future node type plugins (e.g., quotes, code blocks, tables)
- Aligns with issue #275 requirement for unified header pattern detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(wip): Unified pattern detection system for HeaderNode (issue #275)
**🎯 SESSION GOAL:** Implement HeaderNode with textarea-based editing (issue #275)
**✅ COMPLETED IN THIS SESSION:**
## 1. Architectural Foundation: Unified Pattern Detection System
**Problem Solved:** Previously, pattern detection was hardcoded in TextareaController
with separate patterns scattered across the codebase. No single source of truth
for both slash commands (`/h1`) and auto-detection patterns (`## `).
**Solution Implemented:** Plugin registry as single source of truth for BOTH
slash commands AND auto-detection patterns.
### Changes Made:
**A. Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection?: PatternDetectionConfig[]`
**B. Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
**C. Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- **Header plugin:** Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Supports h1-h6 (1-6 hashtags)
- `cleanContent: false` - keeps "# " syntax in content for editing
- Extracts `headerLevel` metadata from capture group
- **Task plugin:** Added `/^[-*+]?\s*\[\s*[xX\s]\s*\]\s/` pattern with taskState extraction
- Supports all markdown checkbox variants: `[ ]`, `[x]`, `- [ ]`, `* [ ]`, `+ [ ]`
- Case-insensitive: `[x]` and `[X]`
- `cleanContent: true` - removes "[ ]" from content, state shown in icon
- Extracts `taskState: 'completed' | 'pending'` from pattern
**D. TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- **Bidirectional conversion:**
- Forward: Pattern match → convert to specialized node type (e.g., `## ` → header)
- Reverse: Pattern removed → convert back to text node (e.g., `##` → text)
- Pattern detection runs on every input keystroke
- Space bar acts as "commit" trigger for conversion
**E. Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
## 2. Key UX Improvements
**Space-triggered Conversion:**
- Typing `##` alone → remains text node
- Adding space `## ` → converts to HeaderNode
- Backspacing space → immediately reverts to TextNode
**Comprehensive Task Support:**
- Plain: `[ ]`, `[x]`, `[X]`
- Dash bullet: `- [ ]`, `- [x]`, `- [X]`
- Asterisk bullet: `* [ ]`, `* [x]`, `* [X]`
- Plus bullet: `+ [ ]`, `+ [x]`, `+ [X]`
- Flexible spacing: `[ ]`, `[x]`, `[ x ]`
---
## 📋 REMAINING WORK FOR ISSUE #275:
### Testing & Verification (Primary Focus):
1. **Manual Testing Required:**
- ✅ Verify `## ` auto-converts to HeaderNode
- ✅ Verify backspace removes space and converts back to TextNode
- ✅ Test h1-h6 levels (1-6 hashtags)
- ✅ Verify real-time CSS styling applies during edit mode
- ✅ Test inline formatting (`## Hello **bold** world`)
- Edit mode: Syntax visible (`**bold**`)
- Blur mode: Syntax hidden, formatted (**bold**)
- ✅ Verify single-line behavior (already default in TextareaController)
- ✅ Verify Enter key creates new TextNode below
- ✅ Test `/h1`, `/h2`, `/h3` slash commands
2. **Automated Tests to Write:**
- Pattern detection unit tests
- Bidirectional conversion tests (text ↔ header)
- HeaderNode h1-h6 level detection tests
- Task pattern variation tests (all bullet types)
3. **Acceptance Criteria from Issue #275:**
- [x] Auto-detect `# ` pattern and convert node type
- [x] Support h1-h6 levels based on hashtag count
- [ ] **VERIFY:** Real-time CSS styling during edit mode
- [ ] **VERIFY:** Inline formatting rendered on blur only
- [ ] **VERIFY:** Single-line enforced (already default)
- [ ] **VERIFY:** Enter creates new TextNode below
- [x] Removing hashtags converts back to TextNode (bidirectional conversion)
- [x] Store hashtags in database content
- [x] Register HeaderNode in plugin system
- [ ] **TODO:** Tests cover all header levels and conversions
### Implementation Notes:
**HeaderNode Component:**
- Already exists: `src/lib/design/components/header-node.svelte`
- Already registered in plugin system
- Already has h1-h6 CSS styling
- Likely already works with new pattern detection system
- **Action:** Test to verify, may not need changes
**BaseNodeViewer:**
- TextNode already uses BaseNodeViewer successfully
- HeaderNode should work the same way (uses BaseNode wrapper)
- **Action:** Verify HeaderNode renders correctly in viewer
**Single-line Behavior:**
- TextareaController defaults to `allowMultiline: false`
- Headers should already be single-line
- **Action:** Verify no multiline input in HeaderNode
**Enter Key Behavior:**
- CreateNodeCommand already handles Enter key
- Should create new TextNode below
- **Action:** Verify Enter doesn't insert newline in header
---
## 🎯 NEXT SESSION CHECKLIST:
1. **Start Dev Server:** `bun run dev`
2. **Manual Testing:**
- Create new node, type `## `, verify conversion to HeaderNode
- Type content, verify styling applies
- Test inline formatting (`**bold**`, `*italic*`)
- Backspace to remove space, verify converts back to TextNode
- Test Enter key creates TextNode below
- Test all h1-h6 levels (1-6 hashtags)
3. **If Manual Tests Pass:**
- Write automated tests for pattern detection
- Write tests for HeaderNode behavior
- Run `bun run quality:fix`
- Create PR with `bun run gh:pr 275`
4. **If Issues Found:**
- Debug and fix issues
- Most likely areas: CSS styling, inline formatting rendering
- HeaderNode component may need tweaks for textarea compatibility
---
## 📊 TECHNICAL CONTEXT:
**Branch:** `feature/issue-275-header-node`
**Parent Issue:** #274 (Textarea migration - COMPLETED)
**Current Status:** Foundation complete, testing/verification needed
**Key Files Modified:**
- `src/lib/plugins/types.ts` - Added PatternDetectionConfig
- `src/lib/plugins/plugin-registry.ts` - Added pattern detection methods
- `src/lib/plugins/core-plugins.ts` - Added pattern configs
- `src/lib/design/components/textarea-controller.ts` - Unified pattern detection
- `src/lib/design/icons/registry.ts` - Registered header icon
**Key Files to Check/Test:**
- `src/lib/design/components/header-node.svelte` - May need verification
- `src/lib/design/components/base-node-viewer.svelte` - Should work as-is
- CSS for h1-h6 styling - Already defined in header-node.svelte
**Architecture Benefits:**
✅ Single source of truth (plugin registry)
✅ Extensible (external plugins can add patterns)
✅ Bidirectional conversion (prevents stuck node types)
✅ Type-safe (full TypeScript support)
✅ Maintainable (no scattered hardcoded patterns)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Add comprehensive HeaderNode tests and fix h1 flash bug (issue #275)
Implements comprehensive test coverage for HeaderNode pattern detection and fixes
a bug where typing incomplete header patterns (e.g., "###") would flash h1 styling.
**Bug Fix: Header Level Flash**
Fixed `parseHeaderLevel` in `header-node.svelte` to count hashtags even when space
is not yet typed. Previously:
- Typing "###" (no space) → defaulted to h1 → showed large h1 font
- Adding " " → matched pattern → switched to h3 → jarring visual jump
Now:
- Typing "###" → counts hashtags → shows h3 font immediately
- Adding " " → confirms pattern → maintains h3 font → smooth UX
**Test Coverage (33 tests, all passing):**
1. **Pattern Detection Configuration (3 tests)**
- Header plugin registration with pattern detection
- Pattern regex validation for h1-h6
- Metadata extraction for header levels
2. **Header Level Pattern Matching (6 tests)**
- All 6 header levels (h1-h6)
- Pattern matching with `# ` through `###### `
- Correct headerLevel metadata extraction
3. **Pattern Non-Matching Cases (5 tests)**
- Hashtags without space
- More than 6 hashtags
- Hashtags in middle of content
- Empty content after hashtags
- Missing space after hashtags
4. **Content Preservation (4 tests)**
- cleanContent: false preserves hashtags
- Inline formatting preservation
- Special characters and unicode
- Headers with numbers
5. **Bidirectional Conversion (4 tests)**
- Forward: text → header when typing `## `
- Reverse: header → text when removing hashtags
- Space removal behavior
- Level change detection
6. **Edge Cases (5 tests)**
- Empty strings and whitespace
- Newlines in content
- Unicode characters
- Very long headers (1000+ chars)
7. **Priority and Ordering (3 tests)**
- Pattern matching priority
- No conflicts with task pattern
- Independent pattern operation
8. **Slash Command Integration (3 tests)**
- /h1, /h2, /h3 commands
- Shortcuts (#, ##, ###)
- Content matches pattern detection
**Files Changed:**
- `src/lib/design/components/header-node.svelte` - Fixed parseHeaderLevel function
- `src/tests/components/header-node-pattern-detection.test.ts` - New comprehensive test suite
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Restore cursor focus after HeaderNode type conversion (issue #275)
Fixes critical UX bug where cursor would disappear after typing header pattern.
**Problem:**
When user typed "## " to create a header, the node would convert from TextNode
to HeaderNode, causing a complete component re-render that destroyed the textarea
and lost focus, making the cursor disappear.
**Solution:**
Added focus restoration in BaseNodeViewer's nodeTypeChanged handler (line 1393-1395).
After updating node type, we restore editing state via FocusManager to re-focus
the newly rendered HeaderNode textarea.
**Changes:**
- `base-node-viewer.svelte:1393-1395` - Added nested requestAnimationFrame with
focusManager.setEditingNode() to restore focus after node type conversion
**Tested via MCP Chrome DevTools:**
- ✅ Type "## " → converts to h2 with proper styling
- ✅ Cursor remains visible and active
- ✅ Can continue typing immediately after conversion
- ✅ No jarring focus loss or display mode switch
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* wip: Implement cursor preservation infrastructure for node type conversion (issue #275)
**HANDOFF COMMIT - Work in Progress**
This commit contains significant progress on issue #275 but cursor preservation
is not yet fully working. Next AI agent should continue from here.
---
## ✅ COMPLETED IN THIS SESSION
### 1. Comprehensive HeaderNode Testing (33 tests passing)
- **File:** `src/tests/components/header-node-pattern-detection.test.ts`
- Pattern detection for all header levels (h1-h6)
- Bidirectional conversion detection tests
- Edge cases and slash command integration
### 2. Bidirectional Conversion Tests (9 tests passing)
- **File:** `src/tests/components/textarea-bidirectional-conversion.test.ts`
- Tests text → header → text conversion sequences
- Validates controller's internal nodeType updates correctly
- Multiple conversion cycles tested
### 3. Bug Fixes
**a) H1 Flash Bug (FIXED ✅)**
- **File:** `src/lib/design/components/header-node.svelte` (line 71-87)
- **Problem:** Typing "###" showed h1 styling before space added
- **Solution:** Enhanced `parseHeaderLevel()` to count hashtags even without space
- Now shows correct h3 styling immediately when typing "###"
**b) Bidirectional Conversion (FIXED ✅)**
- **Files:** `src/lib/design/components/textarea-controller.ts` (line 780, 794, 803)
- **Problem:** Controller's internal `nodeType` wasn't updated after conversion
- **Solution:** Update `this.nodeType` after both forward and reverse conversions
- Now properly detects when to convert back: header → text when pattern removed
**c) Cursor Focus During Conversion (FIXED ✅)**
- **Problem:** Cursor would disappear when typing "## " (node converted but lost focus)
- **Solution:** Removed `{#key node.id-nodeType}` from base-node-viewer.svelte
- Changed to `{#key node.id}` only - prevents unnecessary component destruction
- Cursor now stays visible throughout typing experience
---
## ⚠️ REMAINING WORK - CURSOR POSITION PRESERVATION
### Current State: 95% Complete, Final Bug Remains
**What Works:**
✅ Real-time bidirectional conversion (text ↔ header)
✅ Cursor stays focused (doesn't disappear)
✅ Pattern detection with all metadata
✅ Comprehensive test coverage
**What Doesn't Work Yet:**
❌ Cursor resets to position 0 during node type conversion
- Expected: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at pos 2)
- Actual: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at **pos 0**)
### Infrastructure Implemented (Following Arrow Navigation Pattern)
**1. FocusManager Enhancement**
- **File:** `src/lib/services/focus-manager.svelte.ts`
- **Added:** `_nodeTypeConversionCursorPosition` state (line 68)
- **Added:** `setEditingNodeFromTypeConversion()` method (line 151-157)
- **Added:** `clearNodeTypeConversionCursorPosition()` method (line 180-182)
- **Pattern:** Mirrors arrow navigation's separate state approach
**2. Event Data Flow Enhancement**
- **TextareaController:** Captures cursor BEFORE emitting event (line 760, 791)
- **TextareaController:** Passes cursor in `nodeTypeConversionDetected` event (line 781, 799)
- **BaseNode:** Forwards cursor through `nodeTypeChanged` event (line 519)
- **BaseNodeViewer:** Receives cursor from event data (line 1386, 1441)
- **BaseNodeViewer:** Calls `focusManager.setEditingNodeFromTypeConversion()` (line 1392, 1447)
**3. Cursor Restoration Effect**
- **File:** `src/lib/design/components/base-node.svelte` (line 639-653)
- **Pattern:** Mirrors arrow navigation effect structure
- **Triggers:** When `controller` and `nodeTypeConversionCursorPosition` change
- **Action:** Calls `controller.setCursorPosition(conversionCursorPos)`
### The Bug (Needs Next Agent's Investigation)
**Logs show:**
```
[BaseNodeViewer] nodeTypeChanged: {cursorPosFromEvent: 3}
[FocusManager] setEditingNodeFromTypeConversion: {cursorPosition: 3}
[FocusManager] State after set: {conversionCursorPos: 3}
[BaseNode] Type conversion cursor effect: {conversionCursorPos: null} ← BUG HERE
```
**Analysis:**
1. Cursor position (3) is captured correctly ✅
2. FocusManager stores it correctly ✅
3. But BaseNode's effect sees `null` ❌
**Hypothesis:**
- Component switching (`{#if node.nodeType === 'text'}` vs else block) destroys TextNode, creates HeaderNode
- Both old and new components have the $effect
- Old component's effect might be clearing the value before new component can read it
- OR: Effect timing issue - `conversionCursorPos` gets cleared before effect can consume it
### Next Agent's Task
**Debug the cursor preservation flow:**
1. **Check effect execution order**
- Does old component's effect run before being destroyed?
- Does it call `clearNodeTypeConversionCursorPosition()` prematurely?
2. **Verify reactive dependencies**
- Effect depends on: `controller`, `isEditing`, `conversionCursorPos`, `editingNodeId`
- Does it trigger when new component mounts?
- Check if dependencies are stale when effect runs
3. **Consider timing solutions**
- Add `requestAnimationFrame` wrapper?
- Use different reactive primitive (not derived getter)?
- Check Svelte 5 effect scheduling order
4. **Test with logging**
- Logs are already in place (can be removed after fix)
- Check sequence: capture → store → effect → restore
### Frontend-Architect's Guidance (Session Notes)
**Conclusion:** Architecture is already correct! No MVVM classes needed, no Strategy pattern needed for now. Just a timing bug to fix.
**Key Insight:**
- Svelte components with runes ARE the ViewModel (MVVM not needed)
- Current approach is Svelte-idiomatic
- Fix is < 10 lines once timing issue is identified
**What NOT to do:**
- ❌ Don't add ViewModel classes (that's React, not Svelte)
- ❌ Don't implement Strategy pattern yet (architecture is fine)
- ❌ Don't consolidate TextNode/HeaderNode (current separation is clean)
---
## Files Modified (6 files total)
1. **base-node-viewer.svelte**
- Removed `nodeType` from `{#key}` blocks (3 places)
- Updated `nodeTypeChanged` handlers to use cursor from event data
- Removed DOM queries for cursor position
2. **base-node.svelte**
- Added `cursorPosition` to `nodeTypeChanged` event type (line 409)
- Updated event handler to pass cursor through dispatch (line 519)
- Added $effect for cursor restoration during type conversion (line 639-653)
3. **textarea-controller.ts**
- Updated event interface to include `cursorPosition` (line 93)
- Capture cursor BEFORE emitting forward conversion (line 760, 781)
- Capture cursor BEFORE emitting reverse conversion (line 791, 799)
- Update internal `nodeType` after conversions (line 785, 803)
4. **focus-manager.svelte.ts**
- Added `_nodeTypeConversionCursorPosition` state (line 68)
- Added getter `nodeTypeConversionCursorPosition` (line 112-114)
- Added `setEditingNodeFromTypeConversion()` method (line 151-157)
- Added `clearNodeTypeConversionCursorPosition()` method (line 180-182)
5. **reactive-node-service.svelte.ts**
- Removed `focusManager.setEditingNode()` call from `updateNodeType()` (line 621-622)
- Focus now managed entirely by BaseNodeViewer during conversions
6. **textarea-bidirectional-conversion.test.ts** (NEW)
- 9 comprehensive tests for bidirectional conversion
- Tests controller's internal nodeType tracking
- Tests multiple conversion sequences
---
## Test Results
**All tests passing:**
- 33 tests: header-node-pattern-detection.test.ts ✅
- 58 tests: textarea-controller.test.ts ✅
- 9 tests: textarea-bidirectional-conversion.test.ts ✅
- **Total: 100 tests passing** ✅
---
## For Next AI Agent
**Priority:** Fix the cursor position preservation timing issue
**Starting Point:**
1. Review logs from MCP Chrome DevTools testing (see analysis above)
2. Debug why `conversionCursorPos` is `null` when effect runs
3. Check if old component's effect is clearing the value prematurely
4. Consider using `untrack()` or different reactive primitive
**Success Criteria:**
- Type "## Hello" (cursor at pos 8)
- Backspace space → "##Hello" (cursor should be at pos 2, not pos 0)
- Conversion happens in real-time during editing
- All existing tests continue passing
**Files to focus on:**
- `base-node.svelte` (line 639-653) - The effect that should restore cursor
- `focus-manager.svelte.ts` - Check if state is being cleared prematurely
- Consider: Does effect run too early? Too late? Multiple times?
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Complete HeaderNode implementation with cursor preservation and persistence (issue #275)
This commit completes the HeaderNode implementation by fixing cursor preservation
during node type conversion and enabling full database persistence support.
## Problems Solved
### 1. Cursor Position Lost During Node Type Conversion
**Problem:** Cursor reset to position 0 when converting between text ↔ header nodes
**Impact:** Broke typing flow when users typed "## " or removed spaces
**Root Causes:**
- Focus manager state set AFTER node type update triggered component re-render
- Effect timing: component switch destroyed old instance before new could consume cursor position
- Missing `focus()` call and retry logic for cursor restoration
**Solution:**
- Call `focusManager.setEditingNodeFromTypeConversion()` BEFORE `nodeManager.updateNodeType()`
- Added `$effect` with `requestAnimationFrame` to restore cursor after component switch
- Retry logic: Verify cursor after 10ms and re-apply if reset by component lifecycle
**Files:**
- `base-node-viewer.svelte:1388-1393`: Reordered focus manager before node update
- `base-node.svelte:640-668`: Added cursor restoration effect with retry
- `focus-manager.svelte.ts:151-157`: Added `setEditingNodeFromTypeConversion()` method
### 2. NodeType Changes Not Persisting to Database
**Problem:** After typing "## Hello", page refresh showed it as text node, not header
**Impact:** Node type conversions were temporary, lost on reload
**Root Causes:**
a) **Conflict Detection:** Second update (nodeType + content) detected as conflicting
with first update (content only), blocked by concurrent edit detection
b) **Backend Validation:** Rust backend rejected nodeType="header" - not registered
c) **Schema Missing:** No "header" schema node seeded in database
d) **Persistence Condition:** nodeType changes not included in `shouldPersist` check
**Solutions:**
- `shared-node-store.ts:285`: Added `isNodeTypeChange` to persistence condition
- `shared-node-store.ts:1038-1041`: Skip conflict detection for nodeType conversions
- `reactive-node-service.svelte.ts:621-623`: Include content with nodeType update, skip conflicts
- `behaviors/mod.rs:362-393`: Added `HeaderNodeBehavior` with validation
- `behaviors/mod.rs:627`: Registered HeaderNodeBehavior in registry
- `database.rs:511-527`: Seeded header schema with headerLevel field
### 3. Header Syntax Visible in Blur Mode
**Problem:** Blur mode showed "## Hello" instead of "Hello"
**Solution:** Added `displayContent` prop to BaseNode for node-specific blur rendering
- `base-node.svelte:62,563`: Added optional `displayContent` prop
- `header-node.svelte:68`: Strip hashtags for blur: `internalContent.replace(/^#{1,6}\s+/, '')`
- `header-node.svelte:126`: Pass displayContent to BaseNode
### 4. Width Overflow Issues
**Problem:** Node containers extended beyond parent boundaries
**Solution:** Added `box-sizing: border-box` to `.node` to include padding in width
- `base-node.svelte:858`: Added box-sizing to prevent padding overflow
- `base-node.svelte:943`: Set blur div to `flex: 1 1 100%` for full width
- `header-node.svelte:157-160`: Ensure header wrapper takes full width
### 5. Multiline Content in Single-Line Headers
**Problem:** Converting multiline text to header kept newlines
**Solution:** HeaderNode strips newlines during content sync
- `header-node.svelte:44-55`: Replace `\n` with spaces, dispatch cleaned content
## Testing
**Existing Tests:** 856 tests passing ✅
- Pattern detection tests (33 tests)
- Bidirectional conversion tests (9 tests)
- Textarea controller tests (58 tests)
- No regressions
## Test Coverage Gaps (Next Agent Tasks)
The following scenarios work correctly but lack automated tests:
### Priority 1: NodeType Persistence Tests
**File to create:** `src/tests/integration/node-type-persistence.test.ts`
```typescript
// Test nodeType persistence through full stack
describe('NodeType Persistence', () => {
test('should persist text → header conversion to database', async () => {
// Create text node with "## Hello"
// Verify nodeManager.updateNodeType called
// Wait for persistence
// Check database has node_type='header'
});
test('should persist header → text conversion to database', async () => {
// Create header node with "## Hello"
// Remove space to trigger conversion
// Check database has node_type='text'
});
test('should load persisted header nodes correctly after page refresh', async () => {
// Persist header node
// Simulate page reload
// Verify loads as header type with correct styling
});
});
```
### Priority 2: Conflict Detection Tests
**File to create:** `src/tests/services/node-type-conflict-detection.test.ts`
```typescript
describe('NodeType Conflict Detection', () => {
test('should bypass conflict detection for nodeType conversions', async () => {
// Create rapid content + nodeType updates
// Verify both persist (no conflict detected)
});
test('should still detect conflicts for regular concurrent edits', async () => {
// Verify normal conflict detection still works
});
});
```
### Priority 3: Blur Mode Rendering Tests
**File to create:** `src/tests/components/header-node-blur-mode.test.ts`
```typescript
describe('HeaderNode Blur Mode', () => {
test('should strip hashtags in blur mode', () => {
// Render HeaderNode with "## Hello"
// Switch to blur mode
// Verify displays "Hello" without ##
});
test('should show hashtags in edit mode', () => {
// Click to edit
// Verify textarea shows "## Hello" with syntax
});
test('should strip newlines when converting to header', () => {
// Convert text with "Line 1\nLine 2" to header
// Verify content becomes "Line 1 Line 2"
});
});
```
### Test Implementation Notes
**Testing Stack:**
- Vitest + Happy-DOM (already configured)
- Use `bunx vitest run` (NOT `bun test`)
- Mock TauriNodeService for database operations
**Key Patterns:**
- Use `sharedNodeStore.updateNode()` to simulate updates
- Mock `tauriNodeService.updateNode()` to verify persistence calls
- Use `vi.spyOn()` to verify method calls without changing behavior
**Files to Reference:**
- `src/tests/components/textarea-bidirectional-conversion.test.ts` - Conversion test patterns
- `src/tests/services/shared-node-store.test.ts` - Persistence test patterns
- `src/tests/components/header-node-pattern-detection.test.ts` - Existing header tests
## Manual Verification Completed
✅ Cursor preserved at correct position (both text → header and header → text)
✅ NodeType persists to database (verified with sqlite3 query)
✅ Page refresh loads as header with correct styling
✅ Blur mode hides ## syntax, edit mode shows it
✅ Width respects container boundaries (measured with getBoundingClientRect)
✅ Newlines stripped when converting to headers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add 'header' to ALLOWED_NODE_TYPES and comprehensive test coverage (issue #275)
**Critical Bug Fix:**
- Added "header" to ALLOWED_NODE_TYPES in backend (src-tauri/src/constants.rs:16)
- Backend was rejecting header node creation with HTTP 400 Bad Request
- Bug occurred when pressing Enter from header node to create new header below
- Fix verified: header nodes now persist correctly to database
**Test Coverage Added:**
1. **node-type-persistence.test.ts** (10 tests)
- Create new header node and persist (catches the bug we fixed!)
- Text → Header conversion persistence
- Header → Text conversion persistence
- Conflict detection bypass for nodeType changes
- Database reload verification
2. **node-type-conflict-detection.test.ts** (9 tests)
- Rapid nodeType conversions without conflicts
- Regular conflict detection still works
- Edge cases and performance under load
**Test Results:**
- 873/874 tests passing (99.9%)
- All HeaderNode functionality verified
- Database persistence confirmed with sqlite3 query
- 1 pre-existing failing test in sibling-chain-integrity (unrelated)
**Why This Test Gap Existed:**
Previous tests only covered text→header *conversion*, not creating
header nodes from scratch. The new "Create New Header Node" test
would have caught the missing ALLOWED_NODE_TYPES entry immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: Add new header node creation test and improve timing in rapid conversion test
**Additional Test Coverage:**
- Added test for creating new header nodes from scratch (not just conversions)
- This test would have caught the ALLOWED_NODE_TYPES bug immediately
- Validates that backend accepts header node type on initial creation
**Test Timing Improvements:**
- Added explicit delays between rapid conversion steps
- Ensures proper update ordering and completion
- Makes test more deterministic and less flaky
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
malibio
added a commit
that referenced
this pull request
Feb 5, 2026
* feat: Implement unified pattern detection system for node type conversion
**Architecture Change: Plugin Registry as Single Source of Truth**
This commit introduces a centralized, extensible pattern detection system
that replaces hardcoded patterns in TextareaController with plugin-defined
configurations, making node type auto-conversion fully extensible.
**Changes:**
1. **Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface with:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection` array
2. **Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
3. **Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- Header plugin: Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Task plugin: Added `/^\[\s*[x\s]\s*\]\s/i` pattern with taskState extraction
- Both patterns properly configured with cleanContent and metadata extraction
4. **TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- Now fully extensible - external plugins can define their own patterns
5. **Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
**Benefits:**
✅ **Single Source of Truth**: Plugin registry defines both slash commands AND auto-detection patterns
✅ **Extensibility**: External plugins can register custom pattern detection
✅ **Consistency**: Pattern behavior (cleanContent, metadata) defined alongside node type
✅ **Maintainability**: No more scattered hardcoded patterns across codebase
✅ **Type Safety**: Full TypeScript support for pattern configs and metadata
**Impact:**
- Existing functionality preserved (headers h1-h6, tasks, etc.)
- No breaking changes to existing node types
- Foundation for future node type plugins (e.g., quotes, code blocks, tables)
- Aligns with issue #275 requirement for unified header pattern detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(wip): Unified pattern detection system for HeaderNode (issue #275)
**🎯 SESSION GOAL:** Implement HeaderNode with textarea-based editing (issue #275)
**✅ COMPLETED IN THIS SESSION:**
## 1. Architectural Foundation: Unified Pattern Detection System
**Problem Solved:** Previously, pattern detection was hardcoded in TextareaController
with separate patterns scattered across the codebase. No single source of truth
for both slash commands (`/h1`) and auto-detection patterns (`## `).
**Solution Implemented:** Plugin registry as single source of truth for BOTH
slash commands AND auto-detection patterns.
### Changes Made:
**A. Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection?: PatternDetectionConfig[]`
**B. Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
**C. Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- **Header plugin:** Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Supports h1-h6 (1-6 hashtags)
- `cleanContent: false` - keeps "# " syntax in content for editing
- Extracts `headerLevel` metadata from capture group
- **Task plugin:** Added `/^[-*+]?\s*\[\s*[xX\s]\s*\]\s/` pattern with taskState extraction
- Supports all markdown checkbox variants: `[ ]`, `[x]`, `- [ ]`, `* [ ]`, `+ [ ]`
- Case-insensitive: `[x]` and `[X]`
- `cleanContent: true` - removes "[ ]" from content, state shown in icon
- Extracts `taskState: 'completed' | 'pending'` from pattern
**D. TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- **Bidirectional conversion:**
- Forward: Pattern match → convert to specialized node type (e.g., `## ` → header)
- Reverse: Pattern removed → convert back to text node (e.g., `##` → text)
- Pattern detection runs on every input keystroke
- Space bar acts as "commit" trigger for conversion
**E. Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
## 2. Key UX Improvements
**Space-triggered Conversion:**
- Typing `##` alone → remains text node
- Adding space `## ` → converts to HeaderNode
- Backspacing space → immediately reverts to TextNode
**Comprehensive Task Support:**
- Plain: `[ ]`, `[x]`, `[X]`
- Dash bullet: `- [ ]`, `- [x]`, `- [X]`
- Asterisk bullet: `* [ ]`, `* [x]`, `* [X]`
- Plus bullet: `+ [ ]`, `+ [x]`, `+ [X]`
- Flexible spacing: `[ ]`, `[x]`, `[ x ]`
---
## 📋 REMAINING WORK FOR ISSUE #275:
### Testing & Verification (Primary Focus):
1. **Manual Testing Required:**
- ✅ Verify `## ` auto-converts to HeaderNode
- ✅ Verify backspace removes space and converts back to TextNode
- ✅ Test h1-h6 levels (1-6 hashtags)
- ✅ Verify real-time CSS styling applies during edit mode
- ✅ Test inline formatting (`## Hello **bold** world`)
- Edit mode: Syntax visible (`**bold**`)
- Blur mode: Syntax hidden, formatted (**bold**)
- ✅ Verify single-line behavior (already default in TextareaController)
- ✅ Verify Enter key creates new TextNode below
- ✅ Test `/h1`, `/h2`, `/h3` slash commands
2. **Automated Tests to Write:**
- Pattern detection unit tests
- Bidirectional conversion tests (text ↔ header)
- HeaderNode h1-h6 level detection tests
- Task pattern variation tests (all bullet types)
3. **Acceptance Criteria from Issue #275:**
- [x] Auto-detect `# ` pattern and convert node type
- [x] Support h1-h6 levels based on hashtag count
- [ ] **VERIFY:** Real-time CSS styling during edit mode
- [ ] **VERIFY:** Inline formatting rendered on blur only
- [ ] **VERIFY:** Single-line enforced (already default)
- [ ] **VERIFY:** Enter creates new TextNode below
- [x] Removing hashtags converts back to TextNode (bidirectional conversion)
- [x] Store hashtags in database content
- [x] Register HeaderNode in plugin system
- [ ] **TODO:** Tests cover all header levels and conversions
### Implementation Notes:
**HeaderNode Component:**
- Already exists: `src/lib/design/components/header-node.svelte`
- Already registered in plugin system
- Already has h1-h6 CSS styling
- Likely already works with new pattern detection system
- **Action:** Test to verify, may not need changes
**BaseNodeViewer:**
- TextNode already uses BaseNodeViewer successfully
- HeaderNode should work the same way (uses BaseNode wrapper)
- **Action:** Verify HeaderNode renders correctly in viewer
**Single-line Behavior:**
- TextareaController defaults to `allowMultiline: false`
- Headers should already be single-line
- **Action:** Verify no multiline input in HeaderNode
**Enter Key Behavior:**
- CreateNodeCommand already handles Enter key
- Should create new TextNode below
- **Action:** Verify Enter doesn't insert newline in header
---
## 🎯 NEXT SESSION CHECKLIST:
1. **Start Dev Server:** `bun run dev`
2. **Manual Testing:**
- Create new node, type `## `, verify conversion to HeaderNode
- Type content, verify styling applies
- Test inline formatting (`**bold**`, `*italic*`)
- Backspace to remove space, verify converts back to TextNode
- Test Enter key creates TextNode below
- Test all h1-h6 levels (1-6 hashtags)
3. **If Manual Tests Pass:**
- Write automated tests for pattern detection
- Write tests for HeaderNode behavior
- Run `bun run quality:fix`
- Create PR with `bun run gh:pr 275`
4. **If Issues Found:**
- Debug and fix issues
- Most likely areas: CSS styling, inline formatting rendering
- HeaderNode component may need tweaks for textarea compatibility
---
## 📊 TECHNICAL CONTEXT:
**Branch:** `feature/issue-275-header-node`
**Parent Issue:** #274 (Textarea migration - COMPLETED)
**Current Status:** Foundation complete, testing/verification needed
**Key Files Modified:**
- `src/lib/plugins/types.ts` - Added PatternDetectionConfig
- `src/lib/plugins/plugin-registry.ts` - Added pattern detection methods
- `src/lib/plugins/core-plugins.ts` - Added pattern configs
- `src/lib/design/components/textarea-controller.ts` - Unified pattern detection
- `src/lib/design/icons/registry.ts` - Registered header icon
**Key Files to Check/Test:**
- `src/lib/design/components/header-node.svelte` - May need verification
- `src/lib/design/components/base-node-viewer.svelte` - Should work as-is
- CSS for h1-h6 styling - Already defined in header-node.svelte
**Architecture Benefits:**
✅ Single source of truth (plugin registry)
✅ Extensible (external plugins can add patterns)
✅ Bidirectional conversion (prevents stuck node types)
✅ Type-safe (full TypeScript support)
✅ Maintainable (no scattered hardcoded patterns)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Add comprehensive HeaderNode tests and fix h1 flash bug (issue #275)
Implements comprehensive test coverage for HeaderNode pattern detection and fixes
a bug where typing incomplete header patterns (e.g., "###") would flash h1 styling.
**Bug Fix: Header Level Flash**
Fixed `parseHeaderLevel` in `header-node.svelte` to count hashtags even when space
is not yet typed. Previously:
- Typing "###" (no space) → defaulted to h1 → showed large h1 font
- Adding " " → matched pattern → switched to h3 → jarring visual jump
Now:
- Typing "###" → counts hashtags → shows h3 font immediately
- Adding " " → confirms pattern → maintains h3 font → smooth UX
**Test Coverage (33 tests, all passing):**
1. **Pattern Detection Configuration (3 tests)**
- Header plugin registration with pattern detection
- Pattern regex validation for h1-h6
- Metadata extraction for header levels
2. **Header Level Pattern Matching (6 tests)**
- All 6 header levels (h1-h6)
- Pattern matching with `# ` through `###### `
- Correct headerLevel metadata extraction
3. **Pattern Non-Matching Cases (5 tests)**
- Hashtags without space
- More than 6 hashtags
- Hashtags in middle of content
- Empty content after hashtags
- Missing space after hashtags
4. **Content Preservation (4 tests)**
- cleanContent: false preserves hashtags
- Inline formatting preservation
- Special characters and unicode
- Headers with numbers
5. **Bidirectional Conversion (4 tests)**
- Forward: text → header when typing `## `
- Reverse: header → text when removing hashtags
- Space removal behavior
- Level change detection
6. **Edge Cases (5 tests)**
- Empty strings and whitespace
- Newlines in content
- Unicode characters
- Very long headers (1000+ chars)
7. **Priority and Ordering (3 tests)**
- Pattern matching priority
- No conflicts with task pattern
- Independent pattern operation
8. **Slash Command Integration (3 tests)**
- /h1, /h2, /h3 commands
- Shortcuts (#, ##, ###)
- Content matches pattern detection
**Files Changed:**
- `src/lib/design/components/header-node.svelte` - Fixed parseHeaderLevel function
- `src/tests/components/header-node-pattern-detection.test.ts` - New comprehensive test suite
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Restore cursor focus after HeaderNode type conversion (issue #275)
Fixes critical UX bug where cursor would disappear after typing header pattern.
**Problem:**
When user typed "## " to create a header, the node would convert from TextNode
to HeaderNode, causing a complete component re-render that destroyed the textarea
and lost focus, making the cursor disappear.
**Solution:**
Added focus restoration in BaseNodeViewer's nodeTypeChanged handler (line 1393-1395).
After updating node type, we restore editing state via FocusManager to re-focus
the newly rendered HeaderNode textarea.
**Changes:**
- `base-node-viewer.svelte:1393-1395` - Added nested requestAnimationFrame with
focusManager.setEditingNode() to restore focus after node type conversion
**Tested via MCP Chrome DevTools:**
- ✅ Type "## " → converts to h2 with proper styling
- ✅ Cursor remains visible and active
- ✅ Can continue typing immediately after conversion
- ✅ No jarring focus loss or display mode switch
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* wip: Implement cursor preservation infrastructure for node type conversion (issue #275)
**HANDOFF COMMIT - Work in Progress**
This commit contains significant progress on issue #275 but cursor preservation
is not yet fully working. Next AI agent should continue from here.
---
## ✅ COMPLETED IN THIS SESSION
### 1. Comprehensive HeaderNode Testing (33 tests passing)
- **File:** `src/tests/components/header-node-pattern-detection.test.ts`
- Pattern detection for all header levels (h1-h6)
- Bidirectional conversion detection tests
- Edge cases and slash command integration
### 2. Bidirectional Conversion Tests (9 tests passing)
- **File:** `src/tests/components/textarea-bidirectional-conversion.test.ts`
- Tests text → header → text conversion sequences
- Validates controller's internal nodeType updates correctly
- Multiple conversion cycles tested
### 3. Bug Fixes
**a) H1 Flash Bug (FIXED ✅)**
- **File:** `src/lib/design/components/header-node.svelte` (line 71-87)
- **Problem:** Typing "###" showed h1 styling before space added
- **Solution:** Enhanced `parseHeaderLevel()` to count hashtags even without space
- Now shows correct h3 styling immediately when typing "###"
**b) Bidirectional Conversion (FIXED ✅)**
- **Files:** `src/lib/design/components/textarea-controller.ts` (line 780, 794, 803)
- **Problem:** Controller's internal `nodeType` wasn't updated after conversion
- **Solution:** Update `this.nodeType` after both forward and reverse conversions
- Now properly detects when to convert back: header → text when pattern removed
**c) Cursor Focus During Conversion (FIXED ✅)**
- **Problem:** Cursor would disappear when typing "## " (node converted but lost focus)
- **Solution:** Removed `{#key node.id-nodeType}` from base-node-viewer.svelte
- Changed to `{#key node.id}` only - prevents unnecessary component destruction
- Cursor now stays visible throughout typing experience
---
## ⚠️ REMAINING WORK - CURSOR POSITION PRESERVATION
### Current State: 95% Complete, Final Bug Remains
**What Works:**
✅ Real-time bidirectional conversion (text ↔ header)
✅ Cursor stays focused (doesn't disappear)
✅ Pattern detection with all metadata
✅ Comprehensive test coverage
**What Doesn't Work Yet:**
❌ Cursor resets to position 0 during node type conversion
- Expected: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at pos 2)
- Actual: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at **pos 0**)
### Infrastructure Implemented (Following Arrow Navigation Pattern)
**1. FocusManager Enhancement**
- **File:** `src/lib/services/focus-manager.svelte.ts`
- **Added:** `_nodeTypeConversionCursorPosition` state (line 68)
- **Added:** `setEditingNodeFromTypeConversion()` method (line 151-157)
- **Added:** `clearNodeTypeConversionCursorPosition()` method (line 180-182)
- **Pattern:** Mirrors arrow navigation's separate state approach
**2. Event Data Flow Enhancement**
- **TextareaController:** Captures cursor BEFORE emitting event (line 760, 791)
- **TextareaController:** Passes cursor in `nodeTypeConversionDetected` event (line 781, 799)
- **BaseNode:** Forwards cursor through `nodeTypeChanged` event (line 519)
- **BaseNodeViewer:** Receives cursor from event data (line 1386, 1441)
- **BaseNodeViewer:** Calls `focusManager.setEditingNodeFromTypeConversion()` (line 1392, 1447)
**3. Cursor Restoration Effect**
- **File:** `src/lib/design/components/base-node.svelte` (line 639-653)
- **Pattern:** Mirrors arrow navigation effect structure
- **Triggers:** When `controller` and `nodeTypeConversionCursorPosition` change
- **Action:** Calls `controller.setCursorPosition(conversionCursorPos)`
### The Bug (Needs Next Agent's Investigation)
**Logs show:**
```
[BaseNodeViewer] nodeTypeChanged: {cursorPosFromEvent: 3}
[FocusManager] setEditingNodeFromTypeConversion: {cursorPosition: 3}
[FocusManager] State after set: {conversionCursorPos: 3}
[BaseNode] Type conversion cursor effect: {conversionCursorPos: null} ← BUG HERE
```
**Analysis:**
1. Cursor position (3) is captured correctly ✅
2. FocusManager stores it correctly ✅
3. But BaseNode's effect sees `null` ❌
**Hypothesis:**
- Component switching (`{#if node.nodeType === 'text'}` vs else block) destroys TextNode, creates HeaderNode
- Both old and new components have the $effect
- Old component's effect might be clearing the value before new component can read it
- OR: Effect timing issue - `conversionCursorPos` gets cleared before effect can consume it
### Next Agent's Task
**Debug the cursor preservation flow:**
1. **Check effect execution order**
- Does old component's effect run before being destroyed?
- Does it call `clearNodeTypeConversionCursorPosition()` prematurely?
2. **Verify reactive dependencies**
- Effect depends on: `controller`, `isEditing`, `conversionCursorPos`, `editingNodeId`
- Does it trigger when new component mounts?
- Check if dependencies are stale when effect runs
3. **Consider timing solutions**
- Add `requestAnimationFrame` wrapper?
- Use different reactive primitive (not derived getter)?
- Check Svelte 5 effect scheduling order
4. **Test with logging**
- Logs are already in place (can be removed after fix)
- Check sequence: capture → store → effect → restore
### Frontend-Architect's Guidance (Session Notes)
**Conclusion:** Architecture is already correct! No MVVM classes needed, no Strategy pattern needed for now. Just a timing bug to fix.
**Key Insight:**
- Svelte components with runes ARE the ViewModel (MVVM not needed)
- Current approach is Svelte-idiomatic
- Fix is < 10 lines once timing issue is identified
**What NOT to do:**
- ❌ Don't add ViewModel classes (that's React, not Svelte)
- ❌ Don't implement Strategy pattern yet (architecture is fine)
- ❌ Don't consolidate TextNode/HeaderNode (current separation is clean)
---
## Files Modified (6 files total)
1. **base-node-viewer.svelte**
- Removed `nodeType` from `{#key}` blocks (3 places)
- Updated `nodeTypeChanged` handlers to use cursor from event data
- Removed DOM queries for cursor position
2. **base-node.svelte**
- Added `cursorPosition` to `nodeTypeChanged` event type (line 409)
- Updated event handler to pass cursor through dispatch (line 519)
- Added $effect for cursor restoration during type conversion (line 639-653)
3. **textarea-controller.ts**
- Updated event interface to include `cursorPosition` (line 93)
- Capture cursor BEFORE emitting forward conversion (line 760, 781)
- Capture cursor BEFORE emitting reverse conversion (line 791, 799)
- Update internal `nodeType` after conversions (line 785, 803)
4. **focus-manager.svelte.ts**
- Added `_nodeTypeConversionCursorPosition` state (line 68)
- Added getter `nodeTypeConversionCursorPosition` (line 112-114)
- Added `setEditingNodeFromTypeConversion()` method (line 151-157)
- Added `clearNodeTypeConversionCursorPosition()` method (line 180-182)
5. **reactive-node-service.svelte.ts**
- Removed `focusManager.setEditingNode()` call from `updateNodeType()` (line 621-622)
- Focus now managed entirely by BaseNodeViewer during conversions
6. **textarea-bidirectional-conversion.test.ts** (NEW)
- 9 comprehensive tests for bidirectional conversion
- Tests controller's internal nodeType tracking
- Tests multiple conversion sequences
---
## Test Results
**All tests passing:**
- 33 tests: header-node-pattern-detection.test.ts ✅
- 58 tests: textarea-controller.test.ts ✅
- 9 tests: textarea-bidirectional-conversion.test.ts ✅
- **Total: 100 tests passing** ✅
---
## For Next AI Agent
**Priority:** Fix the cursor position preservation timing issue
**Starting Point:**
1. Review logs from MCP Chrome DevTools testing (see analysis above)
2. Debug why `conversionCursorPos` is `null` when effect runs
3. Check if old component's effect is clearing the value prematurely
4. Consider using `untrack()` or different reactive primitive
**Success Criteria:**
- Type "## Hello" (cursor at pos 8)
- Backspace space → "##Hello" (cursor should be at pos 2, not pos 0)
- Conversion happens in real-time during editing
- All existing tests continue passing
**Files to focus on:**
- `base-node.svelte` (line 639-653) - The effect that should restore cursor
- `focus-manager.svelte.ts` - Check if state is being cleared prematurely
- Consider: Does effect run too early? Too late? Multiple times?
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Complete HeaderNode implementation with cursor preservation and persistence (issue #275)
This commit completes the HeaderNode implementation by fixing cursor preservation
during node type conversion and enabling full database persistence support.
## Problems Solved
### 1. Cursor Position Lost During Node Type Conversion
**Problem:** Cursor reset to position 0 when converting between text ↔ header nodes
**Impact:** Broke typing flow when users typed "## " or removed spaces
**Root Causes:**
- Focus manager state set AFTER node type update triggered component re-render
- Effect timing: component switch destroyed old instance before new could consume cursor position
- Missing `focus()` call and retry logic for cursor restoration
**Solution:**
- Call `focusManager.setEditingNodeFromTypeConversion()` BEFORE `nodeManager.updateNodeType()`
- Added `$effect` with `requestAnimationFrame` to restore cursor after component switch
- Retry logic: Verify cursor after 10ms and re-apply if reset by component lifecycle
**Files:**
- `base-node-viewer.svelte:1388-1393`: Reordered focus manager before node update
- `base-node.svelte:640-668`: Added cursor restoration effect with retry
- `focus-manager.svelte.ts:151-157`: Added `setEditingNodeFromTypeConversion()` method
### 2. NodeType Changes Not Persisting to Database
**Problem:** After typing "## Hello", page refresh showed it as text node, not header
**Impact:** Node type conversions were temporary, lost on reload
**Root Causes:**
a) **Conflict Detection:** Second update (nodeType + content) detected as conflicting
with first update (content only), blocked by concurrent edit detection
b) **Backend Validation:** Rust backend rejected nodeType="header" - not registered
c) **Schema Missing:** No "header" schema node seeded in database
d) **Persistence Condition:** nodeType changes not included in `shouldPersist` check
**Solutions:**
- `shared-node-store.ts:285`: Added `isNodeTypeChange` to persistence condition
- `shared-node-store.ts:1038-1041`: Skip conflict detection for nodeType conversions
- `reactive-node-service.svelte.ts:621-623`: Include content with nodeType update, skip conflicts
- `behaviors/mod.rs:362-393`: Added `HeaderNodeBehavior` with validation
- `behaviors/mod.rs:627`: Registered HeaderNodeBehavior in registry
- `database.rs:511-527`: Seeded header schema with headerLevel field
### 3. Header Syntax Visible in Blur Mode
**Problem:** Blur mode showed "## Hello" instead of "Hello"
**Solution:** Added `displayContent` prop to BaseNode for node-specific blur rendering
- `base-node.svelte:62,563`: Added optional `displayContent` prop
- `header-node.svelte:68`: Strip hashtags for blur: `internalContent.replace(/^#{1,6}\s+/, '')`
- `header-node.svelte:126`: Pass displayContent to BaseNode
### 4. Width Overflow Issues
**Problem:** Node containers extended beyond parent boundaries
**Solution:** Added `box-sizing: border-box` to `.node` to include padding in width
- `base-node.svelte:858`: Added box-sizing to prevent padding overflow
- `base-node.svelte:943`: Set blur div to `flex: 1 1 100%` for full width
- `header-node.svelte:157-160`: Ensure header wrapper takes full width
### 5. Multiline Content in Single-Line Headers
**Problem:** Converting multiline text to header kept newlines
**Solution:** HeaderNode strips newlines during content sync
- `header-node.svelte:44-55`: Replace `\n` with spaces, dispatch cleaned content
## Testing
**Existing Tests:** 856 tests passing ✅
- Pattern detection tests (33 tests)
- Bidirectional conversion tests (9 tests)
- Textarea controller tests (58 tests)
- No regressions
## Test Coverage Gaps (Next Agent Tasks)
The following scenarios work correctly but lack automated tests:
### Priority 1: NodeType Persistence Tests
**File to create:** `src/tests/integration/node-type-persistence.test.ts`
```typescript
// Test nodeType persistence through full stack
describe('NodeType Persistence', () => {
test('should persist text → header conversion to database', async () => {
// Create text node with "## Hello"
// Verify nodeManager.updateNodeType called
// Wait for persistence
// Check database has node_type='header'
});
test('should persist header → text conversion to database', async () => {
// Create header node with "## Hello"
// Remove space to trigger conversion
// Check database has node_type='text'
});
test('should load persisted header nodes correctly after page refresh', async () => {
// Persist header node
// Simulate page reload
// Verify loads as header type with correct styling
});
});
```
### Priority 2: Conflict Detection Tests
**File to create:** `src/tests/services/node-type-conflict-detection.test.ts`
```typescript
describe('NodeType Conflict Detection', () => {
test('should bypass conflict detection for nodeType conversions', async () => {
// Create rapid content + nodeType updates
// Verify both persist (no conflict detected)
});
test('should still detect conflicts for regular concurrent edits', async () => {
// Verify normal conflict detection still works
});
});
```
### Priority 3: Blur Mode Rendering Tests
**File to create:** `src/tests/components/header-node-blur-mode.test.ts`
```typescript
describe('HeaderNode Blur Mode', () => {
test('should strip hashtags in blur mode', () => {
// Render HeaderNode with "## Hello"
// Switch to blur mode
// Verify displays "Hello" without ##
});
test('should show hashtags in edit mode', () => {
// Click to edit
// Verify textarea shows "## Hello" with syntax
});
test('should strip newlines when converting to header', () => {
// Convert text with "Line 1\nLine 2" to header
// Verify content becomes "Line 1 Line 2"
});
});
```
### Test Implementation Notes
**Testing Stack:**
- Vitest + Happy-DOM (already configured)
- Use `bunx vitest run` (NOT `bun test`)
- Mock TauriNodeService for database operations
**Key Patterns:**
- Use `sharedNodeStore.updateNode()` to simulate updates
- Mock `tauriNodeService.updateNode()` to verify persistence calls
- Use `vi.spyOn()` to verify method calls without changing behavior
**Files to Reference:**
- `src/tests/components/textarea-bidirectional-conversion.test.ts` - Conversion test patterns
- `src/tests/services/shared-node-store.test.ts` - Persistence test patterns
- `src/tests/components/header-node-pattern-detection.test.ts` - Existing header tests
## Manual Verification Completed
✅ Cursor preserved at correct position (both text → header and header → text)
✅ NodeType persists to database (verified with sqlite3 query)
✅ Page refresh loads as header with correct styling
✅ Blur mode hides ## syntax, edit mode shows it
✅ Width respects container boundaries (measured with getBoundingClientRect)
✅ Newlines stripped when converting to headers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add 'header' to ALLOWED_NODE_TYPES and comprehensive test coverage (issue #275)
**Critical Bug Fix:**
- Added "header" to ALLOWED_NODE_TYPES in backend (src-tauri/src/constants.rs:16)
- Backend was rejecting header node creation with HTTP 400 Bad Request
- Bug occurred when pressing Enter from header node to create new header below
- Fix verified: header nodes now persist correctly to database
**Test Coverage Added:**
1. **node-type-persistence.test.ts** (10 tests)
- Create new header node and persist (catches the bug we fixed!)
- Text → Header conversion persistence
- Header → Text conversion persistence
- Conflict detection bypass for nodeType changes
- Database reload verification
2. **node-type-conflict-detection.test.ts** (9 tests)
- Rapid nodeType conversions without conflicts
- Regular conflict detection still works
- Edge cases and performance under load
**Test Results:**
- 873/874 tests passing (99.9%)
- All HeaderNode functionality verified
- Database persistence confirmed with sqlite3 query
- 1 pre-existing failing test in sibling-chain-integrity (unrelated)
**Why This Test Gap Existed:**
Previous tests only covered text→header *conversion*, not creating
header nodes from scratch. The new "Create New Header Node" test
would have caught the missing ALLOWED_NODE_TYPES entry immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: Add new header node creation test and improve timing in rapid conversion test
**Additional Test Coverage:**
- Added test for creating new header nodes from scratch (not just conversions)
- This test would have caught the ALLOWED_NODE_TYPES bug immediately
- Validates that backend accepts header node type on initial creation
**Test Timing Improvements:**
- Added explicit delays between rapid conversion steps
- Ensures proper update ordering and completion
- Makes test more deterministic and less flaky
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
malibio
added a commit
that referenced
this pull request
Feb 26, 2026
* feat: Implement unified pattern detection system for node type conversion
**Architecture Change: Plugin Registry as Single Source of Truth**
This commit introduces a centralized, extensible pattern detection system
that replaces hardcoded patterns in TextareaController with plugin-defined
configurations, making node type auto-conversion fully extensible.
**Changes:**
1. **Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface with:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection` array
2. **Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
3. **Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- Header plugin: Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Task plugin: Added `/^\[\s*[x\s]\s*\]\s/i` pattern with taskState extraction
- Both patterns properly configured with cleanContent and metadata extraction
4. **TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- Now fully extensible - external plugins can define their own patterns
5. **Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
**Benefits:**
✅ **Single Source of Truth**: Plugin registry defines both slash commands AND auto-detection patterns
✅ **Extensibility**: External plugins can register custom pattern detection
✅ **Consistency**: Pattern behavior (cleanContent, metadata) defined alongside node type
✅ **Maintainability**: No more scattered hardcoded patterns across codebase
✅ **Type Safety**: Full TypeScript support for pattern configs and metadata
**Impact:**
- Existing functionality preserved (headers h1-h6, tasks, etc.)
- No breaking changes to existing node types
- Foundation for future node type plugins (e.g., quotes, code blocks, tables)
- Aligns with issue #275 requirement for unified header pattern detection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(wip): Unified pattern detection system for HeaderNode (issue #275)
**🎯 SESSION GOAL:** Implement HeaderNode with textarea-based editing (issue #275)
**✅ COMPLETED IN THIS SESSION:**
## 1. Architectural Foundation: Unified Pattern Detection System
**Problem Solved:** Previously, pattern detection was hardcoded in TextareaController
with separate patterns scattered across the codebase. No single source of truth
for both slash commands (`/h1`) and auto-detection patterns (`## `).
**Solution Implemented:** Plugin registry as single source of truth for BOTH
slash commands AND auto-detection patterns.
### Changes Made:
**A. Enhanced Plugin Types** (`src/lib/plugins/types.ts`):
- Added `PatternDetectionConfig` interface:
- `pattern`: RegExp for content matching
- `targetNodeType`: Target node after conversion
- `cleanContent`: Whether to remove pattern syntax
- `extractMetadata`: Function to extract metadata from matches
- `priority`: Pattern matching priority
- Extended `NodeTypeConfig` to include `patternDetection?: PatternDetectionConfig[]`
**B. Plugin Registry** (`src/lib/plugins/plugin-registry.ts`):
- Added `getAllPatternDetectionConfigs()`: Returns all enabled patterns sorted by priority
- Added `detectPatternInContent()`: Detects patterns and extracts metadata
- Enables external plugins to register custom pattern detection
**C. Core Plugins** (`src/lib/plugins/core-plugins.ts`):
- **Header plugin:** Added `/^(#{1,6})\s/` pattern with headerLevel extraction
- Supports h1-h6 (1-6 hashtags)
- `cleanContent: false` - keeps "# " syntax in content for editing
- Extracts `headerLevel` metadata from capture group
- **Task plugin:** Added `/^[-*+]?\s*\[\s*[xX\s]\s*\]\s/` pattern with taskState extraction
- Supports all markdown checkbox variants: `[ ]`, `[x]`, `- [ ]`, `* [ ]`, `+ [ ]`
- Case-insensitive: `[x]` and `[X]`
- `cleanContent: true` - removes "[ ]" from content, state shown in icon
- Extracts `taskState: 'completed' | 'pending'` from pattern
**D. TextareaController Refactor** (`src/lib/design/components/textarea-controller.ts`):
- ❌ Removed hardcoded patterns: `HEADER_PATTERN`, `CHECKBOX_PATTERN`, `QUOTE_PATTERN`
- ✅ Replaced with plugin registry-based detection in `detectNodeTypeConversion()`
- **Bidirectional conversion:**
- Forward: Pattern match → convert to specialized node type (e.g., `## ` → header)
- Reverse: Pattern removed → convert back to text node (e.g., `##` → text)
- Pattern detection runs on every input keystroke
- Space bar acts as "commit" trigger for conversion
**E. Icon Registry** (`src/lib/design/icons/registry.ts`):
- Added 'header' node type with CircleIcon component
- Configured with ring effect for children (consistent with text nodes)
## 2. Key UX Improvements
**Space-triggered Conversion:**
- Typing `##` alone → remains text node
- Adding space `## ` → converts to HeaderNode
- Backspacing space → immediately reverts to TextNode
**Comprehensive Task Support:**
- Plain: `[ ]`, `[x]`, `[X]`
- Dash bullet: `- [ ]`, `- [x]`, `- [X]`
- Asterisk bullet: `* [ ]`, `* [x]`, `* [X]`
- Plus bullet: `+ [ ]`, `+ [x]`, `+ [X]`
- Flexible spacing: `[ ]`, `[x]`, `[ x ]`
---
## 📋 REMAINING WORK FOR ISSUE #275:
### Testing & Verification (Primary Focus):
1. **Manual Testing Required:**
- ✅ Verify `## ` auto-converts to HeaderNode
- ✅ Verify backspace removes space and converts back to TextNode
- ✅ Test h1-h6 levels (1-6 hashtags)
- ✅ Verify real-time CSS styling applies during edit mode
- ✅ Test inline formatting (`## Hello **bold** world`)
- Edit mode: Syntax visible (`**bold**`)
- Blur mode: Syntax hidden, formatted (**bold**)
- ✅ Verify single-line behavior (already default in TextareaController)
- ✅ Verify Enter key creates new TextNode below
- ✅ Test `/h1`, `/h2`, `/h3` slash commands
2. **Automated Tests to Write:**
- Pattern detection unit tests
- Bidirectional conversion tests (text ↔ header)
- HeaderNode h1-h6 level detection tests
- Task pattern variation tests (all bullet types)
3. **Acceptance Criteria from Issue #275:**
- [x] Auto-detect `# ` pattern and convert node type
- [x] Support h1-h6 levels based on hashtag count
- [ ] **VERIFY:** Real-time CSS styling during edit mode
- [ ] **VERIFY:** Inline formatting rendered on blur only
- [ ] **VERIFY:** Single-line enforced (already default)
- [ ] **VERIFY:** Enter creates new TextNode below
- [x] Removing hashtags converts back to TextNode (bidirectional conversion)
- [x] Store hashtags in database content
- [x] Register HeaderNode in plugin system
- [ ] **TODO:** Tests cover all header levels and conversions
### Implementation Notes:
**HeaderNode Component:**
- Already exists: `src/lib/design/components/header-node.svelte`
- Already registered in plugin system
- Already has h1-h6 CSS styling
- Likely already works with new pattern detection system
- **Action:** Test to verify, may not need changes
**BaseNodeViewer:**
- TextNode already uses BaseNodeViewer successfully
- HeaderNode should work the same way (uses BaseNode wrapper)
- **Action:** Verify HeaderNode renders correctly in viewer
**Single-line Behavior:**
- TextareaController defaults to `allowMultiline: false`
- Headers should already be single-line
- **Action:** Verify no multiline input in HeaderNode
**Enter Key Behavior:**
- CreateNodeCommand already handles Enter key
- Should create new TextNode below
- **Action:** Verify Enter doesn't insert newline in header
---
## 🎯 NEXT SESSION CHECKLIST:
1. **Start Dev Server:** `bun run dev`
2. **Manual Testing:**
- Create new node, type `## `, verify conversion to HeaderNode
- Type content, verify styling applies
- Test inline formatting (`**bold**`, `*italic*`)
- Backspace to remove space, verify converts back to TextNode
- Test Enter key creates TextNode below
- Test all h1-h6 levels (1-6 hashtags)
3. **If Manual Tests Pass:**
- Write automated tests for pattern detection
- Write tests for HeaderNode behavior
- Run `bun run quality:fix`
- Create PR with `bun run gh:pr 275`
4. **If Issues Found:**
- Debug and fix issues
- Most likely areas: CSS styling, inline formatting rendering
- HeaderNode component may need tweaks for textarea compatibility
---
## 📊 TECHNICAL CONTEXT:
**Branch:** `feature/issue-275-header-node`
**Parent Issue:** #274 (Textarea migration - COMPLETED)
**Current Status:** Foundation complete, testing/verification needed
**Key Files Modified:**
- `src/lib/plugins/types.ts` - Added PatternDetectionConfig
- `src/lib/plugins/plugin-registry.ts` - Added pattern detection methods
- `src/lib/plugins/core-plugins.ts` - Added pattern configs
- `src/lib/design/components/textarea-controller.ts` - Unified pattern detection
- `src/lib/design/icons/registry.ts` - Registered header icon
**Key Files to Check/Test:**
- `src/lib/design/components/header-node.svelte` - May need verification
- `src/lib/design/components/base-node-viewer.svelte` - Should work as-is
- CSS for h1-h6 styling - Already defined in header-node.svelte
**Architecture Benefits:**
✅ Single source of truth (plugin registry)
✅ Extensible (external plugins can add patterns)
✅ Bidirectional conversion (prevents stuck node types)
✅ Type-safe (full TypeScript support)
✅ Maintainable (no scattered hardcoded patterns)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Add comprehensive HeaderNode tests and fix h1 flash bug (issue #275)
Implements comprehensive test coverage for HeaderNode pattern detection and fixes
a bug where typing incomplete header patterns (e.g., "###") would flash h1 styling.
**Bug Fix: Header Level Flash**
Fixed `parseHeaderLevel` in `header-node.svelte` to count hashtags even when space
is not yet typed. Previously:
- Typing "###" (no space) → defaulted to h1 → showed large h1 font
- Adding " " → matched pattern → switched to h3 → jarring visual jump
Now:
- Typing "###" → counts hashtags → shows h3 font immediately
- Adding " " → confirms pattern → maintains h3 font → smooth UX
**Test Coverage (33 tests, all passing):**
1. **Pattern Detection Configuration (3 tests)**
- Header plugin registration with pattern detection
- Pattern regex validation for h1-h6
- Metadata extraction for header levels
2. **Header Level Pattern Matching (6 tests)**
- All 6 header levels (h1-h6)
- Pattern matching with `# ` through `###### `
- Correct headerLevel metadata extraction
3. **Pattern Non-Matching Cases (5 tests)**
- Hashtags without space
- More than 6 hashtags
- Hashtags in middle of content
- Empty content after hashtags
- Missing space after hashtags
4. **Content Preservation (4 tests)**
- cleanContent: false preserves hashtags
- Inline formatting preservation
- Special characters and unicode
- Headers with numbers
5. **Bidirectional Conversion (4 tests)**
- Forward: text → header when typing `## `
- Reverse: header → text when removing hashtags
- Space removal behavior
- Level change detection
6. **Edge Cases (5 tests)**
- Empty strings and whitespace
- Newlines in content
- Unicode characters
- Very long headers (1000+ chars)
7. **Priority and Ordering (3 tests)**
- Pattern matching priority
- No conflicts with task pattern
- Independent pattern operation
8. **Slash Command Integration (3 tests)**
- /h1, /h2, /h3 commands
- Shortcuts (#, ##, ###)
- Content matches pattern detection
**Files Changed:**
- `src/lib/design/components/header-node.svelte` - Fixed parseHeaderLevel function
- `src/tests/components/header-node-pattern-detection.test.ts` - New comprehensive test suite
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Restore cursor focus after HeaderNode type conversion (issue #275)
Fixes critical UX bug where cursor would disappear after typing header pattern.
**Problem:**
When user typed "## " to create a header, the node would convert from TextNode
to HeaderNode, causing a complete component re-render that destroyed the textarea
and lost focus, making the cursor disappear.
**Solution:**
Added focus restoration in BaseNodeViewer's nodeTypeChanged handler (line 1393-1395).
After updating node type, we restore editing state via FocusManager to re-focus
the newly rendered HeaderNode textarea.
**Changes:**
- `base-node-viewer.svelte:1393-1395` - Added nested requestAnimationFrame with
focusManager.setEditingNode() to restore focus after node type conversion
**Tested via MCP Chrome DevTools:**
- ✅ Type "## " → converts to h2 with proper styling
- ✅ Cursor remains visible and active
- ✅ Can continue typing immediately after conversion
- ✅ No jarring focus loss or display mode switch
**Related Issue:** #275
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* wip: Implement cursor preservation infrastructure for node type conversion (issue #275)
**HANDOFF COMMIT - Work in Progress**
This commit contains significant progress on issue #275 but cursor preservation
is not yet fully working. Next AI agent should continue from here.
---
## ✅ COMPLETED IN THIS SESSION
### 1. Comprehensive HeaderNode Testing (33 tests passing)
- **File:** `src/tests/components/header-node-pattern-detection.test.ts`
- Pattern detection for all header levels (h1-h6)
- Bidirectional conversion detection tests
- Edge cases and slash command integration
### 2. Bidirectional Conversion Tests (9 tests passing)
- **File:** `src/tests/components/textarea-bidirectional-conversion.test.ts`
- Tests text → header → text conversion sequences
- Validates controller's internal nodeType updates correctly
- Multiple conversion cycles tested
### 3. Bug Fixes
**a) H1 Flash Bug (FIXED ✅)**
- **File:** `src/lib/design/components/header-node.svelte` (line 71-87)
- **Problem:** Typing "###" showed h1 styling before space added
- **Solution:** Enhanced `parseHeaderLevel()` to count hashtags even without space
- Now shows correct h3 styling immediately when typing "###"
**b) Bidirectional Conversion (FIXED ✅)**
- **Files:** `src/lib/design/components/textarea-controller.ts` (line 780, 794, 803)
- **Problem:** Controller's internal `nodeType` wasn't updated after conversion
- **Solution:** Update `this.nodeType` after both forward and reverse conversions
- Now properly detects when to convert back: header → text when pattern removed
**c) Cursor Focus During Conversion (FIXED ✅)**
- **Problem:** Cursor would disappear when typing "## " (node converted but lost focus)
- **Solution:** Removed `{#key node.id-nodeType}` from base-node-viewer.svelte
- Changed to `{#key node.id}` only - prevents unnecessary component destruction
- Cursor now stays visible throughout typing experience
---
## ⚠️ REMAINING WORK - CURSOR POSITION PRESERVATION
### Current State: 95% Complete, Final Bug Remains
**What Works:**
✅ Real-time bidirectional conversion (text ↔ header)
✅ Cursor stays focused (doesn't disappear)
✅ Pattern detection with all metadata
✅ Comprehensive test coverage
**What Doesn't Work Yet:**
❌ Cursor resets to position 0 during node type conversion
- Expected: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at pos 2)
- Actual: Type "## Hello" (cursor at pos 8), backspace → "##Hello" (cursor at **pos 0**)
### Infrastructure Implemented (Following Arrow Navigation Pattern)
**1. FocusManager Enhancement**
- **File:** `src/lib/services/focus-manager.svelte.ts`
- **Added:** `_nodeTypeConversionCursorPosition` state (line 68)
- **Added:** `setEditingNodeFromTypeConversion()` method (line 151-157)
- **Added:** `clearNodeTypeConversionCursorPosition()` method (line 180-182)
- **Pattern:** Mirrors arrow navigation's separate state approach
**2. Event Data Flow Enhancement**
- **TextareaController:** Captures cursor BEFORE emitting event (line 760, 791)
- **TextareaController:** Passes cursor in `nodeTypeConversionDetected` event (line 781, 799)
- **BaseNode:** Forwards cursor through `nodeTypeChanged` event (line 519)
- **BaseNodeViewer:** Receives cursor from event data (line 1386, 1441)
- **BaseNodeViewer:** Calls `focusManager.setEditingNodeFromTypeConversion()` (line 1392, 1447)
**3. Cursor Restoration Effect**
- **File:** `src/lib/design/components/base-node.svelte` (line 639-653)
- **Pattern:** Mirrors arrow navigation effect structure
- **Triggers:** When `controller` and `nodeTypeConversionCursorPosition` change
- **Action:** Calls `controller.setCursorPosition(conversionCursorPos)`
### The Bug (Needs Next Agent's Investigation)
**Logs show:**
```
[BaseNodeViewer] nodeTypeChanged: {cursorPosFromEvent: 3}
[FocusManager] setEditingNodeFromTypeConversion: {cursorPosition: 3}
[FocusManager] State after set: {conversionCursorPos: 3}
[BaseNode] Type conversion cursor effect: {conversionCursorPos: null} ← BUG HERE
```
**Analysis:**
1. Cursor position (3) is captured correctly ✅
2. FocusManager stores it correctly ✅
3. But BaseNode's effect sees `null` ❌
**Hypothesis:**
- Component switching (`{#if node.nodeType === 'text'}` vs else block) destroys TextNode, creates HeaderNode
- Both old and new components have the $effect
- Old component's effect might be clearing the value before new component can read it
- OR: Effect timing issue - `conversionCursorPos` gets cleared before effect can consume it
### Next Agent's Task
**Debug the cursor preservation flow:**
1. **Check effect execution order**
- Does old component's effect run before being destroyed?
- Does it call `clearNodeTypeConversionCursorPosition()` prematurely?
2. **Verify reactive dependencies**
- Effect depends on: `controller`, `isEditing`, `conversionCursorPos`, `editingNodeId`
- Does it trigger when new component mounts?
- Check if dependencies are stale when effect runs
3. **Consider timing solutions**
- Add `requestAnimationFrame` wrapper?
- Use different reactive primitive (not derived getter)?
- Check Svelte 5 effect scheduling order
4. **Test with logging**
- Logs are already in place (can be removed after fix)
- Check sequence: capture → store → effect → restore
### Frontend-Architect's Guidance (Session Notes)
**Conclusion:** Architecture is already correct! No MVVM classes needed, no Strategy pattern needed for now. Just a timing bug to fix.
**Key Insight:**
- Svelte components with runes ARE the ViewModel (MVVM not needed)
- Current approach is Svelte-idiomatic
- Fix is < 10 lines once timing issue is identified
**What NOT to do:**
- ❌ Don't add ViewModel classes (that's React, not Svelte)
- ❌ Don't implement Strategy pattern yet (architecture is fine)
- ❌ Don't consolidate TextNode/HeaderNode (current separation is clean)
---
## Files Modified (6 files total)
1. **base-node-viewer.svelte**
- Removed `nodeType` from `{#key}` blocks (3 places)
- Updated `nodeTypeChanged` handlers to use cursor from event data
- Removed DOM queries for cursor position
2. **base-node.svelte**
- Added `cursorPosition` to `nodeTypeChanged` event type (line 409)
- Updated event handler to pass cursor through dispatch (line 519)
- Added $effect for cursor restoration during type conversion (line 639-653)
3. **textarea-controller.ts**
- Updated event interface to include `cursorPosition` (line 93)
- Capture cursor BEFORE emitting forward conversion (line 760, 781)
- Capture cursor BEFORE emitting reverse conversion (line 791, 799)
- Update internal `nodeType` after conversions (line 785, 803)
4. **focus-manager.svelte.ts**
- Added `_nodeTypeConversionCursorPosition` state (line 68)
- Added getter `nodeTypeConversionCursorPosition` (line 112-114)
- Added `setEditingNodeFromTypeConversion()` method (line 151-157)
- Added `clearNodeTypeConversionCursorPosition()` method (line 180-182)
5. **reactive-node-service.svelte.ts**
- Removed `focusManager.setEditingNode()` call from `updateNodeType()` (line 621-622)
- Focus now managed entirely by BaseNodeViewer during conversions
6. **textarea-bidirectional-conversion.test.ts** (NEW)
- 9 comprehensive tests for bidirectional conversion
- Tests controller's internal nodeType tracking
- Tests multiple conversion sequences
---
## Test Results
**All tests passing:**
- 33 tests: header-node-pattern-detection.test.ts ✅
- 58 tests: textarea-controller.test.ts ✅
- 9 tests: textarea-bidirectional-conversion.test.ts ✅
- **Total: 100 tests passing** ✅
---
## For Next AI Agent
**Priority:** Fix the cursor position preservation timing issue
**Starting Point:**
1. Review logs from MCP Chrome DevTools testing (see analysis above)
2. Debug why `conversionCursorPos` is `null` when effect runs
3. Check if old component's effect is clearing the value prematurely
4. Consider using `untrack()` or different reactive primitive
**Success Criteria:**
- Type "## Hello" (cursor at pos 8)
- Backspace space → "##Hello" (cursor should be at pos 2, not pos 0)
- Conversion happens in real-time during editing
- All existing tests continue passing
**Files to focus on:**
- `base-node.svelte` (line 639-653) - The effect that should restore cursor
- `focus-manager.svelte.ts` - Check if state is being cleared prematurely
- Consider: Does effect run too early? Too late? Multiple times?
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: Complete HeaderNode implementation with cursor preservation and persistence (issue #275)
This commit completes the HeaderNode implementation by fixing cursor preservation
during node type conversion and enabling full database persistence support.
## Problems Solved
### 1. Cursor Position Lost During Node Type Conversion
**Problem:** Cursor reset to position 0 when converting between text ↔ header nodes
**Impact:** Broke typing flow when users typed "## " or removed spaces
**Root Causes:**
- Focus manager state set AFTER node type update triggered component re-render
- Effect timing: component switch destroyed old instance before new could consume cursor position
- Missing `focus()` call and retry logic for cursor restoration
**Solution:**
- Call `focusManager.setEditingNodeFromTypeConversion()` BEFORE `nodeManager.updateNodeType()`
- Added `$effect` with `requestAnimationFrame` to restore cursor after component switch
- Retry logic: Verify cursor after 10ms and re-apply if reset by component lifecycle
**Files:**
- `base-node-viewer.svelte:1388-1393`: Reordered focus manager before node update
- `base-node.svelte:640-668`: Added cursor restoration effect with retry
- `focus-manager.svelte.ts:151-157`: Added `setEditingNodeFromTypeConversion()` method
### 2. NodeType Changes Not Persisting to Database
**Problem:** After typing "## Hello", page refresh showed it as text node, not header
**Impact:** Node type conversions were temporary, lost on reload
**Root Causes:**
a) **Conflict Detection:** Second update (nodeType + content) detected as conflicting
with first update (content only), blocked by concurrent edit detection
b) **Backend Validation:** Rust backend rejected nodeType="header" - not registered
c) **Schema Missing:** No "header" schema node seeded in database
d) **Persistence Condition:** nodeType changes not included in `shouldPersist` check
**Solutions:**
- `shared-node-store.ts:285`: Added `isNodeTypeChange` to persistence condition
- `shared-node-store.ts:1038-1041`: Skip conflict detection for nodeType conversions
- `reactive-node-service.svelte.ts:621-623`: Include content with nodeType update, skip conflicts
- `behaviors/mod.rs:362-393`: Added `HeaderNodeBehavior` with validation
- `behaviors/mod.rs:627`: Registered HeaderNodeBehavior in registry
- `database.rs:511-527`: Seeded header schema with headerLevel field
### 3. Header Syntax Visible in Blur Mode
**Problem:** Blur mode showed "## Hello" instead of "Hello"
**Solution:** Added `displayContent` prop to BaseNode for node-specific blur rendering
- `base-node.svelte:62,563`: Added optional `displayContent` prop
- `header-node.svelte:68`: Strip hashtags for blur: `internalContent.replace(/^#{1,6}\s+/, '')`
- `header-node.svelte:126`: Pass displayContent to BaseNode
### 4. Width Overflow Issues
**Problem:** Node containers extended beyond parent boundaries
**Solution:** Added `box-sizing: border-box` to `.node` to include padding in width
- `base-node.svelte:858`: Added box-sizing to prevent padding overflow
- `base-node.svelte:943`: Set blur div to `flex: 1 1 100%` for full width
- `header-node.svelte:157-160`: Ensure header wrapper takes full width
### 5. Multiline Content in Single-Line Headers
**Problem:** Converting multiline text to header kept newlines
**Solution:** HeaderNode strips newlines during content sync
- `header-node.svelte:44-55`: Replace `\n` with spaces, dispatch cleaned content
## Testing
**Existing Tests:** 856 tests passing ✅
- Pattern detection tests (33 tests)
- Bidirectional conversion tests (9 tests)
- Textarea controller tests (58 tests)
- No regressions
## Test Coverage Gaps (Next Agent Tasks)
The following scenarios work correctly but lack automated tests:
### Priority 1: NodeType Persistence Tests
**File to create:** `src/tests/integration/node-type-persistence.test.ts`
```typescript
// Test nodeType persistence through full stack
describe('NodeType Persistence', () => {
test('should persist text → header conversion to database', async () => {
// Create text node with "## Hello"
// Verify nodeManager.updateNodeType called
// Wait for persistence
// Check database has node_type='header'
});
test('should persist header → text conversion to database', async () => {
// Create header node with "## Hello"
// Remove space to trigger conversion
// Check database has node_type='text'
});
test('should load persisted header nodes correctly after page refresh', async () => {
// Persist header node
// Simulate page reload
// Verify loads as header type with correct styling
});
});
```
### Priority 2: Conflict Detection Tests
**File to create:** `src/tests/services/node-type-conflict-detection.test.ts`
```typescript
describe('NodeType Conflict Detection', () => {
test('should bypass conflict detection for nodeType conversions', async () => {
// Create rapid content + nodeType updates
// Verify both persist (no conflict detected)
});
test('should still detect conflicts for regular concurrent edits', async () => {
// Verify normal conflict detection still works
});
});
```
### Priority 3: Blur Mode Rendering Tests
**File to create:** `src/tests/components/header-node-blur-mode.test.ts`
```typescript
describe('HeaderNode Blur Mode', () => {
test('should strip hashtags in blur mode', () => {
// Render HeaderNode with "## Hello"
// Switch to blur mode
// Verify displays "Hello" without ##
});
test('should show hashtags in edit mode', () => {
// Click to edit
// Verify textarea shows "## Hello" with syntax
});
test('should strip newlines when converting to header', () => {
// Convert text with "Line 1\nLine 2" to header
// Verify content becomes "Line 1 Line 2"
});
});
```
### Test Implementation Notes
**Testing Stack:**
- Vitest + Happy-DOM (already configured)
- Use `bunx vitest run` (NOT `bun test`)
- Mock TauriNodeService for database operations
**Key Patterns:**
- Use `sharedNodeStore.updateNode()` to simulate updates
- Mock `tauriNodeService.updateNode()` to verify persistence calls
- Use `vi.spyOn()` to verify method calls without changing behavior
**Files to Reference:**
- `src/tests/components/textarea-bidirectional-conversion.test.ts` - Conversion test patterns
- `src/tests/services/shared-node-store.test.ts` - Persistence test patterns
- `src/tests/components/header-node-pattern-detection.test.ts` - Existing header tests
## Manual Verification Completed
✅ Cursor preserved at correct position (both text → header and header → text)
✅ NodeType persists to database (verified with sqlite3 query)
✅ Page refresh loads as header with correct styling
✅ Blur mode hides ## syntax, edit mode shows it
✅ Width respects container boundaries (measured with getBoundingClientRect)
✅ Newlines stripped when converting to headers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: Add 'header' to ALLOWED_NODE_TYPES and comprehensive test coverage (issue #275)
**Critical Bug Fix:**
- Added "header" to ALLOWED_NODE_TYPES in backend (src-tauri/src/constants.rs:16)
- Backend was rejecting header node creation with HTTP 400 Bad Request
- Bug occurred when pressing Enter from header node to create new header below
- Fix verified: header nodes now persist correctly to database
**Test Coverage Added:**
1. **node-type-persistence.test.ts** (10 tests)
- Create new header node and persist (catches the bug we fixed!)
- Text → Header conversion persistence
- Header → Text conversion persistence
- Conflict detection bypass for nodeType changes
- Database reload verification
2. **node-type-conflict-detection.test.ts** (9 tests)
- Rapid nodeType conversions without conflicts
- Regular conflict detection still works
- Edge cases and performance under load
**Test Results:**
- 873/874 tests passing (99.9%)
- All HeaderNode functionality verified
- Database persistence confirmed with sqlite3 query
- 1 pre-existing failing test in sibling-chain-integrity (unrelated)
**Why This Test Gap Existed:**
Previous tests only covered text→header *conversion*, not creating
header nodes from scratch. The new "Create New Header Node" test
would have caught the missing ALLOWED_NODE_TYPES entry immediately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test: Add new header node creation test and improve timing in rapid conversion test
**Additional Test Coverage:**
- Added test for creating new header nodes from scratch (not just conversions)
- This test would have caught the ALLOWED_NODE_TYPES bug immediately
- Validates that backend accepts header node type on initial creation
**Test Timing Improvements:**
- Added explicit delays between rapid conversion steps
- Ensures proper update ordering and completion
- Makes test more deterministic and less flaky
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #275