Skip to content

feat: Implement TipTap WYSIWYG Editor (Track A - Phase 2)#1

Merged
saadiq merged 1 commit intomainfrom
track-A
Oct 2, 2025
Merged

feat: Implement TipTap WYSIWYG Editor (Track A - Phase 2)#1
saadiq merged 1 commit intomainfrom
track-A

Conversation

@saadiq
Copy link
Copy Markdown
Owner

@saadiq saadiq commented Oct 2, 2025

Summary

Implements Track A of Milestone 1 Phase 2: TipTap WYSIWYG markdown editor with complete formatting capabilities.

This PR adds a fully-featured WYSIWYG editor using TipTap, with:

  • ✅ Live markdown formatting preview
  • ✅ Comprehensive formatting toolbar with DaisyUI components
  • ✅ Keyboard shortcuts for common operations
  • ✅ Auto-formatting with Typography extension
  • ✅ Theme-aware styling (light/dark mode support)
  • ✅ Content update callbacks for file saving integration

Changes

New Files

  • src/renderer/hooks/useEditor.ts - TipTap editor configuration hook
  • src/renderer/components/Editor/EditorComponent.tsx - Main editor component
  • src/renderer/components/Editor/EditorToolbar.tsx - Formatting toolbar with all controls

Modified Files

  • src/renderer/App.tsx - Integration of EditorComponent
  • src/renderer/index.css - ProseMirror editor styling with DaisyUI theme variables
  • package.json - Added TipTap dependencies
  • bun.lock - Lockfile update

Features Implemented

Text Formatting

  • Bold (Cmd+B), Italic (Cmd+I), Strikethrough
  • Inline code (Cmd+E)

Block Formatting

  • Headings H1-H6
  • Ordered and unordered lists
  • Blockquotes
  • Code blocks

Advanced Features

  • Link insertion (Cmd+K)
  • Image insertion
  • Undo/Redo with toolbar buttons
  • Placeholder text
  • Auto-formatting (e.g., **text**text)

Dependencies Added

  • @tiptap/react@^3.6.3
  • @tiptap/starter-kit@^3.6.3
  • @tiptap/extension-typography@^3.6.3
  • @tiptap/extension-placeholder@^3.6.3
  • @tiptap/extension-link@^3.6.3
  • @tiptap/extension-image@^3.6.3

Test Plan

  • App builds and launches successfully
  • Editor renders with placeholder text
  • All toolbar formatting buttons work correctly
  • Keyboard shortcuts function properly
  • Editor content updates trigger onChange callback
  • Styling adapts to DaisyUI theme changes
  • Editor is responsive and scrollable

Integration Notes

This editor component is ready for integration with:

  • Track B (File System): Content can be saved/loaded via onUpdate callback
  • Track C (File Tree): File selection will populate editor content
  • Track D (Layout): Editor fits into main content area

Screenshots

The editor displays with a clean toolbar at the top and a scrollable content area. All DaisyUI button styles and Lucide icons are properly rendered.

Related

  • Closes milestone-1-tasks.md Track A requirements
  • Part of Phase 2 parallel development tracks
  • Depends on Phase 1 completion (Tailwind, DaisyUI, Lucide setup)

Add fully-featured markdown editor with live formatting preview and
comprehensive toolbar controls. This completes Track A of Phase 2
parallel development.

Features:
- TipTap editor integration with StarterKit, Typography, Placeholder,
  Link, and Image extensions
- Custom EditorToolbar with DaisyUI buttons and Lucide icons
- Text formatting: bold, italic, strikethrough, inline code
- Headings (H1-H6), lists (ordered/unordered), blockquotes, code blocks
- Link and image insertion with prompts
- Undo/redo functionality
- Keyboard shortcuts (Cmd+B, Cmd+I, Cmd+K, Cmd+E)
- Auto-formatting with Typography extension
- Theme-aware styling using DaisyUI CSS variables
- Content update callbacks for file saving integration

Files created:
- src/renderer/hooks/useEditor.ts
- src/renderer/components/Editor/EditorComponent.tsx
- src/renderer/components/Editor/EditorToolbar.tsx

Dependencies added:
- @tiptap/react@^3.6.3
- @tiptap/starter-kit@^3.6.3
- @tiptap/extension-typography@^3.6.3
- @tiptap/extension-placeholder@^3.6.3
- @tiptap/extension-link@^3.6.3
- @tiptap/extension-image@^3.6.3
@saadiq saadiq merged commit e63a799 into main Oct 2, 2025
@saadiq saadiq deleted the track-A branch October 3, 2025 02:29
claude Bot pushed a commit that referenced this pull request Oct 4, 2025
…king

Comprehensive fixes for issues identified in PR review:

- Fix #1: Race condition in file switching - synchronous state capture before async operations
- Fix #2: Cache invalidation on external file changes - file watcher now clears stale cache
- Fix #3: Auto-save disabled to prevent conflicts with cache restoration
- Fix #4: Memory leak prevention - cache cleaned on directory change and file watcher events
- Fix #5: Document dirty state persistence behavior (intentionally not persisted)
- Fix #6: Loading gate prevents concurrent file loading operations
- Fix #7: True LRU eviction using timestamps instead of FIFO
- Fix #8: Cache integrity validation via timestamps
- Fix #9: Improved ESLint comment explaining dependency exclusions

Key improvements:
- Added timestamp field to FileContentCache for true LRU and validation
- Added loadingFileRef gate to serialize file loading and prevent race conditions
- Re-enabled file watcher with cache invalidation on external changes
- Updated timestamps on cache access for proper LRU behavior
- Comprehensive documentation in comments explaining design decisions

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>
claude Bot pushed a commit that referenced this pull request Oct 4, 2025
…king

Critical fixes:
- Fix #1: Auto-save race condition - capture file path when setting timer and validate before saving
- Fix #2: Cache invalidation race condition - use synchronous cleanup with validPaths from file tree
- Fix #3: Memory leak - cache now clears when directory closed (rootPath becomes null)

Medium priority fixes:
- Fix #4: Inconsistent dirty state - always clear cache and dirty state for externally modified files
- Fix #5: Loading state error handling - added try/catch with toast notifications for file load errors
- Fix #6: Cache validation error handling - wrapped validation in try/catch with proper error messages

Minor fixes:
- Fix #7: Removed unused cleanupDeletedFilesFromCache function
- Fix #8: Improved ESLint comment to list specific excluded dependencies
- Fix #9: Extracted auto-save delay to AUTO_SAVE_DELAY_MS constant
- Fix #10: Fixed FileWatcherEvent type signature to accept undefined

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>
claude Bot pushed a commit that referenced this pull request Oct 4, 2025
- Fix Critical Issue #1: Remove state.filePath from updateContent dependencies
  - Prevents unnecessary re-creation of updateContent callback
  - Uses currentFilePathRef instead for path validation

- Fix Critical Issue #2: Clear auto-save timer before manual content updates
  - Expose clearAutoSaveTimer from useFileContent hook
  - Call clearAutoSaveTimer before restoring cached content
  - Prevents stale auto-save timers from firing for wrong file

- Remove unused switchingFileRef throughout App.tsx
  - Cleanup of tracking ref that was set but never read

- Set up Vitest testing infrastructure
  - Add vitest.config.ts with React and happy-dom support
  - Create test setup file with Electron API mocks
  - Add App.cache.test.tsx with placeholder tests for cache management
  - Add test scripts to package.json (test, test:ui, test:coverage)
  - Add testing README with setup instructions and patterns

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>
claude Bot pushed a commit that referenced this pull request Oct 4, 2025
- Fix #1: Add getCurrentState() method to useFileContent to avoid stale closures
  in cache state capture during async operations
- Fix #2: Update saveFile to accept pathOverride parameter for auto-save
  validation, ensuring saved content goes to the correct file
- Fix #3: Queue file watcher events during save window instead of dropping
  them, then process queued events after save completes

These fixes prevent data corruption from race conditions in multi-file
editing with auto-save enabled.

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>
saadiq added a commit that referenced this pull request Oct 4, 2025
#7)

* feat: implement multi-file dirty state tracking with visual indicators

Add comprehensive dirty state management across multiple files to support a true multi-document editing experience. Changes persist when switching between files and are visually indicated in the file tree.

Key changes:
- Added dirtyPaths Set to FileTreeState for tracking unsaved changes across all files
- Implemented file content cache in App.tsx to preserve unsaved edits during file switching
- Added setFileDirty action to FileTreeActions for dirty state management
- Enhanced FileTreeItem to display orange dot indicator and italic text for dirty files
- Integrated dirty state synchronization on save operations and file changes
- Cache automatically clears on successful file save

This enables users to work on multiple files simultaneously without losing unsaved changes when switching contexts.

* fix: improve multi-file cache management and dirty state cleanup

- Clear file content cache on directory change to prevent stale data
- Add cache size limit (10 files) with LRU eviction policy
- Clean up dirty paths when files are deleted externally
- Fix race condition by capturing file state synchronously before async operations

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address all code review concerns for multi-file dirty state tracking

Comprehensive fixes for issues identified in PR review:

- Fix #1: Race condition in file switching - synchronous state capture before async operations
- Fix #2: Cache invalidation on external file changes - file watcher now clears stale cache
- Fix #3: Auto-save disabled to prevent conflicts with cache restoration
- Fix #4: Memory leak prevention - cache cleaned on directory change and file watcher events
- Fix #5: Document dirty state persistence behavior (intentionally not persisted)
- Fix #6: Loading gate prevents concurrent file loading operations
- Fix #7: True LRU eviction using timestamps instead of FIFO
- Fix #8: Cache integrity validation via timestamps
- Fix #9: Improved ESLint comment explaining dependency exclusions

Key improvements:
- Added timestamp field to FileContentCache for true LRU and validation
- Added loadingFileRef gate to serialize file loading and prevent race conditions
- Re-enabled file watcher with cache invalidation on external changes
- Updated timestamps on cache access for proper LRU behavior
- Comprehensive documentation in comments explaining design decisions

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address all code review concerns for multi-file dirty state tracking

This commit implements comprehensive fixes for all issues identified in the code review:

Critical Fixes:
- Re-enabled auto-save with cache-aware implementation that prevents saving cached content to wrong files
- Fixed race condition in cache update by adding switchingFileRef to track file switch intent
- Added cache cleanup for deleted files via file watcher validation
- Implemented external file change handling with conflict detection for active file

Medium Priority Fixes:
- Added cache validation on restoration by comparing disk content with cached originalContent
- Implemented loading state (isLoadingFile) during file switches for better UX

Minor Fixes:
- Improved type safety by importing and using FileWatcherEvent type instead of 'any'

Implementation Details:
- Auto-save timer validates file path before saving to prevent wrong-file saves
- Cache restoration now checks if disk content matches cached originalContent
- File watcher detects active file changes and shows conflict warning if dirty
- Cache entries for deleted files are cleaned up asynchronously after directory reload
- Loading spinner shows during file switch operations
- All race conditions addressed with proper ref-based state tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address all code review concerns for multi-file dirty state tracking

Critical fixes:
- Fix #1: Auto-save race condition - capture file path when setting timer and validate before saving
- Fix #2: Cache invalidation race condition - use synchronous cleanup with validPaths from file tree
- Fix #3: Memory leak - cache now clears when directory closed (rootPath becomes null)

Medium priority fixes:
- Fix #4: Inconsistent dirty state - always clear cache and dirty state for externally modified files
- Fix #5: Loading state error handling - added try/catch with toast notifications for file load errors
- Fix #6: Cache validation error handling - wrapped validation in try/catch with proper error messages

Minor fixes:
- Fix #7: Removed unused cleanupDeletedFilesFromCache function
- Fix #8: Improved ESLint comment to list specific excluded dependencies
- Fix #9: Extracted auto-save delay to AUTO_SAVE_DELAY_MS constant
- Fix #10: Fixed FileWatcherEvent type signature to accept undefined

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address critical and medium priority code review concerns

Critical fixes:
- Fix stale closure in auto-save by using currentFilePathRef (useFileContent.ts)
- Fix file watcher closure issues by using refs for activePath, fileContent, and toast (App.tsx)

Medium priority fixes:
- Optimize cache validation to avoid double file reads by reusing diskResult
- Add cache cleanup when reloading active file from disk
- Check all cached files (not just active) in beforeunload handler

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address critical and medium priority code review concerns

- Fix Critical Issue #1: Remove state.filePath from updateContent dependencies
  - Prevents unnecessary re-creation of updateContent callback
  - Uses currentFilePathRef instead for path validation

- Fix Critical Issue #2: Clear auto-save timer before manual content updates
  - Expose clearAutoSaveTimer from useFileContent hook
  - Call clearAutoSaveTimer before restoring cached content
  - Prevents stale auto-save timers from firing for wrong file

- Remove unused switchingFileRef throughout App.tsx
  - Cleanup of tracking ref that was set but never read

- Set up Vitest testing infrastructure
  - Add vitest.config.ts with React and happy-dom support
  - Create test setup file with Electron API mocks
  - Add App.cache.test.tsx with placeholder tests for cache management
  - Add test scripts to package.json (test, test:ui, test:coverage)
  - Add testing README with setup instructions and patterns

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address high priority code review concerns

- Add useEffect cleanup for saveTimeoutRef to prevent memory leak
- Implement comprehensive cache management tests (not placeholders)
- Add tests for cache invalidation, dirty state sync, auto-save timer management
- Add tests for loading states and memory leak prevention
- Update README with complete test dependency list

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

* fix: address critical race conditions and file watcher issues

- Fix #1: Add getCurrentState() method to useFileContent to avoid stale closures
  in cache state capture during async operations
- Fix #2: Update saveFile to accept pathOverride parameter for auto-save
  validation, ensuring saved content goes to the correct file
- Fix #3: Queue file watcher events during save window instead of dropping
  them, then process queued events after save completes

These fixes prevent data corruption from race conditions in multi-file
editing with auto-save enabled.

Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
saadiq added a commit that referenced this pull request Oct 4, 2025
Addresses two critical issues in the auto-save system:

1. Memory Leak (#1): Cache entries were accumulating indefinitely after
   auto-save operations. Added cleanup in handleAfterSave callback to
   delete cache entries and clear dirty state after successful saves.

2. Silent Auto-Save Skips (#2): When users switched files during the
   auto-save debounce period, saves were silently skipped with no
   visibility. Added console logging to track when auto-saves are
   cancelled due to file switching.

Changes:
- App.tsx: Clear cache and dirty state in handleAfterSave after success
- App.tsx: Add setFileDirty to useCallback dependency array
- useFileContent.ts: Log auto-save skips when file path changes

This ensures memory doesn't grow unbounded during long editing sessions
and provides debugging visibility for auto-save behavior.
saadiq added a commit that referenced this pull request Oct 8, 2025
This commit fixes both critical auto-save issues based on debug log analysis:

## Issue #1: External Edit Warnings - Debounce Window Too Short

**Root cause (from logs):**
```
[SaveTracking]   Age: 2164 ms
[SaveTracking]   Debounce window: 2000 ms
[SaveTracking]   Result: FALSE (too old)
```

File watcher events on macOS arrive 2000-2500ms after save completion,
missing the 2000ms window by 19-164ms.

**Fix:**
- Increased FILE_WATCHER_DEBOUNCE_MS from 2000ms → 3000ms
- Location: src/shared/config/timing.ts:21
- This accounts for macOS filesystem latency and disk I/O buffering

## Issue #2: Cursor Jumping - setContent Called on Every Keystroke

**Root cause (from logs):**
```
[EditorComponent] hasContentChanged: true
[EditorComponent] content length: 343
[EditorComponent] prev content length: 342
[EditorComponent] CALLING setContent (WYSIWYG content change)
```

Every keystroke triggered: user types → onUpdate → updateContent → content prop changes → EditorComponent useEffect sees hasContentChanged=true → calls setContent → cursor resets.

This is a circular update problem:
1. User types in TipTap editor
2. TipTap fires onUpdate with new content
3. handleUpdate calls updateContent, updating React state
4. EditorComponent re-renders with new content prop
5. useEffect detects content change and calls setContent ← **CURSOR RESET**
6. Editor now has same content but cursor position is lost

**Fix:**
- Added isUserEditingRef flag to track when content change originates from user typing
- wrappedOnUpdate sets flag=true, calls onUpdate, resets flag after 10ms
- useEffect checks flag and skips setContent if user is actively editing
- Location: src/renderer/components/Editor/EditorComponent.tsx:157-248

**How it works:**
1. User types → onUpdate fires
2. wrappedOnUpdate sets isUserEditingRef.current = true
3. Calls parent's onUpdate → content state updates
4. useEffect runs, sees isUserEditingRef = true
5. Skips setContent call → cursor preserved
6. Flag resets after 10ms to allow external updates

**Files modified:**
- src/shared/config/timing.ts: Increase debounce window to 3000ms
- src/renderer/components/Editor/EditorComponent.tsx: Add isUserEditingRef guard

Resolves cursor jumping on every keystroke and false external edit warnings.
@claude claude Bot mentioned this pull request Oct 8, 2025
9 tasks
saadiq added a commit that referenced this pull request Oct 8, 2025
* fix: address high/medium priority auto-save issues

High Priority Fixes:
- Fix race condition in updateContent auto-save by adding contentRef to track
  latest content synchronously, preventing data loss from rapid typing
- Add clearAutoSaveTimer() to saveFile method to prevent duplicate saves
  when user manually saves (Cmd+S)

Medium Priority Fixes:
- Improve blur handler save tracking consistency by moving trackSaveStart
  before write operation and adding proper trackSaveEnd on all code paths
- Move module-level state to component refs (activeSaves Map and cleanup
  interval) and convert tracking functions to useCallback hooks to prevent
  potential HMR issues and memory leaks

All tests passing (13/13 in useFileContent.test.ts).

* fix: resolve auto-save cursor jump and false external edit warnings

This commit fixes two critical issues in the auto-save implementation:

1. **Cursor jumping during auto-save**
   - Root cause: handleContentLoaded was calling updateContent() even when
     content hadn't changed, triggering unnecessary re-renders
   - Fix: Added content comparison in handleContentLoaded to only update
     when content actually differs
   - Location: src/renderer/App.tsx:204-216

2. **False "external edit" warnings**
   - Root cause 1: Path variable mismatch (currentActivePath vs fileContent.filePath)
   - Root cause 2: trackSaveEnd being called too early (50ms), before file
     watcher events arrived (500-2000ms typical on macOS)
   - Fix: Use consistent path variable (fileContent.filePath) and remove
     all trackSaveEnd calls to let timestamps persist for full 2000ms window
   - Location: src/renderer/App.tsx:488-494, 192-196, 454-469

**Architecture changes:**
- Removed module-level save tracking functions (trackSaveStart, isSaveActive,
  trackSaveEnd) in favor of component-level implementations
- Removed module-level activeSaves Map - now managed per-component instance
- Simplified save tracking: timestamps persist for full debounce window,
  cleaned up eagerly when map exceeds 50 entries
- Exported hashString from useEditor for content comparison

**Files modified:**
- src/renderer/App.tsx: Save tracking refactor, cursor jump fix
- src/renderer/components/Editor/EditorComponent.tsx: Minor documentation
- src/renderer/hooks/useEditor.ts: Export hashString utility

Fixes issues reported in PR review where cursor would jump to end of
document during auto-save and users would see spurious "file modified
externally" warnings for their own saves.

* fix: add onBeforeSave callback and prevent spurious setContent calls

This commit fixes two critical auto-save issues:

1. **External edit warnings for auto-saves** (CRITICAL BUG FIX)
   - Root cause: Auto-save in useFileContent.ts was NOT calling onBeforeSave(),
     so trackSaveStart() never executed for auto-saves
   - Fix: Added onBeforeSave?.() call at line 331 before initiating save
   - Impact: File watcher now correctly ignores auto-save events via
     timestamp checking in isSaveActive()
   - Location: src/renderer/hooks/useFileContent.ts:330-331

2. **Cursor jumping during auto-save** (CRITICAL BUG FIX)
   - Root cause: EditorComponent useEffect was calling setContent even when
     neither viewMode nor content had changed, likely due to state updates
     from auto-save triggering effect re-evaluation
   - Fix: Added early return when !isViewModeTransition && !hasContentChanged
     to prevent any setContent calls when nothing actually changed
   - Impact: Cursor position preserved during auto-save
   - Location: src/renderer/components/Editor/EditorComponent.tsx:210-216

**Technical details:**

Auto-save flow (before fix):
1. User types → updateContent() called → auto-save timer starts (300ms)
2. Timer fires → writeFile() called → file saved
3. State updated: isDirty=false, originalContent updated
4. ❌ onBeforeSave NOT called → trackSaveStart never executed
5. File watcher event arrives 500-2000ms later
6. isSaveActive(filePath) returns false → shows external edit warning

Auto-save flow (after fix):
1. User types → updateContent() called → auto-save timer starts (300ms)
2. Timer fires → onBeforeSave() called → trackSaveStart() executed
3. writeFile() called → file saved
4. State updated: isDirty=false, originalContent updated
5. File watcher event arrives 500-2000ms later
6. ✅ isSaveActive(filePath) returns true → event ignored

Cursor jump flow (before fix):
1. Auto-save completes → state updates (isDirty, saving, originalContent)
2. EditorComponent re-renders with same content prop
3. useEffect runs because dependencies include viewMode, editor, content
4. hasContentChanged = false BUT useEffect still processes
5. ❌ Calls setContent anyway → cursor resets

Cursor jump flow (after fix):
1. Auto-save completes → state updates (isDirty, saving, originalContent)
2. EditorComponent re-renders with same content prop
3. useEffect runs because dependencies include viewMode, editor, content
4. ✅ Early return: !isViewModeTransition && !hasContentChanged
5. No setContent call → cursor position preserved

**Files modified:**
- src/renderer/hooks/useFileContent.ts: Add onBeforeSave() call, add to deps
- src/renderer/components/Editor/EditorComponent.tsx: Add early return guard

Resolves cursor jumping and false external edit warnings reported in testing.

* debug: add comprehensive logging for auto-save cursor jump investigation

Added detailed console logging throughout the auto-save and content sync flow
to diagnose why fixes aren't working:

**Auto-save logging (useFileContent.ts):**
- Log when auto-save starts, file path, content length
- Log when onBeforeSave callback is called
- Log file write success/failure
- Log when onAfterSave is called

**Save tracking logging (App.tsx):**
- trackSaveStart: Log file path normalization and timestamp storage
- isSaveActive: Log file path, timestamp lookup, age calculation, result
- handleBeforeSave: Log when called and current file path
- File watcher: Log all events, path comparisons, isSaveActive checks

**Editor sync logging (EditorComponent.tsx):**
- Log every useEffect trigger with view mode and content change status
- Log when early return prevents sync
- Log every setContent call with stack trace
- Log hash comparisons when checking if content actually changed

**Content loaded logging (App.tsx):**
- Log when handleContentLoaded is called
- Log content lengths and isDirty state
- Log when updateContent is called (causes cursor jump)
- Add stack traces to trace call origin

This will reveal:
1. Whether onBeforeSave is actually being called during auto-save
2. Whether trackSaveStart receives the correct file path
3. Whether isSaveActive returns true/false and why
4. What triggers setContent during auto-save
5. Whether handleContentLoaded is being called unexpectedly

Next step: Test auto-save scenario and analyze console output.

* fix: prevent cursor jump and extend debounce window for macOS

This commit fixes both critical auto-save issues based on debug log analysis:

## Issue #1: External Edit Warnings - Debounce Window Too Short

**Root cause (from logs):**
```
[SaveTracking]   Age: 2164 ms
[SaveTracking]   Debounce window: 2000 ms
[SaveTracking]   Result: FALSE (too old)
```

File watcher events on macOS arrive 2000-2500ms after save completion,
missing the 2000ms window by 19-164ms.

**Fix:**
- Increased FILE_WATCHER_DEBOUNCE_MS from 2000ms → 3000ms
- Location: src/shared/config/timing.ts:21
- This accounts for macOS filesystem latency and disk I/O buffering

## Issue #2: Cursor Jumping - setContent Called on Every Keystroke

**Root cause (from logs):**
```
[EditorComponent] hasContentChanged: true
[EditorComponent] content length: 343
[EditorComponent] prev content length: 342
[EditorComponent] CALLING setContent (WYSIWYG content change)
```

Every keystroke triggered: user types → onUpdate → updateContent → content prop changes → EditorComponent useEffect sees hasContentChanged=true → calls setContent → cursor resets.

This is a circular update problem:
1. User types in TipTap editor
2. TipTap fires onUpdate with new content
3. handleUpdate calls updateContent, updating React state
4. EditorComponent re-renders with new content prop
5. useEffect detects content change and calls setContent ← **CURSOR RESET**
6. Editor now has same content but cursor position is lost

**Fix:**
- Added isUserEditingRef flag to track when content change originates from user typing
- wrappedOnUpdate sets flag=true, calls onUpdate, resets flag after 10ms
- useEffect checks flag and skips setContent if user is actively editing
- Location: src/renderer/components/Editor/EditorComponent.tsx:157-248

**How it works:**
1. User types → onUpdate fires
2. wrappedOnUpdate sets isUserEditingRef.current = true
3. Calls parent's onUpdate → content state updates
4. useEffect runs, sees isUserEditingRef = true
5. Skips setContent call → cursor preserved
6. Flag resets after 10ms to allow external updates

**Files modified:**
- src/shared/config/timing.ts: Increase debounce window to 3000ms
- src/renderer/components/Editor/EditorComponent.tsx: Add isUserEditingRef guard

Resolves cursor jumping on every keystroke and false external edit warnings.

* fix: track editor-originated content to prevent cursor jump on first keystroke

The previous isUserEditingRef approach with setTimeout didn't work because:
- React state updates propagate synchronously
- useEffect runs before the setTimeout (10ms) fires
- Cursor still jumped on first character typed

New approach:
- Track the last content that originated from the editor in lastEditorContentRef
- When content prop changes, check if it matches lastEditorContentRef
- If it matches, skip setContent() call - this content came FROM the editor
- Only call setContent() for external content changes (file loads, etc.)

This prevents the circular flow:
1. User types 'x' → onUpdate fires with content
2. wrappedOnUpdate stores content in lastEditorContentRef
3. Parent state updates with content
4. EditorComponent re-renders with new content prop
5. useEffect checks: content === lastEditorContentRef.current
6. TRUE → skip setContent() → cursor preserved ✓

Location: src/renderer/components/Editor/EditorComponent.tsx:157-244

This fix specifically addresses the issue where cursor jumps to end of
file immediately after typing the first character.

* fix: prevent cursor jump on typing by tracking editor-originated content

Root cause: useEditor's useEffect was calling setContent() on EVERY keystroke
because the markdown content coming back via props had a different hash than
the previously converted markdown (TipTap's HTML→markdown conversion isn't
perfectly stable).

Fix:
- Track markdown sent via onUpdate in lastOnUpdateContentRef
- Skip setContent() if incoming content matches lastOnUpdateContentRef
- This breaks the circular update loop: type → onUpdate → parent state →
  useEffect → setContent → cursor reset

Also increased FILE_WATCHER_DEBOUNCE_MS from 4000ms to 5000ms since macOS
file watcher events can arrive at 4025ms (25ms over the window).

Removed all debug logging added during investigation.

* refactor: implement PR #25 review recommendations for auto-save

Addresses all feedback from PR #25 code review to improve auto-save
robustness, error handling, and code quality:

- Add user-facing error toast for auto-save failures via onError callback
- Add race condition check in auto-save timer to prevent wrong-file writes
- Remove unused _success parameter from handleAfterSave callback
- Clean up eslint-disable comment with better explanation about ref stability
- Document FILE_WATCHER_DEBOUNCE_MS UX tradeoff for collaborative editing

These changes improve user experience by surfacing auto-save errors and
preventing edge case bugs where rapid file switching could cause content
to be saved to the wrong file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant