Conversation
WalkthroughRefactors the event-driven layout glue into a Hybrid Action Hooks pattern: adds Changes
Sequence Diagram(s)sequenceDiagram
participant Layout as Layout component
participant Store as Editor Store
participant Hook as useEditorActions
participant Query as QueryClient
participant Backend as Backend (Tauri)
Note over Layout,Hook: initial wiring
Layout->>Hook: useEditorActions() -> saveFile
Layout->>Store: setAutoSaveCallback(saveFile)
Note over Layout,Store: Trigger paths
alt User action (Ctrl+S / explicit)
Layout->>Hook: saveFile(showToast=true)
else Auto-save timer triggers
Store->>Store: invoke autoSaveCallback(showToast=false)
end
Hook->>Store: getState() (currentFile, editorContent, frontmatter, imports)
Hook->>Query: getQueryData() (schema/order)
Hook->>Backend: save_markdown_content(...)
alt Success
Backend-->>Hook: ok
Hook->>Store: update isDirty=false, lastSaveTimestamp
Hook->>Query: invalidate related queries
Hook->>Layout: optional success toast
else Failure
Backend-->>Hook: error
Hook->>Backend: persist recovery data / log crash
Hook->>Layout: show error toast
Hook->>Store: keep isDirty=true
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Potential focus areas:
Possibly related issues
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/store/__tests__/editorStore.integration.test.ts (1)
31-42: ResetautoSaveCallbackbetween testsWe inject
autoSaveCallbackmocks in multiple specs, but the suite’sbeforeEachreset never clears that field, so whatever mock the previous test left behind persists into the next run. Because the store methods callautoSaveCallbackduring content changes, a leaked mock will cause follow-up tests that expect a null callback to behave unpredictably. Please null the callback in the shared reset block to keep the store state deterministic between tests.useEditorStore.setState({ currentFile: null, editorContent: '', frontmatter: {}, rawFrontmatter: '', imports: '', isDirty: false, autoSaveTimeoutId: null, lastSaveTimestamp: null, + autoSaveCallback: null, })Also applies to: 81-103
🧹 Nitpick comments (4)
src/components/frontmatter/FrontmatterPanel.tsx (1)
171-183: Consider memoizing the filtered top-level fields.The top-level fields from
groupedFields.get(null)are filtered twice: once for schema fields (line 126-128) and once for non-schema fields (line 172-174). Whilst the performance impact is negligible for typical field counts, you could memoize both filtered arrays for clarity.Apply this diff to memoize the filtered lists:
// Group fields by parent path for nested object rendering const groupedFields = React.useMemo(() => { const groups: Map<string | null, typeof allFields> = new Map() for (const field of allFields) { const parentPath = field.schemaField?.parentPath ?? null if (!groups.has(parentPath)) { groups.set(parentPath, []) } groups.get(parentPath)!.push(field) } return groups }, [allFields]) + + // Pre-filter top-level fields for rendering + const topLevelSchemaFields = React.useMemo(() => + groupedFields.get(null)?.filter(({ schemaField }) => schemaField !== undefined) ?? [], + [groupedFields] + ) + + const topLevelExtraFields = React.useMemo(() => + groupedFields.get(null)?.filter(({ schemaField }) => schemaField === undefined) ?? [], + [groupedFields] + ) return ( <div className="h-full flex flex-col"> <div className="flex-1 p-4 overflow-y-auto"> {currentFile ? ( allFields.length > 0 ? ( <div className="space-y-6"> {/* Render top-level SCHEMA fields only */} - {groupedFields - .get(null) - ?.filter(({ schemaField }) => schemaField !== undefined) - .map(({ fieldName, schemaField }) => ( + {topLevelSchemaFields.map(({ fieldName, schemaField }) => ( <FrontmatterField key={fieldName} name={fieldName} label={camelCaseToTitleCase(fieldName)} field={schemaField} collectionName={currentFile.collection} /> ))} {/* Render nested field groups */} {Array.from(groupedFields.entries()) .filter(([parentPath]) => parentPath !== null) .map(([parentPath, fields]) => ( <div key={parentPath} className="space-y-4"> {/* Parent section header */} <h3 className="text-sm font-medium text-foreground pt-2"> {camelCaseToTitleCase( parentPath!.split('.').pop() || parentPath! )} </h3> {/* Nested fields with indentation */} <div className="pl-4 border-l-2 border-border space-y-4"> {fields.map(({ fieldName, schemaField }) => ( <FrontmatterField key={fieldName} name={fieldName} label={ schemaField?.label || camelCaseToTitleCase( fieldName.split('.').pop() || fieldName ) } field={schemaField} collectionName={currentFile.collection} /> ))} </div> </div> ))} {/* Render extra fields (not in schema) at the very end, alphabetically */} - {groupedFields - .get(null) - ?.filter(({ schemaField }) => schemaField === undefined) - .map(({ fieldName, schemaField }) => ( + {topLevelExtraFields.map(({ fieldName, schemaField }) => ( <FrontmatterField key={fieldName} name={fieldName} label={camelCaseToTitleCase(fieldName)} field={schemaField} collectionName={currentFile.collection} /> ))} </div>docs/tasks-todo/task-x-relative-paths-for-images.md (1)
1-284: Excellent technical analysis and planning documentation.This document provides a thorough examination of the relative paths issue, including Astro's expectations, current implementation analysis, multiple solution options with trade-offs, and comprehensive edge case analysis. The implementation guidance with code sketches is particularly helpful.
Optional: Wrap the bare URL at line 3 in markdown link syntax for consistency:
-https://github.com/dannysmith/astro-editor/issues/53 +[Issue #53](https://github.com/dannysmith/astro-editor/issues/53)src/components/layout/Layout.tsx (1)
55-58: Clear auto-save callback on unmountWe set the store callback but never clear it, so the store can keep invoking a stale
saveFileclosure after the layout unmounts. Reset it in the effect cleanup to avoid dangling callbacks.- useEffect(() => { - useEditorStore.getState().setAutoSaveCallback(saveFile) - }, [saveFile]) + useEffect(() => { + useEditorStore.getState().setAutoSaveCallback(saveFile) + return () => { + useEditorStore.getState().setAutoSaveCallback(null) + } + }, [saveFile])src/hooks/useDOMEventListeners.ts (1)
24-35: Bring the comment back in sync with behaviourThe header comment still says we no longer handle
'create-new-file', but the listener below remains active for that event. Updating the prose to match reality will save future readers a head scratch.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
CLAUDE.md(1 hunks)docs/developer/architecture-guide.md(1 hunks)docs/developer/state-management.md(1 hunks)docs/tasks-done/task-2025-11-05-event-bridge-refactor.md(4 hunks)docs/tasks-done/task-2025-11-06-god-hook.md(5 hunks)docs/tasks-todo/task-x-decompose-pos-decorations.md(0 hunks)docs/tasks-todo/task-x-relative-paths-for-images.md(1 hunks)src/components/frontmatter/FrontmatterPanel.tsx(2 hunks)src/components/layout/Layout.tsx(4 hunks)src/hooks/commands/useCommandContext.ts(2 hunks)src/hooks/editor/useEditorActions.ts(1 hunks)src/hooks/editor/useEditorHandlers.test.ts(2 hunks)src/hooks/useDOMEventListeners.ts(1 hunks)src/hooks/useEditorFocusTracking.ts(1 hunks)src/hooks/useKeyboardShortcuts.ts(1 hunks)src/hooks/useLayoutEventListeners.ts(0 hunks)src/hooks/useMenuEvents.ts(1 hunks)src/hooks/useProjectInitialization.ts(1 hunks)src/hooks/useRustToastBridge.ts(1 hunks)src/store/__tests__/editorStore.integration.test.ts(5 hunks)src/store/__tests__/storeQueryIntegration.test.ts(1 hunks)src/store/editorStore.ts(6 hunks)
💤 Files with no reviewable changes (2)
- docs/tasks-todo/task-x-decompose-pos-decorations.md
- src/hooks/useLayoutEventListeners.ts
🧰 Additional context used
🧬 Code graph analysis (11)
src/components/frontmatter/FrontmatterPanel.tsx (2)
src/components/frontmatter/fields/FrontmatterField.tsx (1)
FrontmatterField(23-235)src/lib/utils.ts (1)
camelCaseToTitleCase(9-20)
src/hooks/useMenuEvents.ts (3)
src/lib/projects/actions.ts (1)
openProjectViaDialog(5-18)src/store/editorStore.ts (1)
useEditorStore(38-203)src/store/uiStore.ts (1)
useUIStore(24-77)
src/store/__tests__/storeQueryIntegration.test.ts (1)
src/store/editorStore.ts (1)
useEditorStore(38-203)
src/hooks/useEditorFocusTracking.ts (1)
src/store/editorStore.ts (1)
useEditorStore(38-203)
src/hooks/editor/useEditorActions.ts (6)
src/store/editorStore.ts (1)
useEditorStore(38-203)src/store/projectStore.ts (1)
useProjectStore(46-428)src/types/domain.ts (1)
Collection(66-78)src/lib/query-keys.ts (1)
queryKeys(3-40)src/lib/schema.ts (1)
deserializeCompleteSchema(86-146)src/lib/recovery/index.ts (2)
saveRecoveryData(10-34)saveCrashReport(39-61)
src/hooks/commands/useCommandContext.ts (2)
src/lib/commands/types.ts (1)
CommandContext(8-43)src/hooks/editor/useEditorActions.ts (1)
useEditorActions(27-143)
src/hooks/useRustToastBridge.ts (1)
src/lib/rust-toast-bridge.ts (1)
initializeRustToastBridge(41-72)
src/hooks/useDOMEventListeners.ts (4)
src/store/uiStore.ts (1)
useUIStore(24-77)src/store/projectStore.ts (1)
useProjectStore(46-428)src/lib/project-registry/index.ts (1)
updateGlobalSettings(261-280)src/lib/editor/extensions/copyedit-mode.ts (1)
updateCopyeditModePartsOfSpeech(533-550)
src/store/__tests__/editorStore.integration.test.ts (1)
src/store/editorStore.ts (1)
useEditorStore(38-203)
src/hooks/useKeyboardShortcuts.ts (5)
src/hooks/editor/useEditorActions.ts (1)
useEditorActions(27-143)src/store/editorStore.ts (1)
useEditorStore(38-203)src/store/uiStore.ts (1)
useUIStore(24-77)src/store/projectStore.ts (1)
useProjectStore(46-428)src/lib/focus-utils.ts (1)
focusEditor(9-37)
src/components/layout/Layout.tsx (10)
src/lib/focus-utils.ts (1)
focusEditor(9-37)src/hooks/editor/useEditorActions.ts (1)
useEditorActions(27-143)src/hooks/useCreateFile.ts (1)
useCreateFile(51-260)src/store/editorStore.ts (1)
useEditorStore(38-203)src/hooks/useProjectInitialization.ts (1)
useProjectInitialization(10-14)src/hooks/useRustToastBridge.ts (1)
useRustToastBridge(10-22)src/hooks/useEditorFocusTracking.ts (1)
useEditorFocusTracking(15-41)src/hooks/useKeyboardShortcuts.ts (1)
useKeyboardShortcuts(21-110)src/hooks/useMenuEvents.ts (1)
useMenuEvents(38-125)src/hooks/useDOMEventListeners.ts (1)
useDOMEventListeners(36-187)
🪛 LanguageTool
docs/tasks-done/task-2025-11-06-god-hook.md
[misspelling] ~224-~224: Did you mean the phrasal verb “clean up” instead of the noun ‘cleanup’?
Context: ... now 451 lines (reduced from 486 due to cleanup) - ✅ Still manages keyboard shortcuts, ...
(CLEAN_UP)
docs/tasks-todo/task-x-relative-paths-for-images.md
[uncategorized] ~8-~8: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ... in the astro project) 2. The resultant markdown tag or frontmatter field is generated a...
(MARKDOWN_NNP)
[style] ~26-~26: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...nents intended for use in MDX files. 3. Where to put images & files "uploaded" to the...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~34-~34: The word ‘gonna’ is informal.
Context: ...age.astro), relative paths are probably gonna be harder to handle reliably, in which ...
(GONNA)
[style] ~43-~43: The word ‘kinda’ is informal. Consider replacing it.
Context: ...othing. ## Similar Problem? There's a kinda similar issue with the Component Builde...
(KINDA)
[style] ~43-~43: Qualifiers like “somewhat” can weaken your message and make your writing sound uncertain. Consider removing it or choosing an alternative to sound more confident.
Context: ...ow the frontmatter. While this would be somewhat trivial to do, it doesn't feel like a _...
(SOMEWHAT)
[uncategorized] ~53-~53: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...tro's default expectation for images in markdown:** ```markdown <!-- Relative to current...
(MARKDOWN_NNP)
[typographical] ~187-~187: If specifying a range, consider using an en dash instead of a hyphen.
Context: ... already adapting) Estimated work: 2-4 hours including testing ### Key Gotcha...
(HYPHEN_TO_EN)
[uncategorized] ~260-~260: Possible missing comma found.
Context: ...urrently uses resolve_image_path Rust command which handles absolute paths. **Fix Re...
(AI_HYDRA_LEO_MISSING_COMMA)
[grammar] ~270-~270: An article may be missing.
Context: ...es (confirmed by docs) 2. Better UX for majority of users (no Astro config required) 3. Imp...
(BUNCH_OF)
docs/tasks-done/task-2025-11-05-event-bridge-refactor.md
[misspelling] ~278-~278: This word is normally spelled as one.
Context: ...Create focused hooks to avoid god hook anti-pattern* (coordinates with Task 2): 1. **Crea...
(EN_COMPOUNDS_ANTI_PATTERN)
[typographical] ~315-~315: If specifying a range, consider using an en dash instead of a hyphen.
Context: ...uces its scope for Task 2) Effort: 3-4 hours ### Phase 2: Update Store for Au...
(HYPHEN_TO_EN)
[typographical] ~342-~342: If specifying a range, consider using an en dash instead of a hyphen.
Context: ...rStore.ts` (lines 115-161) Effort: 1-2 hours ### Phase 3: Update Call Sites ...
(HYPHEN_TO_EN)
[typographical] ~351-~351: If specifying a range, consider using an en dash instead of a hyphen.
Context: ...erPanel.tsx` (lines 42-77) Effort: 1-2 hours ### Phase 4: Documentation & Tes...
(HYPHEN_TO_EN)
[typographical] ~385-~385: If specifying a range, consider using an en dash instead of a hyphen.
Context: ...n.test.ts` (lines 334-352) Effort: 2-3 hours ### Phase 5: Clean Up 1. Remove...
(HYPHEN_TO_EN)
🪛 markdownlint-cli2 (0.18.1)
docs/tasks-todo/task-x-relative-paths-for-images.md
3-3: Bare URL used
(MD034, no-bare-urls)
207-207: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
266-266: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
docs/developer/state-management.md
531-531: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (rust)
🔇 Additional comments (12)
src/components/frontmatter/FrontmatterPanel.tsx (1)
125-137: LGTM! Clear separation of schema fields.The filtering logic correctly isolates top-level schema-defined fields for rendering first, improving the organisation of the frontmatter panel.
CLAUDE.md (1)
256-312: Excellent documentation of the Hybrid Action Hooks Pattern.The documentation clearly explains the architectural pattern with practical examples showing the separation of user-triggered actions (hook-based) and state-triggered actions (store-based callbacks). The three-part example demonstrating hook, store, and layout wiring is particularly helpful for understanding the complete flow.
src/hooks/commands/useCommandContext.ts (1)
5-5: LGTM! Correct implementation of the Hybrid Action Hooks pattern.The change correctly moves
saveFilefrom the store to theuseEditorActionshook, enabling direct query access as documented in the architectural pattern. The comment at line 26 appropriately references the pattern for future maintainers.Also applies to: 13-13, 26-27, 50-50
src/hooks/useRustToastBridge.ts (1)
1-22: LGTM! Correctly implements React hook patterns for Rust bridge initialization.The hook properly:
- Initializes the Rust toast bridge on mount
- Stores the cleanup function via promise chaining
- Invokes cleanup on unmount using optional chaining
- Uses
voidfor the async call in useEffect (correct pattern)src/hooks/useProjectInitialization.ts (1)
1-14: LGTM! Correct implementation of one-time project initialization.The hook appropriately:
- Uses
getState()to access the store action without subscribing- Initializes project state once on mount (empty dependency array)
- Uses
voidfor the async call in useEffectsrc/hooks/editor/useEditorHandlers.test.ts (1)
40-41: LGTM! Mock state correctly extended for new store API.The mock store state additions for
autoSaveCallbackandsetAutoSaveCallbackalign with the new Hybrid Action Hooks pattern, ensuring tests can properly interact with the updated store interface.Also applies to: 71-72
src/store/__tests__/storeQueryIntegration.test.ts (1)
333-374: LGTM! Test correctly adapted to callback-based save pattern.The mock save callback properly simulates the production save flow:
- Invokes the Tauri command with correct parameters
- Updates
isDirtyandlastSaveTimestampon success- Preserves
isDirtyon error without throwingThe setup and teardown correctly manage the callback lifecycle, aligning with the Hybrid Action Hooks pattern.
src/hooks/useKeyboardShortcuts.ts (1)
26-46: Good call keeping callbacks freshStoring
saveFileandonOpenPreferencesin refs before wiring the hotkeys neatly avoids stale closures while keeping the hook side-effect free. Nicely done.src/hooks/useMenuEvents.ts (1)
52-124: Tidy lifecycle managementI appreciate how the listener setup batches all unlisten handles and guarantees clean teardown on unmount—this keeps Tauri menu wiring leak-free.
src/hooks/editor/useEditorActions.ts (1)
43-112: Schema-aware save flow looks solidPulling the schema ordering straight from the query cache and invalidating the relevant keys after a successful
invokekeeps the editor state and cached data perfectly aligned—great upgrade over the old event bridge.docs/tasks-done/task-2025-11-05-event-bridge-refactor.md (2)
1-30: Comprehensive architectural documentation with clear problem framing. The document correctly establishes thatuseCreateFilealready implements the proposed Hybrid Action Hooks pattern in production, framing this refactor as extending consistency rather than introducing new patterns. This contextualisation strengthens the proposal.
268-276: Task decomposition clearly coordinates with god hook elimination. The note at line 267 explicitly links this refactor to Task 2 (God Hook Decomposition), and Phase 1 at lines 278–315 correctly keepsuseLayoutEventListenersfor wiring only, reducing its scope. This coordination is well-executed.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/hooks/useFileChangeHandler.ts (1)
45-56: Extract the 3-second magic number to a named constant.The race condition protection logic is working correctly—
lastSaveTimestampis properly updated after save operations (inuseEditorActions.ts:85). However, the hardcoded3000value should be extracted to a named constant for maintainability.Apply this diff:
+// Time window (in milliseconds) to ignore external file changes after our own save +const SAVE_DEBOUNCE_WINDOW_MS = 3000 + export function useFileChangeHandler() { useEffect(() => { const handleFileChanged = (event: Event) => { // ... if (lastSaveTimestamp) { const timeSinceLastSave = Date.now() - lastSaveTimestamp - if (timeSinceLastSave < 3000) { + if (timeSinceLastSave < SAVE_DEBOUNCE_WINDOW_MS) { void debug( `Ignoring external change to ${path} - file was recently saved (${timeSinceLastSave}ms ago)` )src/components/layout/StatusBar.tsx (1)
20-33: Consider addressing the root cause rather than polling.The 500ms polling introduces up to half a second of lag before word counts and the dirty indicator update, and it runs continuously even when the editor is idle. The comment suggests this is to avoid re-renders that interrupt CSS transitions, but polling merely delays re-renders rather than preventing them.
Consider investigating the underlying CSS transition issue:
- Use
React.useTransitionorReact.startTransitionto mark updates as non-urgent- Leverage CSS
transition-behaviorto control interruption behaviour- Or adopt a transition library (framer-motion, react-transition-group) that handles state changes gracefully
Polling is generally an anti-pattern in React applications with reactive state management.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/editor/Editor.tsx(1 hunks)src/components/layout/StatusBar.test.tsx(4 hunks)src/components/layout/StatusBar.tsx(2 hunks)src/components/layout/UnifiedTitleBar.tsx(2 hunks)src/hooks/useFileChangeHandler.ts(2 hunks)src/store/uiStore.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/layout/StatusBar.tsx (2)
src/store/uiStore.ts (1)
useUIStore(25-83)src/store/editorStore.ts (1)
useEditorStore(38-203)
src/hooks/useFileChangeHandler.ts (1)
src/store/editorStore.ts (1)
useEditorStore(38-203)
🔇 Additional comments (8)
src/components/editor/Editor.tsx (1)
172-172: LGTM!The comment accurately describes the behaviour and improves code readability.
src/hooks/useFileChangeHandler.ts (1)
28-29: LGTM!Correctly reads
lastSaveTimestampfrom the editor store for the new race condition protection logic.src/components/layout/StatusBar.test.tsx (1)
43-66: LGTM! Test updates align with polling implementation.The tests correctly account for the 500ms polling interval by using
waitForwith a 600ms timeout, providing adequate buffer for polling and render cycles.Also applies to: 68-92, 94-118
src/store/uiStore.ts (2)
21-21: Semantic API improvement.The
showBarsaction provides a clearer, more intention-revealing API compared to callingsetDistractionFreeBarsHidden(false). Whilst this creates some duplication, the improved semantics justify the additional surface area.Also applies to: 63-65
67-73: LGTM! Comment clarification improves understanding.The addition of "Immediately" usefully emphasises the synchronous nature of this action, distinguishing it from debounced or deferred operations.
src/components/layout/UnifiedTitleBar.tsx (1)
34-34: LGTM! Cleaner API usage.The migration to
showBarsimproves both semantic clarity and implementation simplicity. The direct function reference on line 97 is preferable to the previous arrow function wrapper.Also applies to: 97-97
src/components/layout/StatusBar.tsx (2)
13-13: LGTM! Clean adoption of the new API.The migration to
showBarscorrectly implements the updated UI store API with cleaner, more semantic code.Also applies to: 48-48
54-54: Verify the intended styling for the dirty indicator.The dirty indicator has been simplified to a plain dot with inherited colour. If the previous implementation used bold text or a primary colour to increase visibility, ensure this subtler styling is intentional and provides adequate user feedback.
Summary by CodeRabbit
Refactor
New Features
Bug Fixes