Skip to content

Refactors pre 1.0.0 release#54

Merged
dannysmith merged 10 commits intomainfrom
refactors-pre-1.0.0-release
Nov 6, 2025
Merged

Refactors pre 1.0.0 release#54
dannysmith merged 10 commits intomainfrom
refactors-pre-1.0.0-release

Conversation

@dannysmith
Copy link
Copy Markdown
Owner

@dannysmith dannysmith commented Nov 6, 2025

Summary by CodeRabbit

  • Refactor

    • Switched editor control flow to a hook-driven hybrid pattern and decoupled auto-save into a store-registered callback for more reliable, testable saves.
    • Broke up monolithic layout event handling into smaller composable wiring and explicit lifecycle hooks.
  • New Features

    • Centralised keyboard shortcuts, menu and DOM event wiring and added focused hooks for editor actions, auto-save scheduling and project initialization.
    • New editor auto-save scheduling with a short debounce.
  • Bug Fixes

    • Frontmatter panel now renders schema-defined fields first and lists extra fields alphabetically.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 6, 2025

Walkthrough

Refactors the event-driven layout glue into a Hybrid Action Hooks pattern: adds useEditorActions and focused hooks, introduces autoSaveCallback and scheduling in the editor store, removes the monolithic useLayoutEventListeners/DOM event bridge, and wires Layout to register the saveFile callback for deterministic auto-save.

Changes

Cohort / File(s) Summary
New editor actions hook
src/hooks/editor/useEditorActions.ts
Adds useEditorActions() exposing saveFile(showToast?: boolean): Promise<void> which reads store/query state synchronously, calls backend save, handles success/failure, updates store state and invalidates queries.
Editor store: callback-based auto-save
src/store/editorStore.ts, src/store/__tests__/*
Introduces autoSaveCallback state and setAutoSaveCallback API; scheduleAutoSave/setEditorContent scheduling; store delegates save invocation to registered callback. Tests updated to inject/mock callbacks.
Layout and wiring
src/components/layout/Layout.tsx
Layout now composes focused hooks, manages local preferences state, and registers saveFile into the editor store via useEditorStore.getState().setAutoSaveCallback(...).
Decomposed focused hooks
src/hooks/useKeyboardShortcuts.ts, src/hooks/useMenuEvents.ts, src/hooks/useDOMEventListeners.ts, src/hooks/useEditorFocusTracking.ts, src/hooks/useProjectInitialization.ts, src/hooks/useRustToastBridge.ts
New smaller hooks extracted from the removed god hook to handle hotkeys, menu events, DOM events, editor focus/menu state, project init and Rust toast bridge.
Removed god hook
src/hooks/useLayoutEventListeners.ts
Deleted centralised layout/event-handling hook (the previous god hook) and its internal event bridge.
Commands/context updates
src/hooks/commands/useCommandContext.ts
saveFile now sourced from useEditorActions() instead of from the editor store.
DOM & frontmatter changes
src/components/frontmatter/FrontmatterPanel.tsx
Removed cross-window schema-field-order event handling; top-level fields filter by defined schema and extra fields render alphabetically at the end.
Polling / UI tweaks
src/components/layout/StatusBar.tsx, src/components/layout/UnifiedTitleBar.tsx, src/components/editor/Editor.tsx
StatusBar now polls store.getState() every 500ms for counts/isDirty; UnifiedTitleBar and UI store use showBars() instead of previous API; only a comment changed in Editor.
File-change protection
src/hooks/useFileChangeHandler.ts
Adds 3s race-window protection to ignore external file-change events shortly after a user save.
Tests & infra updates
src/hooks/editor/useEditorHandlers.test.ts, src/store/__tests__/*, src/components/layout/StatusBar.test.tsx
Test scaffolding updated for autoSaveCallback; assertions adapted to callback pattern and polling timings.
Docs and tasks
docs/developer/architecture-guide.md, docs/developer/state-management.md, CLAUDE.md, docs/tasks-done/*, docs/tasks-todo/*
Documentation updated to describe the Hybrid Action Hooks pattern, migration plan, new tasks added, and one task doc removed.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Potential focus areas:

  • lifecycle and teardown correctness for setAutoSaveCallback in editorStore;
  • debounce/scheduling and concurrency of scheduleAutoSave and max-delay flows;
  • correctness of synchronous query reads and invalidation keys in useEditorActions;
  • hotkey/menu/DOM listener scoping and interactions after removing the god hook;
  • updated tests to ensure reliable simulation of autoSaveCallback behaviour.

Possibly related issues

Possibly related PRs

Poem

🐇 A rabbit nibble, quick and spry,

Hooks unrolled beneath the sky,
Callbacks set and toasts held dear,
No more noisy event bridge here,
Save hops home — the meadow’s clear.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Refactors pre 1.0.0 release' is vague and uses non-descriptive phrasing that does not convey the specific architectural changes made in this changeset. Consider using a more specific title that summarises the main architectural change, such as 'Implement Hybrid Action Hooks Pattern for state management' or 'Refactor event-driven architecture to Hybrid Action Hooks pattern'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactors-pre-1.0.0-release

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Reset autoSaveCallback between tests

We inject autoSaveCallback mocks in multiple specs, but the suite’s beforeEach reset never clears that field, so whatever mock the previous test left behind persists into the next run. Because the store methods call autoSaveCallback during 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 unmount

We set the store callback but never clear it, so the store can keep invoking a stale saveFile closure 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 behaviour

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6aad04 and 55e8f28.

📒 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 saveFile from the store to the useEditorActions hook, 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 void for 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 void for the async call in useEffect
src/hooks/editor/useEditorHandlers.test.ts (1)

40-41: LGTM! Mock state correctly extended for new store API.

The mock store state additions for autoSaveCallback and setAutoSaveCallback align 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 isDirty and lastSaveTimestamp on success
  • Preserves isDirty on error without throwing

The 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 fresh

Storing saveFile and onOpenPreferences in 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 management

I 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 solid

Pulling the schema ordering straight from the query cache and invalidating the relevant keys after a successful invoke keeps 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 that useCreateFile already 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 keeps useLayoutEventListeners for wiring only, reducing its scope. This coordination is well-executed.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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—lastSaveTimestamp is properly updated after save operations (in useEditorActions.ts:85). However, the hardcoded 3000 value 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.useTransition or React.startTransition to mark updates as non-urgent
  • Leverage CSS transition-behavior to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b637cd and 7a34169.

📒 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 lastSaveTimestamp from 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 waitFor with 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 showBars action provides a clearer, more intention-revealing API compared to calling setDistractionFreeBarsHidden(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 showBars improves 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 showBars correctly 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.

@dannysmith dannysmith merged commit 092349d into main Nov 6, 2025
8 checks passed
@dannysmith dannysmith deleted the refactors-pre-1.0.0-release branch November 6, 2025 15:13
@coderabbitai coderabbitai bot mentioned this pull request Nov 7, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 15, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 6, 2026
10 tasks
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